Hash PII in the TS, Go, Python and .NET SDKs, not just Rust - #89
Merged
Conversation
🦋 Changeset detectedLatest commit: 8be7f17 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
All four scrubbed credentials only — `Bearer`, `password=`, `token`/`api_key`/`secret=`, `sk-…` — while their module docs claimed "PII scrubbing". Emails, phone numbers and street addresses went to the backend untouched. Rust fixed this in #82; this brings the other four to parity with byte-identical output, which matters now that telemetry is actually being exported (chat-ws was just wired up). Personal identifiers are hashed, not dropped: `a@b.com` -> `[email:9f2a41c8]`. `[redacted]` destroys the ability to ask "is this the same user as that other trace?"; a keyed hash keeps correlation while storing nothing reversible, and the type prefix stays visible so you can still see what kind of value was there. - HMAC-SHA256, keyed — a bare digest of an email is rainbow-tabled in seconds. - Org id inside the HMAC message, so identical PII hashes differently per org. The kind is in there too, NUL-separated, so values that normalize alike can't collide. - Fail closed: no key => `[email:redacted]`, never plaintext and never a hash under a guessable key. - Credentials matched FIRST and dropped entirely, never hashed — a hash of a live token is still a token oracle, and PII inside a secret (`token=a@b.com`) goes with the secret. - Normalized by kind (phone -> digits, email -> lowercase) so `(415) 555-0142` and `415-555-0142` correlate. Key comes from `SMOOAI_OBSERVABILITY_PII_HASH_KEY` at bootstrap, or the per-SDK setter; set-once, and the setters refuse a second key — rotating it silently forks every correlation already stored. The browser TS bundle has no env, so it calls `setPiiHashKey` explicitly (now exported from the entry). `piiToken(kind, raw, orgId)` is the search seam: hash a typed query term the same way and match the stored token. Existing org-less signatures keep working (they hash under the empty salt); `*ForOrg` variants are additive. The TS SDK ships a small sync SHA-256/HMAC rather than a dependency: `scrubString` is sync and runs in the browser bundle, where `node:crypto` is unavailable and WebCrypto is async-only. Pinned by the RFC 4231 / FIPS 180-4 vectors. All five SDKs assert the same `cross_sdk_parity_vectors` — computed independently — so any drift in message framing, normalization or truncation breaks exactly one SDK's suite. Two unrelated pre-existing format failures are fixed here because this PR touches those lanes and would otherwise be red: `CrashChild.cs` whitespace (`dotnet format`) and a stray blank line in `bootstrap.rs` (`cargo fmt`). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
brentrager
force-pushed
the
pii-hashing-polyglot
branch
from
August 15, 2026 19:49
9d5ff49 to
695cee1
Compare
#86 routed prompt content through `scrubString` and noted the TS scrub was credentials-only "until keyed per-org hashing lands". It has landed in this PR, and that call site inherited it exactly as predicted — no second redactor. The comment now describes what the code does instead of what it will do. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The TypeScript, Go, Python and .NET SDKs scrubbed credentials only —
Bearer,password=,token/api_key/secret=,sk-…— while their module docs claimed "PII scrubbing". Emails, phone numbers and street addresses went to the backend untouched. That's the false-advertising half of the bug, and it matters now: telemetry that was previously going nowhere is actually being exported (chat-ws was just wired up), so this data starts landing for real.Rust fixed this in #82. This brings the other four to parity — with byte-identical output, not just similar behavior.
Solution
Hash, don't drop.
[redacted]destroys the ability to ask "is this the same user as that other trace?". A deterministic keyed hash keeps correlation while storing nothing reversible, and the type prefix stays visible so you can still see what kind of value was there:[email:redacted]. Never plaintext, never a hash under a guessable key.token=a@b.com) is dropped with the secret.(415) 555-0142and415-555-0142correlate.Key supply
SMOOAI_OBSERVABILITY_PII_HASH_KEY, read at bootstrap in every SDK, plus a per-SDK setter. Set-once: the setters refuse a second key, because rotating it silently forks every correlation already stored. Rotate never.setPiiHashKey()(exported from the package entry — the browser bundle has no env, so it must call this explicitly)BootstrapEnv.piiHashKeySetPiiHashKey()BootstrapEnv.PiiHashKeyset_pii_hash_key()BootstrapEnv.pii_hash_keyPii.SetPiiHashKey()BootstrapEnv.PiiHashKeyCompatibility
The existing org-less signatures keep working unchanged (they hash under the empty org salt).
scrubStringForOrg/scrubHeadersForOrg/piiTokenare additive.piiToken(kind, raw, orgId)is the search seam: hash a typed query term the same way and match the stored token.TypeScript: why a hand-rolled HMAC
scrubStringis synchronous and runs in both the browser and Node bundles.node:cryptobreaks the browser build and WebCrypto'ssubtle.signis async-only, so neither can back a sync scrubber. Rather than add a dependency to a published SDK,packages/core/src/hmac-sha256.tsis ~130 lines pinned by the published RFC 4231 and FIPS 180-4 vectors (including the 55/56/64-byte padding boundaries and the million-along vector).Verification
Every SDK's suite mirrors the Rust test names case-for-case: same input + same org ⇒ same token; different org ⇒ different token; no key ⇒ redacted and never the raw value; credentials dropped not hashed; type prefix survives; a raw email/phone never appears in output;
piiTokenmatches what scrubbing wrote.Cross-SDK byte parity. All five SDKs — including Rust — assert the same
cross_sdk_parity_vectors, computed independently. If any SDK's message framing, normalization or truncation drifts, exactly one suite breaks.Mutation-checked. Each guard removed one at a time in all four languages — 24 mutations, every one caught by a specific named test, no survivors. Files verified byte-identical after restore.
hashes_emails_keeping_the_type_prefixhashes_phone_numbershashes_street_addressessame_value_different_org_hashes_differently+cross_sdk_parity_vectorscredentials_are_dropped_not_hashedno_key_redacts_rather_than_hashingGates, run as CI runs them (all exit 0):
pnpm typecheck/test(295) /build/format:check(oxfmt);gofmt -l/go vet/go testper-module (80);uvx ruff@0.16.2 check+format --check/pytest(98);dotnet build/test(105) /format --verify-no-changes;cargo fmt --all --check/clippy -D warnings/test --all-features.Unrelated fixes included
Two pre-existing format failures on
mainthat this PR's CI lanes would otherwise turn red, both verified against a clean tree first:CrashChild.cswhitespace (dotnet format) and a stray blank line inbootstrap.rs(cargo fmt).A pre-existing flaky test,
OtelSetupTests.Setup_IsIdempotent, fails intermittently on cleanorigin/mainunder the full-suite run and is unrelated to this change.🤖 Generated with Claude Code