← all branches

feat/one-api

needs attentionviewing older commit
0716018 · incrementalpre-PRreviewed 2026-07-27 19:22 UTC2H · 11M · 12L · 7I
The branch
Purpose
API-first key management for Batu's public API — feat/one-api branch migrates credential management from dashboard-only endpoints to a first-class public /v1/api-keys surface with no-amplification, cascade revocation, service keys, and the DUAL migration pattern for backward compat.
Goal
Ship /v1/api-keys (list/create/rotate/revoke) with correct privilege enforcement so machine callers can manage their own credentials programmatically.
Sub-goals
  • W0: P0 security (catalog admin gate, scopes, IDOR harness)
  • W1: Service keys and internal mount expansion
  • W2: /v1/api-keys with no-amplification + cascade revoke
  • W3: DUAL #15 migration — retarget hooks, delete dashboard twin
The changes (whole branch)
What
This commit (incremental) closes the privilege-escalation hole in rotate (granterScopes enforcement in decideRotate), adds dead-token liveness check in resolveCaller, adds callerMayActOn authority check for revoke at the handler layer, acquires org lock before cascade snapshot in revokeApiKeyShell, scopes unique-violation catchers with nameWriteInFlight/orphanedSecret flags, fixes dead CI integration tests, preserves key lineage through rotation.
Why
A previous review found any key with api-keys:write could rotate the org's ['*'] key and receive wildcard authority — a one-call full escalation. This commit addresses that finding and the other issues surfaced in the same review cycle.
Areas
domains/core/src/api-key+10216apps/platform/src/api/handlers/public-v1+29012domains/core/src/column-configuration+16274domains/core/src/secret-configuration+15244.github/workflows+260packages/api/src/schemas+152.claude/rules+190
Blast
Security-critical path — API key credential lifecycle. No DB schema changes. Affects all machine callers of /v1/api-keys. Column-config and secret-config shell fixes are correctness improvements with no API surface change.
auth public-api cascade-revocation
CI· No CI data available (pre-PR branch)CodeRabbit· No .coderabbit.yaml in repo

Findings · 13

security3

medium

Revoked machine-key tokens can still LIST org API keys

apps/platform/src/api/handlers/public-v1/api-keys.handler.ts:164

apiKeysListHandler does not call resolveCaller. A revoked key's JWT (valid up to 1h) can enumerate all org key prefixes, scopes, and metadata. Dead-token window was closed for write paths but left open for reads.

medium

Lock acquired AFTER fetch in revokeApiKeyShell — TOCTOU window on concurrent creates

domains/core/src/api-key/api-key.shells.ts:292

fetch-then-lock ordering means concurrent createApiKeyShell could insert a new child between the two calls. CTE reads live data at execution time so exploitability is low but the documented invariant requires lock-then-fetch.

medium

callerMayActOn not applied to rotate at handler level — only inside decideRotate

apps/platform/src/api/handlers/public-v1/api-keys.handler.ts:280

Functionally correct but inconsistent with revoke's early-return handler check; error body comes from a different code path (mapper vs inline constant).

conventions3

high

Revoke scope-authority check lives only in the handler, not in decideRevoke — FCIS violation

apps/platform/src/api/handlers/public-v1/api-keys.handler.ts:545

callerMayActOn for revoke is handler-only, outside the transaction. decideRevoke has no granterScopes param. The dashboard twin calling revokeApiKeyShell directly bypasses this protection. decideCreate/decideRotate enforce no-amplification in the decision; decideRevoke should too.

medium

Revoke handler scope check races on target's scopes — pre-transaction snapshot

apps/platform/src/api/handlers/public-v1/api-keys.handler.ts:539

Handler pre-fetches existing outside the transaction, runs callerMayActOn on stale data, then calls shell which re-fetches inside the transaction. Target's scopes could change between the pre-fetch and the write.

medium

RotateApiKeyCommand.granterScopes is optional (?) — undefined and null conflate silently

domains/core/src/api-key/api-key.decisions.ts:224

Both undefined (field absent) and null (session) skip the no-amplification check. An accidental omission silently grants session-level authority. Should be non-optional: `readonly granterScopes: readonly string[] | null`.

tests5

high

Authority test file missed by CI gate — security tests skip on handler-only PRs

.github/workflows/pr-checks.yml:152

db-changes path filter excludes handlers/public-v1/api-keys.handler.ts and the new authority test file. A PR changing only the public-v1 api-keys handler triggers changed=false and the database-checks job is skipped. These mocked-only tests should run in the always-on validate job.

medium

Rotate 403 mapper branch untested at handler level

apps/platform/src/api/handlers/public-v1/__tests__/api-keys.authority.test.ts:191

Rotate describe block only checks granterScopes forwarding. Does not test ApiKeyScopeEscalation returned by shell → 403 via mapPublicRotateApiKeyError.

medium

Outbox-collision test asserts .rejects.toThrow() — shell breaks never-throw on outbox-23505 path

domains/core/src/column-configuration/__tests__/column-configuration.shells.integration.test.ts:901

Correctly reflects that .catch() rethrows when nameWriteInFlight is false. But it documents an unresolved never-throw contract breach for callers without a try block.

medium

Dead-token test for rotating status does not cover rotationExpiresAt === null

apps/platform/src/api/handlers/public-v1/__tests__/api-keys.authority.test.ts:163

it.each passes a past expiry for rotating. Does not verify rotating + null expiry which resolveCaller also treats as dead.

medium

Secret-config admin shell: membershipQueries mock is a misleading no-op

domains/core/src/secret-configuration/__tests__/secret-configuration.shells.integration.test.ts:542

adminCreateSecretConfigurationShell skips membership lookup, so mocking findForUserInOrg gives false confidence.

improvement2

medium

createSecretConfigurationShell / adminCreateSecretConfigurationShell are near-verbatim duplicates

domains/core/src/secret-configuration/secret-configuration.shells.ts:712

Same orphanedSecret patch applied twice. A shared private helper would eliminate the parallel maintenance surface.

medium

Mutable closure vars (nameWriteInFlight, dupWriteInFlight, orphanedSecret) — try/finally would be structurally safer

domains/core/src/column-configuration/column-configuration.shells.ts:929

Flag left true permanently on an exception — handled incidentally by the outer .catch. A try/finally around the write statement makes the contract explicit.

History · 47 commits

  1. 82bb5b9blockedincremental5H · 5M · 4L2026-08-12 01:48
  2. 90aa3d5needs attentionincremental1H · 5M · 3L2026-08-11 19:37
  3. 29d19a0needs attentionincremental1H · 5M · 9L2026-08-11 17:41
  4. 9bd8a0cneeds attentionfull0H · 5M · 9L2026-08-11 02:14
  5. 62ec3f7needs attentionincremental2H · 5M · 6L2026-08-10 22:51
  6. f93bca9needs attentionincremental2H · 5M · 8L2026-08-10 17:51
  7. 052db6fneeds attentionincremental1H · 3M · 4L2026-08-09 21:13
  8. 45699caneeds attentionincremental0H · 7M · 11L2026-08-09 17:44
  9. b843d8aneeds attentionincremental1H · 7M · 9L2026-08-09 04:05
  10. e1757b8needs attentionincremental0H · 3M · 6L2026-08-05 02:11
  11. 7a762faneeds attentionincremental2H · 5M · 5L2026-08-05 01:25
  12. 3300a60needs attentionincremental2H · 4M · 7L2026-08-04 19:06
  13. 0c8a7f5needs attentionincremental0H · 4M · 9L2026-08-04 18:15
  14. 345f42eneeds attentionincremental2H · 6M · 9L2026-08-04 17:28
  15. 8338a9aneeds attentionincremental5H · 14M · 14L2026-08-04 00:33
  16. 41be4c3needs attentionincremental0H · 5M · 7L2026-08-03 23:49
  17. 5ed593dneeds attentionincremental1H · 6M · 6L2026-08-03 21:32
  18. b333e25needs attentionincremental4H · 9M · 8L2026-08-03 21:00
  19. 5642cccneeds attentionincremental2H · 3M · 2L2026-08-03 20:17
  20. 73b0b39needs attentionincremental3H · 10M · 13L2026-07-31 18:29
  21. b19852eneeds attentionincremental0H · 1M · 5L2026-07-29 05:04
  22. 3845205needs attentionincremental3H · 6M · 4L2026-07-29 04:47
  23. eb8eb50needs attentionincremental0H · 1M · 2L2026-07-29 03:03
  24. f4720a3needs attentionincremental6H · 8M · 7L2026-07-29 02:54
  25. f8d341ablockedincremental2H · 2M · 5L2026-07-29 00:00
  26. a7f1a64needs attentionincremental2H · 8M · 8L2026-07-28 18:41
  27. 738b60bblockedincremental3H · 6M · 5L2026-07-28 00:46
  28. 2c248b6needs attentionincremental8H · 12M · 8L2026-07-27 23:23
  29. 1346cc0needs attentionincremental2H · 8M · 6L2026-07-27 20:15
  30. 0716018needs attentionincremental2H · 11M · 12L2026-07-27 19:22current
  31. 215cd2dneeds attentionincremental3H · 6M · 5L2026-07-27 17:04
  32. ec46958needs attentionincremental0H · 3M · 5L2026-07-27 16:51
  33. de7b337blockedincremental4H · 9M · 14L2026-07-27 06:36
  34. b1bb9c0needs attentionincremental1H · 2M · 4L2026-07-27 05:09
  35. 4701d11needs attentionincremental0H · 4M · 3L2026-07-27 04:44
  36. e1626c4needs attentionincremental3H · 9M · 10L2026-07-27 03:21
  37. 195f198needs attentionincremental3H · 3M · 3L2026-07-25 01:22
  38. 42c7358safeincremental0H · 0M · 0L2026-07-22 20:46
  39. 85b9018needs attentionincremental0H · 1M · 6L2026-07-21 23:51
  40. a7b2a9aneeds attentionincremental0H · 9M · 12L2026-07-21 18:49
  41. c2ee0daneeds attentionincremental4H · 7M · 7L2026-07-21 02:17
  42. e8ffa5eneeds attentionincremental4H · 7M · 5L2026-07-21 01:33
  43. a2d2a54needs attentionincremental2H · 7M · 3L2026-07-21 00:51
  44. 576fbd6needs attentionfull1H · 6M · 7L2026-07-21 00:35
  45. d3465e8needs attentionincremental1H · 7M · 10L2026-07-21 00:23
  46. dc794a7needs attentionincremental0H · 5M · 5L2026-07-20 23:46
  47. 9082773needs attentionfull1H · 3M · 3L2026-07-20 23:13