Changelog
This page tracks user-facing changes to js-recon, mirroring the dev-branch CHANGELOG.md in the tool's own repository.
2.1.0-alpha.3 - 2026-11-09
Added
taintwire build -o <file>.twwrites one PookieGraph per unit instead of one graph of every file: each file plus the files that define the modules it loads through the webpack or Turbopack registry, transitively. The unit graphs and amanifest.jsonare packed into one.twfile (a tar archive), andtaintwire analyze -g <file>.twruns the rules on each unit's graph in turn, reporting a finding once even if several units contain it. A.twsaves memory only when every unit is much smaller than the app; in most framework builds the shared framework and vendor chunks are loaded into every unit, so it is larger and slower than a.pg. The default output staysgraph.pg. A.twwhosemanifest.jsonis missing, malformed or empty, or names a file outside the archive, exits with39. (taintwire)--buffer-pool <MB>and--threads <n>ontaintwire buildandtaintwire analyzeset the memory taintwire's database may use per graph and its query threads. (taintwire)
Changed
- taintwire is updated to 0.2.0-alpha.3. Its database now takes at most half of the memory the process may use, which is the container's limit inside Docker or Kubernetes. Before, the limit was sized from the host's RAM even inside a container, so a large app could grow past the container's limit until the container killed the process; past the new limit, a load or query fails with
The buffer pool is full. Queries run on 2 threads instead of one per core of the host, which lowered the peak CPU oftaintwire analyzeby about a third with the same run time. Query results are freed as soon as they are read, which lowers memory when graphs are built one after another. (analyze,map,run,taintwire)
2.1.0-alpha.2 - 2026-11-06
Security
npm auditis clean again; it had reported 51 vulnerabilities (32 high).zodmoves to 4.6.7: no 3.x release fixes the memory-exhaustion advisory, andjimp, whichcli-print-imguses, is overridden to the same version.undicimoves to 7.30.2. Transitive@hono/node-server,body-parser,fast-uri,glob,ip-address,minimatchandqsare updated, anddeepmergeis pinned to 4.1.2, which predates the prototype-poisoning advisory. (all)
Added
- New
taintwirecommand builds a PookieGraph (a.pgfile) once and queries it with.pgqfiles.taintwire build -d <dir> -o graph.pgloads every JS file of a directory (for example lazyload'soutput/<host>) into one taintwire graph. The files are linked through the webpack or Turbopack module registry, so a value can flow from one chunk into another.taintwire analyze -g graph.pg -q <file/dir>runs the rules in.pgqquery files against it and writes the findings totaintwire.json(theanalyze.jsonshape).taintwire validate -q <file/dir>checks query files without a graph. A.pgqfile is a TypeScript module of Cypher selectors and rules, evaluated in a separate, locked-down Node.js process that can read only its query pack (Node.js 22.15 or newer); the taintwire-queries repository is the public pack. Each finding'smessageis the rule's own message. A rule that fails while running is reported and the other rules still run; the command then exits with41. New exit codes37to41. (taintwire) - New rule type
taintwireand the--analyze-taintwireflag onanalyzeandrun. A taintwire rule's steps are Cypher queries over a taintwire graph of eachmapped.jsonchunk: a step's row ids are passed to later steps as$<step name>, and a step withtaintFromkeeps only the rows that a value from that step flows into (strict, lenient or deep, with an optionalmaxHops), following variables, function arguments and return values. An optional rule-levelprefilterregex skips chunks before they are loaded. The rules run through taintwire's pgq runner (runPgq). They are opt-in because loading the graphs is slow: without the flag,analyzeprintsSkipped N taintwire rule(s). AtaintFrommust name an earlier step andmaxHopscan be0; a step whoserequiresnames no earlier step gives no findings. Chunks over 8 MB are skipped with a warning, likemap --map-taintwirefiles. When a taintwire rule fails, the other rules' findings are still written and the command exits with41(runstill writes the report). js-recon-rules 2.0.0 ships 31 of them. (analyze,run) - New Next.js lazyload method
next_interceptRouteNextUrlcovers intercepting routes defined under a path that middleware guards, such asapp/admin/@modal/(..)pub/[id]. Next.js serves such a route through its public source route when the request sends aNext-Urlheader naming the guarded path, and the middleware matcher only sees the public URL. The method reads the intercepting-route rules from_buildManifest.js(__rewrites.beforeFiles, thenext-urlcondition). When the crawl hasn't found that file, it builds the URL from the buildId in the landing page. Each rule's parent path and every crawled,strings-extracted or--wordlistpath gets a plain request. A path that redirects then gets the same path and its/_next/data/<buildId>/<path>.jsonroute withx-nextjs-data: 1. The path counts as middleware-guarded if a response carries a header only middleware sets (anyx-middleware-*,x-nextjs-redirectorx-nextjs-rewrite). For each rule whose condition matches a guarded path, the public route is requested withNext-Url: <parent>, and chunk paths that are missing from the plain response are downloaded. Without a manifest, the method tries every pair of guarded path and public page, up to 200 pairs. Manifest strings that Next.js hoisted into the manifest function's parameters are resolved, onlyheaderconditions count, and a manifest that doesn't parse falls back to the pairs mode. Each matching guarded path is also tried asNext-Url, next to the rule's parent path. Data routes keep thebasePath, new chunks resolve against the asset prefix orbasePathof the known chunk URLs, and the crawl target is always a candidate. The rule'snext-urlregex comes from the target, so each match runs with a 100 ms time limit. It runs afternext_routerStateForgeon every Next.js lazyload pass, and--include-methods/--exclude-methodscontrol it. (lazyload,run)
Changed
- taintwire is updated to 0.2.0-alpha.2. (
analyze,map,run,taintwire) --wordlistentries now also feednext_interceptRouteNextUrl, as well asnext_routerStateForge. (lazyload,run)
Fixed
mcp --serverno longer prints the banner. The banner is an image written to stdout, which is the server's JSON-RPC channel, so MCP clients got a corrupted stream. (mcp)
2.0.1-beta.1 - 2026-08-17
Added
- New
nuxt-devtech: detects a Nuxt.js dev server (nuxi dev/npm run dev) as a distinct tech from a productionnuxtbuild, mirroringnext-dev/vue-dev/svelte-dev/react-dev/angular-dev(#194). Nuxt 3/4 (Vite) and Nuxt 2 (webpack) dev-server markers were both confirmed live this round — unlike several sibling*-devwebpack branches.checkNuxtDevServer.tslooks for Nuxt's own/_nuxt/-prefixed Vite client path (/_nuxt/@vite/client, distinct from vanilla Vite's root/@vite/client) plus a weak__NUXT_DEVTOOLS_TIME_METRIC__inline-script signal for Nuxt 3/4, and an unhashed_nuxt/app.jsentry filename plus a/__webpack_hmrintercepted-request pathname for Nuxt 2. The gate is checked only aftercheckNuxtJSmatches, at both the HTML-level check and the intercepted-URL fallback. No dedicated crawler was needed — the existingnuxt_js/*pipeline already handles dev mode once two shared (not dev-specific) bugs were fixed:nuxt_stringAnalysisJSFiles.ts's literal-path scanner previously matched only a bare.jssuffix, missing every cache-busted (?v=<hash>) Vite import and every raw.mjs/.ts/.vuedev-mode source reference; and a malformed/oversized literal (for example, an entire module's source embedded in a webpackeval-devtool string) could throw inside the per-file resolver and permanently block that file from ever being marked analyzed, spinning the discovery loop forever. A third, general bug (affecting every*-devwebpack branch, not just Nuxt's) was also fixed:techDetect/index.tsno longer reads the full response body of an interceptedtext/event-streamconnection (for example, a webpack-hot-middleware HMR socket), which previously hung tech detection indefinitely against any webpack dev server with an open HMR connection.run's tech allow-list and pipeline condition were extended accordingly (Nuxt's map/analyze calls already hardcode"vue"regardless of dev/prod, so no new remap arm was needed). Benchmarked at effectively complete recovery for both Nuxt generations against an independentkatanacrawl (Nuxt 3: 176/177 URLs, the one miss being a Vite-internal-only file never fetched as app content; Nuxt 2: 0 missed, full overlap) and cross-checked with ajsr-network-benchdual-capture pass showing zero real discovery gaps. (lazyload,run)
Fixed
run's generated OpenAPI spec and Postman collection now use the actual target URL instead of a placeholder:openapiGenerator.ts'sservers[].urlandpostmanGenerator.ts'sbaseUrlvariable read a newtargetUrlglobal (src/utility/globals.ts), set from the-uvalue at the start ofprocessUrl, falling back to the previous{{baseUrl}}/https://example.complaceholder if unset.setTargetUrlstripsusername/password/search/hashbefore storing, so a-uvalue carrying basic-auth credentials or a signed query token doesn't leak into the generated artifacts. (run,openapi,postman)- Nuxt 2 dev-server detection (
checkNuxtDevServer.ts) no longer misclassifies a production build asnuxt-dev: the unhashed-entry marker now requires the full/_nuxt/app.jssuffix (was matching anyapp.jsbasename, for example,/assets/app.js), and the webpack-HMR marker requires a complete__webpack_hmrpath segment instead of a bare substring match. techDetect/index.ts's event-stream content-type check is now case-insensitive, so a server returningText/Event-Stream; charset=utf-8(mixed case, with parameters) still gets skipped instead of hanging tech detection on that persistent HMR connection.refactor -t react-webpack's webpack-async-chunk-loading rewrite (requireParam.e(N).then(requireParam.bind(requireParam, N))→ dynamic import) now emits a realImportExpressionnode (t.importExpression) instead of aCallExpressionwith anImport-type callee — the latter is Babel 7's legacy shape and is no longer a valid expression under the@babel/typesv8 migrated in this same release, which silently brokerenameRouteComponents' route-component renaming for any React-Router lazy route using this webpack pattern.- AI SDK clients (
src/utility/ai.ts) are now constructed lazily on first use instead of at module load. Theopenaidependency's v7 line changed itsOpenAIclient constructor to throw immediately when noapiKeyis configured, which crashed every js-recon invocation on import — not just runs that actually use an AI feature (mcp's AI chat,analyze's AI-assisted rule helpers) — for any user who hadn't set an AI API key. The OpenAI, Ollama, and Anthropic clients are now held behind??=-memoized getters and only instantiated the first timeai()resolves a provider, so an unconfigured AI provider is inert until actually selected. (ai) - Migrated
@babel/types,@babel/parser, and@babel/traversefrom v7 to v8 (^8.0.4), dropping the now-stale@types/babel__traversepin that was shipping a duplicate nested v7 type copy. Beyond the mechanical fallout of v8 dropping default exports (namespace imports) and no longer CJS-interop-wrappingtraverse/generator(removing the(_x.default ?? _x)unwrap pattern) across roughly 120 call sites, the upgrade carried one real functional risk that was caught and fixed in the same change: v8's parser now emits a dynamicimport(...)call as anImportExpressionnode directly, rather than the v7 shape (aCallExpressionwith anImport-type callee). Four dynamic-import detection call sites still checked for the old shape —angular_recursiveChunkImports.ts,vue_jsImports.ts,react-vite's andrefactor/react/transform.ts's import-rewriting — and would have silently stopped matching any dynamic import in every affected pipeline had they not been updated to also recognizeImportExpression. Verified with a cleantscbuild, the full unit suite, an end-to-endrunagainst local targets, and arefactor -t react-webpackpass against a real code-split React bundle. (lazyload,map,analyze,refactor,endpoints,strings)
Security
- CI workflow checkouts that don't perform an authenticated git operation (
codeql.yml,plumber.yml,pr_checker.yml, the smoke-test workflows, most jobs inbuild-and-prettify.yaml/promote-js-recon.yml) now setpersist-credentials: false, so the ambientGITHUB_TOKENisn't left in.git/configfor later steps or third-party actions in that job to read.
Fixed
@babel/generator(imported directly bymap'sesqueryinteractive command) is now declared as a directdependenciesentry instead of only being present transitively via@babel/traverse/@babel/core.
2.0.1-alpha.3 - 2026-08-14
Added
- New
vue-dev,react-dev,svelte-dev, andangular-devtechs: each detects a dev server (Vite/webpack-dev-server for Vue and React, always-Vite for SvelteKit, and either the esbuild/Vite or legacy webpack builder for Angular) as a distinct tech from its production build counterpart, mirroring the existingnext-devtech.lazyloadandrunreuse each framework's existing crawler pipeline, with small additive fixes (import-following, filename sanitization, and chunk-map parsing) closing the gaps between dev-server output and production-build output.map/analyzeremap each*-devtech to its production equivalent so existing rules keep working unchanged.
Fixed
lazyload's Svelte/SvelteKit inline-<script type="module">boot-pattern scan now also matches a static ESimport ... from "...js"declaration, in addition to the dynamicimport("...")call form it already handled. Some SvelteKit builds bootstrap the client via a static import instead of a dynamic one; since the entry chunk seeds the entire downstream chunk-discovery graph, missing it caused the whole crawl to collapse to a single JS file even though framework detection succeeded.
Security
- Removed the
extract-zipdependency, which carried a high-severity, unfixed advisory (GHSA-jmr9-qjv8-65gv, unvalidated symlink path traversal on archive extraction) with no patched release available upstream.analyze's rules downloader now fetches thejs-recon-rulesrelease as a tarball and extracts it with the already-depended-on, actively maintainedtarpackage instead.
2.0.1-alpha.2 - 2026-08-12
Fixed
- The response cache is now backed by a single SQLite file (
.resp_cache.db) instead of the previous JSON-based formats.--cache-file's default changed from.resp_cache.jsonto.resp_cache.dbacrosslazyload,run, andload. This is a clean break, not an in-place migration — an existing.resp_cache.json/.entries/directory from a prior version is left untouched but never read again; the first run after upgrading simply starts with an empty cache. load(Caido/Burp import) now writes each imported entry directly into the SQLite cache instead of building one large in-memory JSON object and serializing it in a single write at the end. This also removes the previous "cache too large to serialize as one JSON string" failure mode entirely.lazyload's cache pre-creation step now opens the SQLite cache database up front instead of writing an empty JSON stub; a failure to open it is now a fatal error with a dedicated exit code (32) — see Exit Codes.
Security
- Removed the
fsnpm package from dependencies. It was never referenced by the codebase and sat unused inpackage-lock.jsonas an extraneous entry; thefspackage on npm (distinct from Node's builtin module) is flagged as malicious by OSV (MAL-2025-21003). - Enabled Dependabot version updates for the npm, Docker, and GitHub Actions ecosystems, opening weekly pull requests against
devso outdated dependencies surface automatically instead of only being caught at release time.
Added
lazyload/runnow decode inline (base64 or percent-encoded)data:URI sourcemaps found in//# sourceMappingURL=...comments — for example, Vite'sbuild.sourcemap: 'inline'or webpack'sdevtool: 'inline-source-map'/eval-source-map— instead of treating the reference as a fetchable URL and losing the sourcemap entirely. Handled for every framework the crawler supports: React, Vue, and Svelte gained adata:branch; Next.js, Angular, and Nuxt gained sourcemap scanning entirely, where they previously had none.- Release CI now runs a dependency-audit job before publishing to npm, blocking the release if any dependency is a known malicious package.
report.htmlhas a fancier, dark-mode-aware UI: a light/dark theme toggle (defaults to the OS color scheme, persisted vialocalStorage) restyles the navbar, collapsible sections, and the findings/mapped views; severity values render as colored badges.report.htmlno longer depends on any CDN to render correctly, including when opened directly viafile://.- New
-H/--header <name: value>flag for authenticated scanning: repeatable, parses each value asName: Valueand applies it to every outbound request path (direct, SOCKS, HTTP, Oxylabs, AWS, and Puppeteer page loads). Available onlazyload,run, andexploit.