Documentation

Guides for protecting production JavaScript

Reference guides for release workflows, command-line usage, cross-file protections, and the desktop app.

Inside The Docs

Practical guides, not placeholder pages.

How-to guides Start with release sequencing and command-line usage, then move into feature-specific references.
Advanced protection Browse cross-file controls like Replace Globals and Protect Members when a build spans multiple scripts.

Cookbook

  • Teams already past the quick-start
  • Self-contained recipes — pick the ones you need

Each recipe below is a copy-pasteable answer to a real question that comes up in support. They assume you've finished the npm CLI quick-start; if not, start there.

1. Tag protection runs with the current git tag (not just commit SHA)

Customers triaging a bug report ask "which release was that?" and want a human-readable tag, not a 40-char hex. The CI templates default to $COMMIT_SHA; override to use the closest tag instead:

# In GitHub Actions:
- run: |
    LABEL="$(git describe --tags --always --dirty)"
    npx jso-protector --config jso.config.json --label "$LABEL" \
        --manifest dist-protected/jso-manifest.json \
        --report   dist-protected/jso-report.json

The label is forwarded as ReleaseLabel on the API request and shown in the JSO dashboard audit log. Use whatever format groups well for your team — v3.4.1, 2026-05-20-1432, release/q2-frontend.

2. Protect only the files that actually need it

Maximum-mode protection is slow to download. Vendor bundles, polyfills, and framework runtime files don't carry business logic and don't need to ship through it. Limit the input via include / exclude:

// jso.config.json
{
  "input": "dist",
  "output": "dist-protected",
  "preset": "maximum",
  "extensions": [".js"],
  "exclude": [
    "**/vendor/**",
    "**/polyfills*.js",
    "**/runtime*.js",
    "**/framework*.js",
    "**/*.map"
  ],
  "copyAssets": true,
  "assetExclude": ["**/*.map"]
}

Run npx jso-protector --dry-run --json --config jso.config.json first to confirm the file list before any source goes to the API. If the dry run lists 800 files when you expected 12, your exclude list needs tuning.

3. Blue-green API-key rotation

Rotating an API key while staying online: generate the new one in the dashboard, set both old and new in your CI secrets, run two protection runs in parallel, verify the new one produces working output, then retire the old key.

# Step 1: provision the new key as JSO_API_KEY_NEXT, leave JSO_API_KEY pointing at the old one.

# Step 2: run a side-by-side protection. Identical input, two keys, two reports.
JSO_API_KEY=$OLD_KEY JSO_API_PASSWORD=$OLD_PWD \
    npx jso-protector --config jso.config.json --report dist-old/jso-report.json
JSO_API_KEY=$NEW_KEY JSO_API_PASSWORD=$NEW_PWD \
    npx jso-protector --config jso.config.json --report dist-new/jso-report.json

# Step 3: BuildIds will differ — that's expected. The PolymorphismFingerprint will differ too
# because every build is polymorphic. The smoke test that matters is whether each protected
# dist boots in your test harness.

# Step 4: flip CI to JSO_API_KEY_NEXT, revoke the old key in the dashboard.

Don't compare PolymorphismFingerprint across runs — polymorphism by design produces different fingerprints. Do compare EnabledOptions to confirm both keys hit the same plan tier.

4. Inject BuildId as a runtime global for crash reports

So that a production crash report carries the right BuildId without any client-side guesswork:

# In CI, after the protect step:
BUILD_ID="$(jq -r '.Report.BuildId' dist-protected/jso-report.json)"

# Prepend a one-liner declaring the global before every protected file.
for f in dist-protected/*.js; do
    { printf 'window.__JSO_BUILD_ID__=%s;\n' "$(jq -nR --arg b "$BUILD_ID" '$b')"; cat "$f"; } > "$f.tmp"
    mv "$f.tmp" "$f"
done

At runtime, your error reporter sees window.__JSO_BUILD_ID__ and tags the crash with it. When the crash arrives at support, the symbolication script looks up jso-report.json by BuildId and demangles the stack with jso-symbolicate.

The same idea works for Sentry's release:

Sentry.init({
    dsn: "...",
    release: window.__JSO_BUILD_ID__ || "unknown",
    beforeSend: createSentryEventProcessor(lookup)  // jso-symbolicate
});

5. Run a separate prerelease protection channel

Beta / alpha / nightly channels can use a less aggressive preset to keep iteration fast, while production uses Maximum:

{
  "scripts": {
    "protect:prerelease": "jso-protector --config jso.config.json --preset balanced --label \"${GIT_COMMIT}-pre\"",
    "protect:release":    "jso-protector --config jso.config.json --preset maximum  --label \"${GIT_COMMIT}-rel\""
  }
}

The audit log groups by ReleaseLabel, so prerelease vs release builds stay separate in the dashboard.

6. Gate the full protection on a passing dry-run

Save quota and avoid surprises by running --dry-run as a fail-early gate before the real protection. The dry run hits the API in shape-validation-only mode — no source ships, no protection is performed.

# Stage 1: cheap validation. Fails fast on config errors, missing files, or credential trouble.
npx jso-protector --config jso.config.json --release-check --json

# Stage 2: only if stage 1 passed, do the real run.
npx jso-protector --config jso.config.json --label "$GIT_SHA" \
    --manifest dist-protected/jso-manifest.json \
    --report   dist-protected/jso-report.json

Every shipped CI template runs both stages in this order. The pre-commit hooks (jso-release-check + jso-dry-run) only run stage 1 — never the full protect — for the same reason.

7. Route runtime tamper beacons into your existing SIEM

Runtime Defense beacon callbacks post to a customer-controlled URL when the integrity check fails. The cheapest production wiring is a 20-line server endpoint that translates the beacon into a Datadog Event / Sentry message / PagerDuty page:

// pseudo-Express
app.post("/jso-beacon", express.json(), (req, res) => {
    const { buildId, fingerprint, reason, userAgent } = req.body;
    dd.event({
        title: `JSO tamper beacon: ${reason}`,
        text:  `BuildId=${buildId} UA=${userAgent} fp=${fingerprint}`,
        alert_type: "warning",
        tags: ["jso", `release:${buildId}`, `reason:${reason}`]
    });
    res.sendStatus(204);
});

Set the beacon URL via RuntimeDefenseBeaconUrl in your jso.config.json or as an API option. The hosted Threat-Monitoring Dashboard is on the roadmap — until it lands, route the beacons through your existing alerting stack.

8. Protect a polyglot monorepo from one CI job

A repo with several JS apps under apps/<name>/dist each needs its own protection run. Iterate in CI:

for app in apps/*/; do
    name="$(basename "$app")"
    if [ -f "$app/jso.config.json" ]; then
        npx jso-protector \
            --config "$app/jso.config.json" \
            --label "${GIT_SHA}-${name}" \
            --manifest "$app/dist-protected/jso-manifest.json" \
            --report   "$app/dist-protected/jso-report.json"
    fi
done

Each app's label is suffixed with the app name so the audit log groups by app. Reports stay alongside each app's protected dist.

9. Force a new BuildId without a code change

Useful when you suspect a runtime guard was tripped by a stale cached bundle and want to know if a fresh one fixes it. Every protection call is polymorphic, so re-running yields a different BuildId and PolymorphismFingerprint automatically — no source change required:

# Same input, second run = different protected output bytes.
npx jso-protector --config jso.config.json \
    --label "$(date -u +%Y%m%d-%H%M)-cachebust" \
    --report dist-protected/jso-report.json

The PolymorphismFingerprint in the report proves divergence. If your CDN keys cache entries by file content hash, the new build invalidates automatically.

10. Keep reports forever, drop protected source after 30 days

Protected dists are big; reports are small. You need the reports forever (for stack-trace symbolication), but the protected source is reproducible from the original source + the same build pipeline, so it doesn't need long-term archive. Split the artifacts:

# In CI, after protect:
aws s3 cp dist-protected/jso-report.json \
    "s3://jso-reports-permanent/$BUILD_ID/jso-report.json"
aws s3 cp --recursive dist-protected/ \
    "s3://jso-builds-30day/$BUILD_ID/"

# Apply an S3 lifecycle policy to jso-builds-30day that expires objects after 30 days.
# jso-reports-permanent keeps everything indefinitely.

Two years from now, a customer ticket comes in with a stack trace from BuildId=rel-abcdef123; you pull jso-reports-permanent/rel-abcdef123/jso-report.json and demangle the stack with jso-symbolicate without needing the protected JS bytes at all.

JSO AI recipes

The recipes below compose the new JSO AI endpoints with the existing JSO surface. Today they exercise preview mode; from 2026-Q3 the same code paths benefit from LLM-backed responses without a config change.

11. Block a CI build on compatibility findings

Run compat-check against your built JS before protection. Fail the CI job on any severity:error finding. Stops broken builds from ever reaching the protection step (and using your JSO API quota for files that won't run).

# In CI, after `npm run build` and before `jso-protector`:
RESPONSE=$(curl -fsS -X POST https://www.javascriptobfuscator.com/v1/ai/compat-check.ashx \
    -H "Content-Type: application/json" \
    -d "{
      \"APIKey\":    \"$JSO_API_KEY\",
      \"APIPwd\":    \"$JSO_API_PASSWORD\",
      \"source\":    $(jq -Rs . < dist/app.js),
      \"framework\": \"react\"
    }")

ERRORS=$(echo "$RESPONSE" | jq '.report.summary.errors')
if [ "$ERRORS" != "0" ]; then
    echo "::error::compat-check found $ERRORS error(s) — see report above. Fix before protecting."
    echo "$RESPONSE" | jq '.report.findings[] | select(.severity=="error")'
    exit 1
fi

The point of running before protection (not after) is that compat-check tells you what would break under obfuscation. Fixing the issues in the source costs a few minutes; debugging the same issues in mangled output costs an afternoon.

12. Sentry beforeSend with auto-diagnose

Wire jso-symbolicate + explain-error into the same beforeSend callback. Every Sentry event arrives demangled and tagged with the JSO transform that likely caused the issue. Full pattern at AIErrorRouting.aspx; one-liner:

Sentry.init({
    beforeSend: async (event) => {
        const demangled = createSentryEventProcessor(lookup)(event);
        const r = await fetch("/api/jso/explain-error", {
            method: "POST",
            body: JSON.stringify({ error: demangled.exception.values[0].value }),
        }).then(r => r.json()).catch(() => null);
        if (r && r.ok) {
            demangled.tags = { ...demangled.tags, jso_diagnosis: r.explanation.transform };
            demangled.extra = { ...demangled.extra, jso_diagnosis: r.explanation };
        }
        return demangled;
    },
});

Server-side proxy at /api/jso/explain-error recommended — never expose your JSO API key in the browser. The AI augmentation is additive: if the explain-error call fails or quota's exhausted, the demangled event still ships.

13. Monitor your AI quota from your own dashboard

The dashboard widget at /dashboard/AIUsage.aspx shows the live counters, but you usually want the numbers in your own observability stack (Datadog, Prometheus, PagerDuty). Poll /v1/ai/usage.ashx from a small cron job and emit a metric. Polling the JSON endpoint does not itself count against actionsCap.

# Example: emit "jso.ai.actions_used" as a custom Datadog metric every hour.
USED=$(curl -fsS -X POST \
    https://www.javascriptobfuscator.com/v1/ai/usage.ashx \
    -H "Content-Type: application/json" \
    -d "{\"APIKey\":\"$JSO_API_KEY\",\"APIPwd\":\"$JSO_API_PASSWORD\"}" \
    | jq -r '.actionsUsed')
echo "MONITORING jso.ai.actions_used:$USED|g" | nc -u datadog-agent 8125

# Alert at 90% of cap.
CAP=$(curl -fsS -X POST .../usage.ashx ... | jq -r '.actionsCap')
if [ $((USED * 100 / CAP)) -ge 90 ]; then
    echo "JSO AI quota at 90%" | mail -s "jso quota alert" ops@example.com
fi

The full response envelope (tier, actionsUsed / actionsCap, tokensUsed / tokensCap, approxCostCents / costCapCents, quotaRejections, asOfUtc) is documented in the API reference. previewMode: true indicates either pre-GA defaults or that the AI schema isn't provisioned in your account yet — the wire format is identical either way, so your client code does not need to branch on it.

Prometheus shortcut: if you run node_exporter with the textfile collector, drop in our zero-dependency exporter and you're done.

# Download once.
curl -fsS -o /opt/jso/jso-ai-quota-exporter.js \
    https://www.javascriptobfuscator.com/download/jso-ai-quota-exporter.js

# Cron line — minute granularity; the usage endpoint is free to poll.
* * * * * JSO_API_KEY=... JSO_API_PASSWORD=... \
    /usr/bin/node /opt/jso/jso-ai-quota-exporter.js \
    --out /var/lib/node_exporter/textfile_collector/jso_ai.prom

Exposed metrics: jso_ai_scrape_success, jso_ai_actions_used / _cap, jso_ai_tokens_used / _cap, jso_ai_cost_cents / _cap_cents, jso_ai_quota_rejections_total. All labeled with tier, preview_mode, billing_month. A scrape failure flips jso_ai_scrape_success to 0 so Alertmanager can fire on prolonged outage. Source is ~100 lines — read it before you run it.

14. Generate jso.config.json from a one-line description

Skip the config-tuning conversation by piping a natural-language description through preset-suggest:

DESC="React SaaS frontend, balanced performance, lock to example.com, protect license-check strongly"

curl -fsS -X POST https://www.javascriptobfuscator.com/v1/ai/preset-suggest.ashx \
    -H "Content-Type: application/json" \
    -d "{
      \"APIKey\":      \"$JSO_API_KEY\",
      \"APIPwd\":      \"$JSO_API_PASSWORD\",
      \"description\": $(jq -Rs . <<< \"$DESC\")
    }" | jq '.suggestion.config' > jso.config.json

# Optionally inspect the detected-signals list to understand what the
# assistant noticed and what it might be missing.
echo "Detected:"
curl -fsS -X POST ... | jq -r '.suggestion.signals[]'

The generated config is a reasonable starting point, not a final answer. Always review before committing. The Phase 1 LLM-backed version (2026-Q3) will ask clarifying questions when the description is ambiguous — the preview version flattens those into a "description was too generic" hint instead.

Supply-chain integrity recipes

Patterns built on the watermark, release attestation, and pre-flight quota gate shipped in jso-protector 0.5+. All four pieces compose: stamp during build ⇒ sign the manifest ⇒ verify per-file post-deploy ⇒ bulk-scan a tree for forensics. Wire format spec at WireFormat.aspx.

15. Tag every shipped artifact with a per-customer watermark

Add a customer-specific HMAC-signed marker to every protected file. If your code shows up on a torrent or a competitor's site, you can prove which customer's build it came from. The marker survives every transform (string array, control-flow flattening, dead code) because it rides in a header comment that the obfuscator's KeepComment option preserves.

export JSO_WATERMARK_KEY="$(openssl rand -hex 32)"   # one-time, store in CI secrets

# In CI, per-customer build:
npx jso-protector --config jso.config.json \
    --watermark "customer-${CUSTOMER_ID}-${RELEASE}" \
    --watermark-key "$JSO_WATERMARK_KEY"

# Forensics later, when a leak is suspected:
npx jso-protector --verify-watermark suspect-file.js \
    --watermark-key "$JSO_WATERMARK_KEY"
# OK    suspect-file.js: valid watermark, tag=customer-42-v3.1.0

# Or bulk-scan a CDN snapshot to inventory every customer's exposure:
npx jso-protector --scan-watermarks ./cdn-snapshot/ \
    --watermark-key "$JSO_WATERMARK_KEY" --json | jq '.files[] | "\(.tag)\t\(.file)"'

Exit codes are CI-friendly: 0 all watermarks valid (or lookup-only when no key), 1 at least one signature mismatch, 2 no watermarks found (probably wrong directory). Drop --scan-watermarks dist/ --watermark-key $KEY into a CI step after build to catch any output that DIDN'T receive the stamp — a real-world way to detect a misconfigured workflow.

For ad-hoc one-off checks, the interactive watermark verifier runs entirely in your browser via SubtleCrypto — useful when you want to confirm a single leaked file without installing tooling.

16. Ship a signed release attestation alongside the manifest

SLSA-style supply-chain proof: every release ships with an Ed25519-signed envelope covering BuildId + polymorphism fingerprint + per-file SHA-256. Customers (or your own CD pipeline) verify the signature before deploying. The on-disk re-hash also catches post-signing artifact tampering.

# One-time: mint a keypair. Commit the .pub.pem; keep the .priv.pem in CI secrets only.
npx jso-protector --genkey-release ci-key
git add ci-key.pub.pem
# Stash ci-key.priv.pem in CI as a file secret (GitHub: encrypted file; GitLab: file variable).

# In CI:
npx jso-protector --config jso.config.json \
    --manifest dist-protected/build.manifest.json \
    --sign-release ci-key.priv.pem \
    --label "$GITHUB_SHA"
# Writes both: dist-protected/build.manifest.json  AND  .json.sig

# In the deploy stage / on the customer's box:
npx jso-protector --verify-release dist-protected/build.manifest.json.sig \
    --public-key ci-key.pub.pem \
    --verify-root dist-protected/
# OK    .../build.manifest.json.sig: signature valid (BuildId=...), 42 file(s) re-hashed

The --public-key pin defeats key-substitution attacks: when the envelope's embedded public key doesn't match the trusted one, verification fails immediately even if the embedded sig technically validates against the embedded key. Without --verify-root, verification is signature-only (cheap, fast); add it when you have local artifacts to confirm haven't drifted post-signing.

17. Pre-flight quota + compat gate in one CI step

Catch broken builds before they consume obfuscation quota. Run --estimate to read the current month's quota (fails fast when actions remaining = 0), then --ai-precheck to scan every input file for obfuscation-incompatible patterns (eval, Function constructor, framework reflection traps). Only if both pass does the obfuscation API get called.

# Single chain — each step short-circuits the next on failure:
npx jso-protector --config jso.config.json --estimate \
  && npx jso-protector --config jso.config.json \
       --ai-precheck --ai-precheck-fail-on error \
       --watermark "$GITHUB_SHA" --watermark-key "$JSO_WATERMARK_KEY" \
       --sign-release ci-key.priv.pem \
       --manifest dist-protected/build.manifest.json \
       --label "$GITHUB_SHA"

The first --estimate call won't consume any AI actions itself (the /v1/ai/usage endpoint is free to poll). --ai-precheck burns one AI action per file but the build aborts on findings without consuming any obfuscation quota. Net effect: a single CI step that's safe to run on every push, including PR validation jobs.

18. Full supply-chain integrity in a GitHub Actions workflow

Combine all of the above as opt-in inputs to the official action. Six lines of workflow YAML beyond a normal build step:

- name: Protect with integrity
  id: protect
  uses: javascriptobfuscator/jso-github-action@v0.2
  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'
    ai-precheck: 'true'
    watermark: ${{ github.sha }}
    watermark-key: ${{ secrets.JSO_WATERMARK_KEY }}
    sign-release-key: ./ci-key.priv.pem    # checked out from secret store

- name: Publish signed attestation
  if: steps.protect.outputs.release-sig-path != ''
  uses: actions/upload-artifact@v4
  with:
    name: release-attestation
    path: ${{ steps.protect.outputs.release-sig-path }}

Customers verifying your build run jso-protector --verify-release against the artifact + your published ci-key.pub.pem. No JSO involvement required for verification — the format is open and documented at WireFormat.aspx.

More recipes? If your workflow isn't covered here, open a support ticket with the question — recipes get added based on what real customers ask. See also AIErrorRouting.aspx for the full production triage composition and JSO AI overview for the full roadmap.