feat/api-keys-v2
needs attentionb5b6c4c · fullpre-PRreviewed 2026-07-09 16:32 UTC5H · 12M · 8L · 5I- Purpose
- Upgrade API key subsystem to support multiple named active credentials per org
- Goal
- Enable up to 10 active keys per org (one per integration/environment); replace singleton model that forced credential sharing across integrations
- Sub-goals
- SG-1: Multiple active keys per org with advisory-lock cap enforcement
- SG-2: 24h rotation grace window — old key mints tokens until rotation_expires_at
- SG-3: Token exchange checks active + in-window rotating keys via findActiveByPrefix
- SG-4: Hard-delete flow (revoke → delete) retained
- SG-5: Admin-only management handlers with RLS enforcement
- What
- Dropped per-org unique index on active keys; added rotation_expires_at column; introduced per-org advisory lock for cap enforcement; updated token exchange to check grace window; upgraded admin UI for multi-key listing
- Why
- Single-credential-per-org model forced integrators to share keys across environments and broke when rotating (all integrations hit 401 during migration window)
- Areas
- domains/core/src/api-key+350−207apps/platform/src/api+80−33apps/platform/src/app/[locale]/(dashboard)/credentials+52−17packages/database+21−17packages/api+2−3
- Blast
- 23 files, +537/-350 across api-key domain, platform handlers, DB schema, UI components
Findings · 10
correctness2
rotateApiKeyShell: null insert returns err() in tx — no rollback, old key stuck in rotating
domains/core/src/api-key/api-key.shells.ts
After old key is flipped to rotating, if insert() returns null the shell returns err() without throwing. Drizzle rolls back only on throw. Old key is left permanently in rotating state with no successor — blocks re-rotation until 24h grace window expires. Fix: throw inside the transaction on null insert.
Revoking a rotating key leaves replacement key active — silent orphan
domains/core/src/api-key/api-key.decisions.ts
decideRevoke allows rotating->revoked. The new active key from the rotation remains alive with no link to the revoked key. Operators cancelling a rotation unknowingly leave the replacement credential live.
security2
Timing oracle: prefix-miss ~1ms vs prefix-hit/wrong-hash ~50ms (scrypt)
apps/platform/src/api/handlers/public-v1/auth-token.handler.ts
Attacker can distinguish active prefix existence from timing. Prefix is visible to org admins via list endpoint. Fix: dummy scrypt verify when candidates.length === 0.
Per-instance in-memory rate limiter bypassable via distributed serverless requests
apps/platform/src/app/api/v1/[...ts-rest]/route.ts
Each Vercel cold start gets empty rate window. Distributed requests face no effective ceiling. Fix: Redis/Upstash or edge enforcement.
tests4
Zero tests for auth-token handler — grace-window feature has no end-to-end test
apps/platform/src/api/handlers/public-v1/auth-token.handler.ts
All handler branches (format-reject, prefix-miss, hash-miss, grace-expired, success) are untested. This is the only production consumer of the findActiveByPrefix grace-window logic.
No test: grace-window-expired rotating key returns 401
apps/platform/src/api/handlers/public-v1/auth-token.handler.ts
If the now parameter defaults incorrectly, expired rotating keys authenticate indefinitely. No test verifies this path.
No test: decideCreate name at exactly 64-char boundary (should succeed)
domains/core/src/api-key/__tests__/api-key.decisions.test.ts
Test covers 65 chars (rejected) but not 64 chars. Classic off-by-one boundary gap.
No test: countActiveByOrg excludes rotating keys
domains/core/src/api-key/__tests__/api-key.integration.test.ts
This invariant is the only thing preventing rotating keys from consuming cap slots.
improvement2
Expired rotating rows accumulate in admin UI — no lazy cleanup path
domains/core/src/api-key/api-key.queries.ts
findByOrg returns all rows including past-expiry rotating rows. They show indefinitely with expired badge.
No duplicate-name guard within org active keys
domains/core/src/api-key/api-key.decisions.ts
Two active keys can share the same name. Names identify integrations so duplicates create operational confusion.