feat(db): seal account tokens by default and backfill them during migrate
Merged
WA
1d ago+600+600+012345678901234567890123456789-89-89-01234567890123456789
Summary
- Account OAuth tokens are now sealed on write by default whenever
ENCRYPTION_KEYis 64 hex characters;ENCRYPT_ACCOUNT_TOKENS=false(or0) 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, ordisabled (ENCRYPT_ACCOUNT_TOKENS is false)at INFO for the opt-out - Registers
0037_seal_account_tokensas a script migration somigratebackfills stored tokens. The runner gainsScriptMigrationDeferred: 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-tokenstask now refusessealonly when sealing is opted out - Helm's migrations init container and the Compose
migrationsservices now receiveENCRYPTION_KEYandENCRYPT_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 withfalse,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 rowapps/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 keypackages/db/sealed-text.test.ts: a non-hex key writes plaintext and logs one WARN with the reasonapps/sim/background/seal-account-tokens.test.ts: seals with the flag unset, refuses when opted outhelm 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/dbtsc; 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-auditauthoring gate) - No new warnings introduced
- I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)