feat(webapp,database): API key rotation grace period#3420
Conversation
Regenerating a RuntimeEnvironment API key no longer immediately invalidates the previous key. The old key is archived in a new `RevokedApiKey` table with a 24-hour expiresAt. `findEnvironmentByApiKey` falls back to this table when the primary lookup misses. An admin endpoint (`POST /admin/api/v1/revoked-api-keys/:id`) lets us shorten or extend the grace window by updating `expiresAt`. - schema: new RevokedApiKey model, indexes on apiKey and runtimeEnvironmentId - regenerateApiKey: wraps archive + update in a single transaction - modal copy updated to describe the 24h overlap instead of downtime
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📜 Recent review details⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (28)
🧰 Additional context used📓 Path-based instructions (7)**/*.{ts,tsx}📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Files:
{packages/core,apps/webapp}/**/*.{ts,tsx}📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Files:
**/*.{ts,tsx,js,jsx}📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Files:
**/*.ts📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)
Files:
**/*.{js,ts,jsx,tsx,json,md,yaml,yml}📄 CodeRabbit inference engine (AGENTS.md)
Files:
**/*.ts{,x}📄 CodeRabbit inference engine (CLAUDE.md)
Files:
apps/webapp/**/*.{ts,tsx}📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)
Files:
🧠 Learnings (2)📚 Learning: 2026-03-22T13:26:12.060ZApplied to files:
📚 Learning: 2026-03-22T19:24:14.403ZApplied to files:
🔇 Additional comments (1)
WalkthroughRegenerating a RuntimeEnvironment API key now records the previous key in a new RevokedApiKey table with a 24-hour expiresAt, instead of immediately invalidating it. findEnvironmentByApiKey first looks up live environments and, if none match, falls back to non-expired RevokedApiKey entries to return the associated environment. A new admin route allows updating a revoked key’s expiresAt. Prisma schema and migration add the RevokedApiKey model and relation. API key rotation is performed inside a transaction. UI copy for the regenerate-key modal and JWT signing now use the environment apiKey where applicable. Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
During the grace window after an API key rotation, a client calling /api/v1/auth/jwt with their old (revoked but still valid) key would get back a JWT that immediately failed validation, because signing used the header key (old) while validation in jwtAuth.server.ts uses environment.apiKey (new). Sign with the environment's canonical current key instead so minted JWTs validate regardless of which accepted key was used to authenticate the mint call. Works for dev/prod/preview — in the PREVIEW branch path, findEnvironmentByApiKey already merges the parent's current apiKey onto the returned env.
Summary
Regenerating a RuntimeEnvironment API key no longer immediately invalidates the previous one. Rotation is now overlap-based: the old key keeps working for 24 hours so customers can roll it out in their env vars without downtime, then stops working.
Design
RevokedApiKeytable (one row per revocation). Holds the archivedapiKey, a FK to the env, anexpiresAt, and acreatedAt. Indexed onapiKey(high-cardinality equality — single-row hits) and onruntimeEnvironmentId.regenerateApiKeywraps both writes in a single$transaction: insert aRevokedApiKeywithexpiresAt = now + 24h, update the env with the newapiKey/pkApiKey.findEnvironmentByApiKeydoes a two-step lookup: primary unique-index hit onRuntimeEnvironment.apiKeyfirst; on miss,RevokedApiKey.findFirst({ apiKey, expiresAt: { gt: now } })with aninclude: { runtimeEnvironment }. Two-step (notOR-join) keeps the hot path identical to today and puts the fallback cost only on invalid keys. Both lookups use$replica.POST /admin/api/v1/revoked-api-keys/:idaccepts{ expiresAt }and updates the row. Setting tonowends the grace window immediately; setting to the future extends it.Why a separate table instead of columns on
RuntimeEnvironmentTest plan
Verified locally against hello-world with dev and prod env keys:
GET /api/v1/runs) →200RevokedApiKeywithexpiresAt ≈ now+24h, env has new key200; bogus key →401expiresAt = now→ old key401expiresAt = +1h(after early-expire) → old key200againexpiresAt = past→ old key401pnpm run typecheck --filter webapppasses