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 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.

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.