feat/one-api
needs attentionviewing older commit0716018 · incrementalpre-PRreviewed 2026-07-27 19:22 UTC2H · 11M · 12L · 7I- 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
- 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+102−16apps/platform/src/api/handlers/public-v1+290−12domains/core/src/column-configuration+162−74domains/core/src/secret-configuration+152−44.github/workflows+26−0packages/api/src/schemas+15−2.claude/rules+19−0
- 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.
Findings · 13
security3
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.
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.
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
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.
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.
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
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.
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.
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.
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.
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
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.
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
- 82bb5b9blockedincremental5H · 5M · 4L2026-08-12 01:48
- 90aa3d5needs attentionincremental1H · 5M · 3L2026-08-11 19:37
- 29d19a0needs attentionincremental1H · 5M · 9L2026-08-11 17:41
- 9bd8a0cneeds attentionfull0H · 5M · 9L2026-08-11 02:14
- 62ec3f7needs attentionincremental2H · 5M · 6L2026-08-10 22:51
- f93bca9needs attentionincremental2H · 5M · 8L2026-08-10 17:51
- 052db6fneeds attentionincremental1H · 3M · 4L2026-08-09 21:13
- 45699caneeds attentionincremental0H · 7M · 11L2026-08-09 17:44
- b843d8aneeds attentionincremental1H · 7M · 9L2026-08-09 04:05
- e1757b8needs attentionincremental0H · 3M · 6L2026-08-05 02:11
- 7a762faneeds attentionincremental2H · 5M · 5L2026-08-05 01:25
- 3300a60needs attentionincremental2H · 4M · 7L2026-08-04 19:06
- 0c8a7f5needs attentionincremental0H · 4M · 9L2026-08-04 18:15
- 345f42eneeds attentionincremental2H · 6M · 9L2026-08-04 17:28
- 8338a9aneeds attentionincremental5H · 14M · 14L2026-08-04 00:33
- 41be4c3needs attentionincremental0H · 5M · 7L2026-08-03 23:49
- 5ed593dneeds attentionincremental1H · 6M · 6L2026-08-03 21:32
- b333e25needs attentionincremental4H · 9M · 8L2026-08-03 21:00
- 5642cccneeds attentionincremental2H · 3M · 2L2026-08-03 20:17
- 73b0b39needs attentionincremental3H · 10M · 13L2026-07-31 18:29
- b19852eneeds attentionincremental0H · 1M · 5L2026-07-29 05:04
- 3845205needs attentionincremental3H · 6M · 4L2026-07-29 04:47
- eb8eb50needs attentionincremental0H · 1M · 2L2026-07-29 03:03
- f4720a3needs attentionincremental6H · 8M · 7L2026-07-29 02:54
- f8d341ablockedincremental2H · 2M · 5L2026-07-29 00:00
- a7f1a64needs attentionincremental2H · 8M · 8L2026-07-28 18:41
- 738b60bblockedincremental3H · 6M · 5L2026-07-28 00:46
- 2c248b6needs attentionincremental8H · 12M · 8L2026-07-27 23:23
- 1346cc0needs attentionincremental2H · 8M · 6L2026-07-27 20:15
- 0716018needs attentionincremental2H · 11M · 12L2026-07-27 19:22current
- 215cd2dneeds attentionincremental3H · 6M · 5L2026-07-27 17:04
- ec46958needs attentionincremental0H · 3M · 5L2026-07-27 16:51
- de7b337blockedincremental4H · 9M · 14L2026-07-27 06:36
- b1bb9c0needs attentionincremental1H · 2M · 4L2026-07-27 05:09
- 4701d11needs attentionincremental0H · 4M · 3L2026-07-27 04:44
- e1626c4needs attentionincremental3H · 9M · 10L2026-07-27 03:21
- 195f198needs attentionincremental3H · 3M · 3L2026-07-25 01:22
- 42c7358safeincremental0H · 0M · 0L2026-07-22 20:46
- 85b9018needs attentionincremental0H · 1M · 6L2026-07-21 23:51
- a7b2a9aneeds attentionincremental0H · 9M · 12L2026-07-21 18:49
- c2ee0daneeds attentionincremental4H · 7M · 7L2026-07-21 02:17
- e8ffa5eneeds attentionincremental4H · 7M · 5L2026-07-21 01:33
- a2d2a54needs attentionincremental2H · 7M · 3L2026-07-21 00:51
- 576fbd6needs attentionfull1H · 6M · 7L2026-07-21 00:35
- d3465e8needs attentionincremental1H · 7M · 10L2026-07-21 00:23
- dc794a7needs attentionincremental0H · 5M · 5L2026-07-20 23:46
- 9082773needs attentionfull1H · 3M · 3L2026-07-20 23:13