Skip to content

feat(api): provision a per-org session-key salt - #431

Closed
JeremyFunk wants to merge 1 commit into
ai/01-registry-artifactfrom
ai/02-org-session-salt
Closed

feat(api): provision a per-org session-key salt#431
JeremyFunk wants to merge 1 commit into
ai/01-registry-artifactfrom
ai/02-org-session-salt

Conversation

@JeremyFunk

@JeremyFunk JeremyFunk commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

What this layer contains

The per-org secret that AI session-key hashing will need:

  • packages/dbsession_salt_ciphertext / session_salt_iv / session_salt_tag on org_ingest_keys, migration 0040_org_session_salt.sql plus its drizzle snapshot/journal.
  • apps/apiOrgIngestKeysService.getSessionSalt, which lazily provisions and decrypts the salt using the same AES-256-GCM envelope (and the same MAPLE_INGEST_KEY_ENCRYPTION_KEY) as the private ingest key, with tests.

Why it is shaped this way

Session keys are stored as cityHash64(concat(salt, '\0', value)) so the raw key never reaches the warehouse and hashes cannot be correlated across orgs. That makes the salt a prerequisite of the write path rather than part of it, which is why it ships as its own reviewable unit ahead of the classifier.

Two properties are deliberate. The columns are nullable only because existing rows predate them, so a null means "not provisioned yet" and is backfilled on first read — never "this org has no salt". And there is no reroll path: rotating the salt changes every hash, which breaks SessionsApprox HLL continuity across the rotation boundary, double-counting sessions on either side of it.

Gates run at this level

  • bun typecheck — 37/37 tasks pass.
  • apps/api vitest, src/services/org/ — 5 files, 62 tests pass.
  • apps/api vitest, src/routes/v2/ — 12 files, 142 tests pass (covers the v2-test-support stub gaining getSessionSalt).

Dependencies

Stacked on #430 for ordering only — it shares no code with the registry artifact and is independently reviewable.

🤖 Generated with Claude Code


Stack created with GitHub Stacks CLIGive Feedback 💬


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Adds `session_salt_{ciphertext,iv,tag}` to `org_ingest_keys` (migration
0040) and `OrgIngestKeysService.getSessionSalt`, which lazily provisions
and decrypts a per-org secret using the same AES-256-GCM envelope as the
private ingest key.

The salt exists so AI session keys can be stored as
`cityHash64(concat(salt, '\0', value))`: the raw key never reaches the
warehouse and hashes cannot be correlated across orgs. The columns are
nullable only because existing rows predate them — a null means "not
provisioned yet" and is backfilled on first read. There is deliberately no
reroll path: rotating the salt changes every hash and would break
SessionsApprox HLL continuity across the rotation boundary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JeremyFunk
JeremyFunk force-pushed the ai/01-registry-artifact branch from d6c7d9b to 1860da6 Compare August 12, 2026 20:04
@JeremyFunk
JeremyFunk force-pushed the ai/02-org-session-salt branch from 34798b3 to 3490ce0 Compare August 12, 2026 20:04
@JeremyFunk

Copy link
Copy Markdown
Collaborator Author

Dropping this one. The salt bought no confidentiality: AiSessionKeyHash is a trailing column on traces, and the identifier it digests stays in the clear in SpanAttributes on the same row, so anything that can read the hash can read the source value beside it. The rest of the stack now hashes the bare value with cityHash64, which is all the column ever needed — 8 fixed bytes and a numeric input to uniqCombined in service_ai_vendors_hourly.

That also removes migration 0040, the lazy backfill, the AES-GCM decrypt on the ingest auth path, and the ai_salt_missing counter.

@JeremyFunk JeremyFunk closed this Aug 12, 2026
@JeremyFunk
JeremyFunk deleted the ai/02-org-session-salt branch August 12, 2026 21:05
@github-actions

Copy link
Copy Markdown

🍁 Maple PR preview

Note

Preview resources were removed when this pull request closed.

Final commit 3490ce0 · View workflow run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant