← all branches

feat/one-api

needs attentionviewing older commit
a7f1a64 · incrementalpre-PRreviewed 2026-07-28 18:41 UTC2H · 8M · 8L · 5I
The branch
Purpose
Unify Batu's public API surface — one contract tree, one credential per org, one auth gate
Goal
Ship a production-ready one-credential API model: master key minted at org creation, reveal/rotate/revoke from dashboard, no scoped-key complexity until customer demand warrants it
Sub-goals
  • W1 (done): RouteMeta gate, route inventory, IDOR harness
  • W2 (done): Service keys as api_keys (kind discriminator), /v1/auth/token unification, internal mount dual-accept, issueMaster repair path
  • Scope removal: strip dormant scoped-key machinery to prevent it reading as a shipped feature
  • Revocation reversibility: issueMasterApiKeyShell lets orgs recover from a revoked master
The changes (whole branch)
What
Two commits: (1) refactor removes scopes from RouteMeta/JWT/gate/decisions — withMetaGate now asks only 'what credential CLASS is this'; (2) fix adds issueMasterApiKeyShell (repair path for orgs with no active master) + migration 0069 promotes oldest active key to master for pre-existing orgs
Why
Dormant scope machinery reads as a shipped feature, invites drift, and adds complexity with no current customer value. Revocation was one-way (a single click could permanently brick an org's API access); the repair path makes it survivable.
Areas
domains/core/src/api-key+800120apps/platform/src/api+650180apps/platform/src/__tests__+49010packages/api/src+320180packages/database+21020.claude/rules+9560docs/security+400
Blast
65 files in incremental review; ~2600 adds / ~570 dels across api-key domain, platform handlers, contracts, middleware, and DB schema. Core credential machinery — high-stakes but well-tested at integration level.
scope-removal credential-management migration-with-data-mutation auth-gate-simplification
CI· No open PR — CI status unavailable for pre-PR branchCodeRabbit· No .coderabbit.yaml present

Findings · 26

correctness3

medium

Revoke endpoint description says 'not reversible' — now factually wrong

apps/platform/src/api/contracts/public-v1/api-keys.contract.ts:99

The public revoke description ships to /developers docs and states revocation is not reversible. The fix(core) commit explicitly makes it reversible via issueMasterApiKeyShell. A customer who revokes and expects permanence has the wrong mental model; a customer who revokes by mistake needs to know recovery exists.

low

Stale module-level comment in public-v1 api-keys.handler.ts describes removed scopes claim

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

Comment says the meta gate checked 'api-keys:read / api-keys:write' scopes. Scope gates were removed. Could lead a future developer to believe a scope check exists that does not.

low

createApiKeyShell is exported dead code — no production caller after issueMaster change

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

Dashboard POST /organizations/:orgId/api-keys now calls issueMasterApiKeyShell instead of createApiKeyShell. No production caller remains. Keeping it exported risks a future developer using it for the 'named key creation' flow that is explicitly prohibited in the one-credential model.

security4

medium

decideRotate/decideRevoke master guard uses === false — null isMaster bypasses it

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

The master-only guard is `command.granterIsMaster === false`. If isMaster were ever null on a machine key row (DB migration, direct manipulation), the check returns false (null !== false) and the master guard is silently bypassed. Currently safe because the DB default is false, but the type is boolean|null and the asymmetry is a latent trap. Recommend: check !== true && !== null, or document the invariant in resolveCaller.

info

Scope removal: no privilege escalation — machine keys always had ['*'] grant

withMetaGate now checks only credential class (machine/session/service). No narrower grant ever existed in production; removing the always-vacuously-true check changes nothing at runtime. The gate is still fail-closed on missing metadata.

info

Migration 0069 service-key exclusion is double-guarded

packages/database/drizzle/0069_backfill_master_api_keys.sql

Explicit org_id IS NOT NULL filter AND the api_keys_master_is_org_scoped CHECK constraint both prevent service key promotion. Robust.

info

issueMaster IDOR protection: requireOrgAccess resolves org from authenticated membership, not raw path param

apps/platform/src/api/handlers/api-keys.handler.ts

Handler uses requireOrgAccess(database, profileId, params.orgId, 'owner') which resolves org.id from the authenticated profile's membership. Org A cannot issue a master for org B.

conventions6

medium

Stale JSDoc in public-v1 api-keys.handler.ts — describes removed scopes model

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

Lines 16–19 describe 'api-keys:read / api-keys:write' scope enforcement and decideCreate's no-amplification rule. Both were removed in 738b60b8. The comment makes the refactor look reversible and will mislead the next reader.

medium

resolveCaller JSDoc describes removed granterScopes field

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

The JSDoc above resolveCaller says 'granterScopes is NULL for a session on purpose'. The field granterScopes no longer exists in the return type; the function returns { ok, parentKeyId, isMaster }. The comment should document isMaster semantics instead.

medium

route-meta.ts file comment still says gate enforces 'auth + scopes'

packages/api/src/meta/route-meta.ts:7

The file-level comment reads: 'withPublicApiAuth's meta gate enforces auth + scopes at the mount'. RouteMeta.scopes was removed in 738b60b8. Should read 'auth at the mount'.

low

api-key.api-types.ts CreateApiKeyRequest comment still mentions 'optional scopes'

packages/api/src/types/api-key.api-types.ts:63

JSDoc says body carries 'name (and optional scopes)'. scopes was removed from CreateApiKeyCommand. Should say 'only name'.

low

decideRotate comment retains 'a key always satisfies its own scopes' — scope model gone

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

'Trivially allowed — a key always satisfies its own scopes' references the removed scope model. The underlying lineage-chain argument is valid but the parenthetical is now orphaned noise.

low

issueMasterApiKeyShell omits pure-decision function — accepted deviation, not documented

domains/core/src/api-key/api-key.master.ts:171

FCIS canonical form requires a pure decideIssueMaster. The rule is omitted because state must be re-read inside the transaction. This is the sanctioned escape hatch per domain-patterns.md, but the reason is not documented in the shell. A future reader may add the function thinking the shell is incomplete.

tests7

high

buildKey fixtures missing required fields — isMaster, secretArn, kind

domains/core/src/api-key/__tests__/api-key.shells.test.ts:85

The buildKey base object in both shells.test.ts and decisions.test.ts is missing required ApiKey fields (isMaster, secretArn, kind). TypeScript should reject the return site as ApiKey. Tests that exercise isMaster-dependent behavior get silent undefined instead of false, making granterKey.isMaster === false checks subtly unsafe.

high

No unit test for issueMasterApiKeyShell orchestration (compensation, race re-check, null-insert)

domains/core/src/api-key/__tests__/api-key.shells.test.ts

issueMasterApiKeyShell has integration coverage but zero mocked-unit tests. The three-phase structure (pre-check → prepareMasterApiKey → advisory lock → re-read → insert) has untested paths: discardMasterApiKeySecret compensation on failure, the 'committed err' race path, ApiKeyDatabaseError on null insert, and the unique-violation → ApiKeyMasterExists mapping.

medium

No authority test for POST /organizations/:orgId/api-keys — admin→owner gate tightened silently

apps/platform/src/api/handlers/api-keys.handler.ts:83

The handler now calls requireOrgAccess(..., 'owner') but previously required only 'admin'. No test asserts that an admin (non-owner) gets 403. This is the surface that hands out a live credential — the missing regression test is the highest-priority gap.

low

buildKey missing createdByApiKeyId default — === null assertions vacuously pass on undefined

domains/core/src/api-key/__tests__/api-key.decisions.test.ts:49

buildKey base object omits createdByApiKeyId (required string|null). Tests asserting createdByApiKeyId !== null may pass vacuously because the field is undefined rather than null.

low

Migration 0069 backfill edge cases untested: promotion, service-key exclusion, idempotency

packages/database/drizzle/0069_backfill_master_api_keys.sql

The SQL is idiomatic and idempotent but the three edge cases — an org with one active machine key (should be promoted), a service key (org_id NULL, must be excluded), and a re-run on an already-promoted org — have no test assertions. These are migration invariants that can only be tested via direct SQL seeding.

info

IDOR harness correctly omits dashboard issueMaster route (not yet on public-v1)

apps/platform/src/__tests__/integration/public-v1-idor.test.ts

The issueMaster endpoint is on the internal dashboard contract (not yet converged to /v1). IDOR omission is architecturally correct. Cross-org guard is protected by requireOrgAccess which is tested via existing infrastructure.

info

route-meta.guard.test.ts correctly updated — no stale scope assertions

apps/platform/src/api/contracts/__tests__/route-meta.guard.test.ts

Guard test updated with 'Per-route scopes were removed 2026-07-28 — class is the whole check now'. All four assertions are scope-agnostic and remain valid. Count pin (>=48) satisfied with 51 routes.

improvement6

medium

Dead 'POST /v1/api-keys — issue one' line + scope model doc in public-v1 handler module doc

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

Module doc still lists 'POST /v1/api-keys — issue one (secret returned once)' (line 5) and describes no-amplification scope grants (lines 17–20). Neither exists after 738b60b8. The handler exports exactly three handlers (list/rotate/revoke).

medium

Stale 'W2 deliverable — until it lands' note about RouteMeta in one-api.md

.claude/rules/one-api.md:432

RouteMeta shipped in W1 (commit 1c6325ac, marked done in the wave table). The 'W2 deliverable — until it lands, the registry seed is scripts/one-api/route-inventory.json' parenthetical is permanently false and misleads anyone implementing a new route.

low

Mis-indented closing brace of eventData in insertMasterApiKey

domains/core/src/api-key/api-key.master.ts:145

Closing } of eventData object is at 8 spaces instead of 6, inconsistent with surrounding code. Formatter artifact from scope-field removal.

low

InsertApiKeyValues docstring lists 'scopes' as a shell-prepared value — field was removed

domains/core/src/api-key/api-key.queries.ts:189

Comment says 'hashedSecret, prefix, scopes, etc.' — scopes is no longer in InsertApiKeyValues. Minor but misleads someone reading to understand what the shell must prepare.

info

decideRotate 'satisfies its own scopes' comment is orphaned noise after scope removal

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

The parenthetical 'a key always satisfies its own scopes' references removed scope model. The underlying lineage argument is valid — remove only the parenthetical.

info

VESTIGIAL api_keys.scopes rationale duplicated between api-key.type.ts and api-patterns.md

domains/core/src/api-key/api-key.type.ts:80

api-patterns.md is the SSOT for the VESTIGIAL note. The multi-line rationale in the type file could just point to that doc. Minor deduplication opportunity.

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:41current
  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:22
  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