feat(db): seal account tokens by default and backfill them during migrate

Merged
WA
1d ago
+600-89

Summary

  • Account OAuth tokens are now sealed on write by default whenever ENCRYPTION_KEY is 64 hex characters; ENCRYPT_ACCOUNT_TOKENS=false (or 0) is the opt-out. A key in any other format keeps writing plaintext and never fails a write
  • Startup reports why sealing is off: Account token sealing: disabled (ENCRYPTION_KEY is not a 64-character hex key) at WARN, once per process, or disabled (ENCRYPT_ACCOUNT_TOKENS is false) at INFO for the opt-out
  • Registers 0037_seal_account_tokens as a script migration so migrate backfills stored tokens. The runner gains ScriptMigrationDeferred: a migration that throws it is left unrecorded without failing the run, so the backfill stays pending while sealing is off, or while a batch hits its lock or statement timeout, and runs on a later migrate
  • Rolling upgrade from a release older than v0.9.33: the backfill waits until an hour after v0.9.33's own script migration (0036) was recorded, so old pods that cannot read sealed tokens have rolled out before every stored token is sealed. New writes still seal right away, and the docs cover that window
  • The backfill now pauses only after a batch that rewrote rows. On an already sealed table it scans by primary key without pausing, writes nothing, and records itself
  • The Trigger.dev seal-account-tokens task now refuses seal only when sealing is opted out
  • Helm's migrations init container and the Compose migrations services now receive ENCRYPTION_KEY and ENCRYPT_ACCOUNT_TOKENS, read the same way the app reads them. Chart bumped to 1.11.10
  • Self-hosting docs explain account token sealing, the 64-hex key requirement, the opt-out, the rolling-upgrade window, and the rule never to roll back below v0.9.33 once tokens are sealed

Type of Change

  • New feature

Testing

  • packages/db/script-migrations/0037_seal_account_tokens.integration.ts (real Postgres): migrate seals a mixed plaintext/sealed table and records it; stays pending and untouched with false, 0, or a non-hex key, then runs once sealing is on; waits out the rollout grace; defers instead of failing when a batch times out on a locked row
  • apps/sim/lib/oauth/credential-revocation.integration.ts (real Postgres + Redis): sealed writes with the flag unset, plaintext with the opt-out, plaintext with a non-hex key
  • packages/db/sealed-text.test.ts: a non-hex key writes plaintext and logs one WARN with the reason
  • apps/sim/background/seal-account-tokens.test.ts: seals with the flag unset, refuses when opted out
  • helm unittest (2 new cases for the init container env), helm lint --strict
  • Red→green: each test fails against the staging code, or with its guard reverted (runner deferral, opt-out check, rollout grace, timeout deferral, init-container env)
  • bun run lint, check:sealed-columns, check:unused-exports, check:comment-hygiene, check:test-types, check:utils, check:source-conventions, check:docs, check:boundaries, packages/db tsc; CI runs the full audits and type-check

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing (new tests pass the test-audit authoring gate)
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)