A minified JavaScript file can be accompanied by a source map containing an exact copy of the developer's source. Omitting the map URL from the script is not enough if the map itself still ships. Release Guard splits a completed build into two new directories: a public release and a private debugging copy of its external maps. It does not execute or rewrite the application.
Use Python 3.10+ and install this checkout in a build environment
(python -m pip install .). Build into an unpublished, immutable directory.
Request external maps without a sourceMappingURL comment. For example, an
existing esbuild project can use:
esbuild src/app.ts --bundle --minify --sourcemap=external --outfile=build/app.js
python -m releaseguard split-source-maps --root build --public release-web --private debug-symbolsWith Terser, set sourceMap: { filename: "app.js", includeSources: true } and
write the returned code and map separately. Leave the url option unset.
The experiment includes a complete,
version-locked example. With other bundlers, verify the files your build really
produces before deployment. A linked or inline map will block this workflow;
configure the build to emit an unlinked external map, then run the split again.
For an input containing js/app.js and js/app.js.map:
| Destination | Contents | Use |
|---|---|---|
release-web/js/app.js |
Original generated JS bytes | Upload this public tree only to the web root or CDN |
debug-symbols/js/app.js.map |
Original map, at its matching relative path | Store outside the served tree, with access limited to authorized debugging or symbolication |
build/ |
Original build, unchanged, still containing the map | Keep in trusted staging; do not deploy it |
Both output paths must be fresh, disjoint directories with existing, trusted parents. Use a new pair for each build. Only act on the outputs after success. Normal errors are rolled back. A crash or an interrupted filesystem is not a two-directory atomic transaction; discard incomplete staging before publishing. The command does not update your host, CDN, HTTP headers or application.
The splitter applies its own fixed checks. If you use a saved project policy,
point its release root at the new public directory and run check or prepare
there as usual. Keep your private maps out of that signed public release.
- Splits unpacked regular files with a
.mapsuffix (case-insensitive) into the private tree; expects flat v3 maps. Indexed maps, binary/linker maps and other unsupported map layouts do not silently pass. It does not fetch source URLs. - Checks the copied public inventory and SHA-256 hashes, runs the portable release scanner, and blocks recognized data such as map markers, disguised v3 JSON maps and private-key PEM markers. It also compares public file hashes with embedded source text to catch an exact loose copy.
- The split profile also rejects the literal token
sourcesContentin public files, in UTF-8, UTF-16 LE/BE and UTF-32 LE/BE. This catches a real packaging mistake: bundling the map as a JavaScript object can leave all source text in a normal module, even after removing the external file. The check is deliberately conservative: a library or documentation that merely mentions this property can also be rejected. Inspect the reported path; ordinaryscan-releasepolicies have not changed. Computed property names, transformed source and arbitrary encodings are outside this check. - Operates before compression. Recognized archives/disabled archive inspection and precompressed assets covered by the policy block publication. It is not a decoder for every container or arbitrary encoding.
- Rejects overlapping paths, links/reparse points and special files. Uses bounded input and fixed checks. Keep the inputs and ancestor directories trusted for the duration of the build. Protect private maps with appropriate filesystem permissions or ACLs; they may contain the full source.
The current profile allows 64 MiB per input file, 256 MiB total and 8 MiB for a JSON-like file inspected as a possible map. It rejects unsupported inputs instead of giving partial success. On POSIX, the private directories and files are created with permissions at most 0700/0600. Ordinary rwx bits of public files and directories are preserved; privileged bits and platform ACLs are not copied. Set the intended deployment ACLs explicitly on Windows.
Exit 0 means both checked outputs were produced. Exit 3 means findings
blocked the split. Exit 2 means invalid/unsupported input, an I/O error or
an incomplete check. Never publish after a nonzero exit. A map-free input is
not reported as a successful mitigation by this command; use ordinary check
when auditing builds that did not generate maps.
Store the private output with the exact release identifier or the deployed
bundle digest. Give your debugging service that map over a private channel.
Do not upload maps to a public prefix, use the debug directory as a fallback
route, or leave older maps in a CDN cache. Check production SourceMap and
X-SourceMap response headers, as well as predictable sibling URLs such as
/js/app.js.map. Deploy the public directory into a fresh release prefix
or remove stale objects; overlaying it on yesterday's directory leaves old maps
reachable. The offline split cannot verify your live deployment.
The source review identifies the relevant
operations in REA and ArtCraft's Craft Anything. In the recorded Craft
experiment, the official Linux
v0.5.0 CLI runs js-sources-restore in Deep mode on one project-owned fixture.
It reformatted the same bundle on every run, with complete coverage:
| What Craft received | Exact original returned |
|---|---|
Ordinary release, including the adjacent app.js.map |
1 file with the known source SHA-256 |
Split public release, with the same app.js |
0 files |
| Same public release, with the private map supplied again | 1 file with the original SHA-256 |
The published REA 6.8.0 CLI was also exercised through
analyze-javascript-application on both directories, without an AI model.
The ordinary graph had 9 nodes including a map and an original-source node
with the known source digest. The public graph had 7 nodes, no map and no
map-backed original. Both explicitly reported complete, untruncated graphs,
one parsed JS file with the same digest, and zero JS skips or parse errors.
REA reports the embedded source's digest in this operation; Craft and the
independent consumer below check the actual text. REA report
· successful paired tool job
at the measured revision.
The trace-mapping observation
also checks the actual source text and restores the original location from the
private map. In a separate negative control Terser compiles the same map into
a JavaScript module containing its source text and the unquoted key
sourcesContent. Splitting that build returns exit 3, identifies
repacked.mjs, and leaves neither output directory. The normal generated
JavaScript is byte-identical and gives the same
results for four fixture inputs. This is exact source recovery through a
specific shipped artifact. Generated logic remains available to the user;
the experiment does not establish resistance to behavioral reconstruction,
native decompilation, runtime observation or a complete AI agent session.
Use an installed Release Guard Python environment and Node 24.22.1 (the CI version). The npm dependencies below are for experiments only. From this checkout:
npm ci --prefix benchmarks/recovery-tools --omit=optional --ignore-scripts --no-audit --no-fund
node benchmarks/source_map_recovery.mjs --work ./map-fixture --python "$(command -v python)" --out ./rea-result.jsonUse fresh work/report paths. This builds the synthetic fixture, invokes the
installed split command, runs the published REA CLI on ordinary and public
directories, and checks original content/positions with trace-mapping. A
provider error or incomplete graph cannot pass. For an explicit consumer-only
check, add --consumer-only; its report does not claim a REA run.
Download the pinned Linux x86-64 Craft release, then use the same fixture:
python benchmarks/craft_source_map_boundary.py --archive ./craft-anything-0.5.0-linux-x86_64.tar.gz --baseline ./map-fixture/baseline --public ./map-fixture/public --private ./map-fixture/private --output ./craft-result.jsonThe script verifies both the archive and the CLI member by SHA-256 before use. Only project-owned synthetic inputs are used. The consumer and Craft results are distinct from the REA CLI evidence; no model credentials are required.
构建时生成单独的 .map,让它留在构建目录,不要把映射地址或内联图嵌进
JS。执行上面的 split-source-maps 命令后,只发布 release-web/;
debug-symbols/ 供受限的调试或错误跟踪服务使用,原始 build/ 也不能上线。
两个输出目录都必须是新目录,先分离,再压缩或打包。部署到已有站点时请同时
清理旧的公开 map 和 CDN 缓存,检查源映射响应头。
这一措施阻断的是用发布包中的 source map 原样取回源码。三组实验中相同工具都 正常完成、同一份 JS 都能格式化;授权调试方拿回私有图仍可恢复原文。它不需要 模型合作,也不会给应用增加启动检查、后台任务或运行库。复制和检查发生在构建 阶段,会产生磁盘和 CPU 开销。