Posted 2026-05-28 · ~7 min read
JSO now ships a complete client-side supply-chain integrity surface. Four composable pieces: an HMAC-SHA256 watermark that rides through every obfuscation transform, an Ed25519-signed release attestation covering BuildId + per-file SHA-256, a pre-flight quota gate, and a bulk watermark tree scanner. The wire formats are open, the verifier helpers are reference-implemented in three languages with pinned cross-language conformance tests, and the entire surface is verified end-to-end. No new JSO endpoints; no schema migrations; no server involvement to verify.
If you've been waiting for a way to prove — cryptographically, offline, with no JSO involvement — that a particular protected file came from your CI pipeline and hasn't been tampered with since, this is that.
The four pieces, in order
1. Watermark — HMAC-signed marker, embedded in every output
Before source goes to the obfuscation API, the CLI prepends a header-comment block:
/*! __jso_watermark_v1
* tag: <base64url(utf8(tag))>
* sig: <base64url(HMAC-SHA256(key, utf8(tag)))>
*/
The obfuscator's existing KeepComment option preserves the block verbatim through string array moves, control-flow flattening, dead code insertion, member renaming — everything. The marker survives intact in the protected output.
Why a header comment and not a string literal? String literals get encoded, encrypted, and moved into the string array. A verifier would have to reverse the obfuscation to find them. Header comments don't get touched. Zero-friction.
jso-protector --config jso.config.json \
--watermark "customer-${CUSTOMER_ID}-${RELEASE}" \
--watermark-key "$JSO_WATERMARK_KEY"
Holders of the secret can verify; everyone else sees an opaque comment block. Two natural use cases:
- Anti-piracy: per-customer tag → leaked code traceable to the leaking account.
- Dispute proof: "did this code come from our pipeline or theirs?" → run the verifier with your key; constant-time HMAC compare gives a yes/no answer.
2. Release attestation — Ed25519 signature, SLSA-style
After a successful obfuscation, the CLI writes a JSON envelope alongside the existing manifest:
{
"v": 1,
"signedAt": "2026-05-27T00:00:00Z",
"manifest": {
"buildId": "...",
"polymorphismFingerprint": "...",
"label": "v1.2.3",
"files": [
{ "name": "app.js", "sha256": "<hex>" },
{ "name": "lib/dep.js", "sha256": "<hex>" }
]
},
"publicKey": "<base64 SPKI DER Ed25519 public key>",
"signature": "<base64 64-byte Ed25519 sig over canonical(manifest)>"
}
The signature is over a canonical JSON serialization of the manifest subobject (sorted keys, no whitespace, no trailing newline). This way the same logical manifest always produces the same bytes to sign and the same bytes to verify, regardless of which JSON library shipped the envelope downstream.
Verification is two-stage:
- Stage 1 — Ed25519 signature over canonical manifest. Catches forgery + manifest tampering.
- Stage 2 — re-hash every referenced file on disk; compare against the embedded SHA-256. Catches post-signing tampering of the actual artifacts even when the signature itself is intact.
Ed25519 chosen for 64-byte signatures, 32-byte keys, fast verify, no key-size choices to fumble. Node has it built-in since v12; no new dependencies. --genkey-release mints a fresh keypair so you don't have to wrangle openssl.
One subtlety worth flagging: when the verifier is given a trusted public key pin (--public-key), the envelope's embedded public key bytes MUST equal the trusted ones. This defeats a key-substitution attack where someone hands you a self-signed envelope claiming to come from your pipeline.
3. Pre-flight quota gate — --estimate
Run before the obfuscation API is called. Walks the input files, hits /v1/ai/usage to read the current month's quota, prints a three-state gate:
- OK (exit 0) — quota healthy.
- WARN (exit 0) — under 5 actions OR under 20% cost cap remaining. Print, don't block.
- FAIL (exit 1) — actions remaining = 0. Block the build before it consumes anything.
Network failure on the usage endpoint downgrades to WARN with a note rather than crashing the pipeline. Composes via shell && with the obfuscation step: estimate && obfuscate means the build can't even start if quota is exhausted.
4. Bulk forensic scanner — --scan-watermarks
Walks a directory tree (skipping node_modules / .git), finds every JS file with a watermark marker, optionally validates each with the supplied key. Exit codes are CI-friendly:
- 0 — all found watermarks valid (or no key supplied → lookup-only mode).
- 1 — at least one signature did NOT match the key (real tampering / cross-key contamination).
- 2 — no watermarks found at all (wrong directory, or build wasn't watermarked).
Use cases: CDN audit (does the prod bundle still carry your marker?); forensic dispute (which build did this leaked file come from?); CI gate (run after build, non-zero exit means something in the output isn't from this pipeline).
Cross-language verified across three implementations
Both wire formats are mirrored in Node (packages/jso-protector/watermark.js + release-signer.js), Python (jso_protector.watermark), and .NET (JsoProtector.Watermark). The test suites in each language don't just verify their own implementation — they also spawn the Node module and round-trip artifacts in both directions:
- Stamp in Python → verify in Node ✓
- Stamp in Node → verify in Python ✓
- Stamp in .NET → verify in Node ✓
- Stamp in Node → verify in .NET ✓
This is the strongest possible proof the wire format is implementable as written. A customer can stamp in their Python CI step, audit in a .NET enterprise tool, and report to a Node-based dashboard with no conversion layer.
To make future ports easy, eight conformance vector tests are pinned in packages/jso-protector/test/wire-format-vectors.test.js: ASCII tags, multi-byte UTF-8 tags (Japanese + Greek), the base64url alphabet rule, canonical-JSON properties, and a six-mutation tampering sweep. A new-language port that passes these is wire-compatible.
Composing it all in one CI step
The GitHub Action (bumped to v0.2.0) exposes the whole surface as opt-in inputs:
- uses: javascriptobfuscator/[email protected]
with:
input: dist
output: dist-protected
manifest: dist-protected/build.manifest.json
api-key: ${{ secrets.JSO_API_KEY }}
api-password: ${{ secrets.JSO_API_PASSWORD }}
estimate: 'true' # quota gate
ai-precheck: 'true' # AI compat scan
watermark: ${{ github.sha }} # forensic tag
watermark-key: ${{ secrets.JSO_WATERMARK_KEY }}
sign-release-key: ./ci-key.priv.pem # SLSA attestation
The watermark key is wired via the JSO_WATERMARK_KEY env var rather than command-line args so it doesn't appear in step logs — a real concern on CI runners that echo their commands. The action's outputs include watermark-tag and release-sig-path, so the next step in the workflow can actions/upload-artifact the signed envelope as a build artifact.
Why client-side?
The obvious alternative is "JSO signs your build for you" — we hold a signing key, every customer's artifacts are signed under it. We deliberately chose not to do that.
The point of an attestation is "this came from you", not "this came from JSO". If JSO holds the key, the attestation degrades into "JSO once accepted these bytes" — useful for service auditing, useless for proving customer-to-customer that a leak came from a specific account. Client-held keys mean each customer's attestation is their own claim about their own pipeline, and verification doesn't require JSO to be online or in business.
The same reasoning applies to watermarks: holders of the secret can verify; JSO doesn't see the secret and doesn't need to.
Read the spec, copy the recipes
- Wire format spec — exact regex, base64url rules, canonical-JSON properties. Enough detail to implement in any language.
- Cookbook recipes 15-18 — copy-paste-ready: per-customer watermarking, SLSA attestation, pre-flight gate composition, the full GitHub Action workflow.
- Roadmap entry — how this fits alongside the upcoming runtime-enforced envelopes (Beta) that gate execution rather than just proving provenance.
TL;DR. Cryptographic provenance for protected JavaScript builds, verifiable offline with no JSO involvement. HMAC watermark + Ed25519 attestation, cross-language verified across Node / Python / .NET, opt-in via 6 lines of GitHub Action workflow YAML. Wire format is open. If you're building against supply-chain compliance (SLSA, SBOM tooling, government procurement), start with the
spec.