js-harvest
Crawl in-scope target, download every JS file + public sourcemap. Enumerates script tags, dynamic imports, webpack chunks, and .js.map siblings. Run after target-init when targets/<name>/raw/ is missi
SKILL.md.claude/skills/js-harvest/SKILL.md
GitHubjs-harvest
Purpose
Acquire every JavaScript asset the target ships, plus every sourcemap that is publicly accessible, so later phases can analyze original sources rather than minified bundles.
Inputs
targets/<name>/status.jsonwithscope.inpopulated.
Steps
Read scope. For each in-scope host, queue the bare host root as the first URL.
Use
chrome-devtoolsMCPnavigate_page, thenlist_network_requestsandevaluate_scriptto enumerate:- every
<script src>(including async/defer) - every dynamic
import()(regex pass on inline JS as fallback) - every webpack chunk URL referenced by the first script
- every
For each JS URL, fetch through chrome-devtools (so cookies + proxy apply). Save under
targets/<name>/raw/<host>/<path>.js.Sourcemap discovery for each JS:
//# sourceMappingURL=...pragma at file endSourceMap/X-SourceMapresponse header- try
<url>.mapif pragma/header absent
Save discovered maps as siblings:
<path>.js.map. If a map is referenced but returns 4xx, log tostatus.json.harvest.missing_maps[].Crawl follow-ups: respect a depth limit (default 2 hops on in-scope hosts only). Do not follow out-of-scope links — log them to
status.json.harvest.out_of_scope_skipped[].Hidden-map + Sentry recon (T1.5). Run:
python3 bin/sourcemap_recon.py targets/<name>This brute-forces common Webpack/Vite/Next/Nuxt hidden-map filenames next to each bundled
.js, and pulls.mapfiles from any in-scope Sentry release-artifact API. Brute-list and Sentry recipe are maintained inwiki/tools/tlx/sourcemap-recon.md(the spec). The script reads scope fromstatus.json.scope.inand refuses out-of-scope hosts. Per-bundle attempt cap defaults to 40; 429s back off automatically. Sentry phase can be skipped with--no-sentry.After this phase, re-run
sourcemap-explodeto decode any newly recovered.js.mapfiles intotargets/<name>/sources/.
Outputs
targets/<name>/raw/<host>/<path>.js[.map]- updated
status.json.phases.harvest - updated
status.json.phases.sourcemap_recon(Step 7 only)
Failure modes
- chrome-devtools MCP not responding → suggest
chrome-devtools-mcp:troubleshooting. - Auth-walled site (login redirect) → load Caido auth workflow first
(
caido_login(workflow_id=...)), then re-run. - Hitting rate limits → back off; log; resume.
sourcemap_recon429-storm against a single host → script auto-pauses per attempt; if persistent, raise--rate-limit-ms(default 200) or drop--max-attempts-per-bundle.- Sentry artifact API returns 401/403 (the common case for self-hosted with auth) → script silently skips; only misconfigured public instances enumerate.
Result block
"phases": {
"harvest": { "status": "done", "ts": "<iso>",
"files": N, "maps_public": M, "maps_missing": K,
"hosts_seen": [...], "out_of_scope_skipped": [...] },
"sourcemap_recon": { "status": "done", "ts": "<iso>",
"bundles_processed": N, "maps_recovered": M, "maps_missing": K,
"requests_total": R, "out_of_scope_hosts_skipped": [...],
"sentry": { "hosts_seen": [...], "releases_enumerated": [...] },
"found": [{ "url": "...", "saved_to": "...", "bundle": "..." }] }
}