…ase64-22 rule
Enables the detection deliberately disabled in #111. The rootkey crosses
the wire as strict base64 of 16 bytes (backend/index.js enforces
/^[A-Za-z0-9+/]{22}==$/), and any valid 16-byte base64 ends its 22nd
char in [AQgw], so the rule matches exactly 22 base64/base64url chars
ending there, optionally ==-padded, bounded by non-base64 chars. Padded
matches always redact; unpadded ones also need mixed case plus a
digit/symbol and must not be pure hex, which spares 32-hex trace ids,
16-hex span ids, PascalCase exception type names, ERR_* codes,
error_class tag values, and base64-ish ids of other lengths.
Applied identically to both hand-mirrored scrubber copies and to the
forbidden-metric tag-value gate; shared cases in
test-support/scrubber-cases.js cover both suites.
Closes #77
Closes #77.
PR #111 landed the PII scrubbers with one rule deliberately disabled: a broad "any 22+-char base64 run" meant to catch a bare rootkey with no rootKey= marker. It over-matched 32-hex trace ids, PascalCase exception type names, and error_class metric tags, so bare unmarked rootkeys passed through unredacted.
This enables a much narrower rule built from the rootkey's actual shape. The key crosses the wire as strict base64 of 16 bytes — backend/index.js already enforces /^[A-Za-z0-9+/]{22}==$/ — and any valid 16-byte base64 must end its 22nd char in A/Q/g/w (the final 4 bits are padding zeros). The rule therefore matches exactly 22 base64/base64url chars ending in [AQgw], optionally ==-padded, bounded by non-base64 chars. A padded match is the exact wire shape and always redacts. An unpadded match must additionally show a random-key character mix: both cases, at least one digit or symbol, and not pure hex. That structurally spares trace/span ids (wrong length, hex), exception type names and ERR_* codes (letters-only or single-case), error_class values, and base64-ish ids of other lengths (43-char public keys, 52-char project ids).
Known limits, documented in docs/sentry-integration.md §8: roughly 1% of random rootkeys are all-letters and would slip through in unpadded form (the padded wire form is always caught — verified over 200k random keys: 0 padded misses, 1.29% unpadded), and a 22-char mixed-case identifier containing a digit that happens to end in [AQgw] is falsely redacted — accepted, since false negatives are worse here.
The rule is implemented identically in both hand-mirrored scrubber copies (src/sentry-scrub.ts, backend/before-send.js) and also gates metric tag values in isForbiddenMetric. Shared cases in test-support/scrubber-cases.js (consumed by both the jest and node:test suites) cover: padded and unpadded bare rootkeys redact, JSON-quoted rootkey values redact, and trace/span ids, type names, 22-char PascalCase identifiers, 22-char hex fragments, ERR_* codes, error_class tags, and 43/52-char tokens all survive.
Verified with npm run lint, npm run build, npm run test (79 passed), and npm run backend:test (83 passed).