← all branches

feat/one-api

needs attentionviewing older commit
2c248b6 · incrementalpre-PRreviewed 2026-07-27 23:23 UTC8H · 12M · 8L · 5I
The branch
Purpose
Establish a one-org-one-issuer model for the public API v1 — only a designated master key may mint or rotate child keys
Goal
Ship complete /v1/api-keys surface with is_master hierarchy enforced DB-side and in the decision layer, atomic cascade revoke, and auto-provisioned master at org creation
Sub-goals
  • SG-1: ServiceKey entity merged into api_keys table with kind discriminator
  • SG-2: Platform service actor JWT + AuthContext gate
  • SG-3: Internal mount dual-accepts service tokens
  • SG-4: API-first key management — /v1/api-keys CRUD + cascade revoke
  • SG-5: Round-2/3 review hardening (privilege escalation, rotation laundering, never-throw contract)
  • SG-6: Master API key — one org, one issuer (this commit)
The changes (whole branch)
What
Added api-key.master.ts with prepareMasterApiKey + insertMasterApiKey; wired both org creation shells to auto-provision master atomically; added is_master column + partial unique index + migration 0067; updated decisions/errors/queries/shells for master-only minting enforcement; refreshed handler, mapper, and integration tests.
Why
Enforce a clear privilege hierarchy where only a designated organizational master key may issue or revoke child keys, closing the previously flat key-minting surface.
Areas
domains/core+2309144apps/platform+185639packages/api+65643packages/database+1245docs/security+3090.claude/rules+1148.github/workflows+742
Blast
62 files, +5511/-259 lines across domains/core, apps/platform, packages/api, packages/database (excluding 40k-line snapshot JSON)
breaking-change-for-existing-machine-api-users plaintext-drop-functional-gap idor-harness-broken
CI· No PR open yetCodeRabbit· No .coderabbit.yaml

Findings · 29

correctness4

high

Master key plaintext silently dropped from org-creation response

apps/platform/src/api/handlers/organizations.handler.ts

Both org-creation handlers drop result.value.masterApiKey. The secret is never stored (only scrypt hash), so the provisioned master key becomes permanently inaccessible via machine path until dashboard rotation.

high

revokeAllInOrg two-phase SELECT+UPDATE non-atomic — race window for in-flight creates

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

SELECT snapshots live keys then UPDATE by id. A concurrent create between the two statements escapes the sweep. Advisory lock not held in revoke path. Fix: single UPDATE...WHERE...RETURNING or acquire lock.

medium

decideRotate master check === false does not guard undefined; comment misleading

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

Intentional for sessions but comment implies all non-master callers are blocked. Document the three-state intent: undefined/null = session, false = non-master machine, true = master.

low

Orphaned JSDoc block for revokeDescendants sits above revokeAllInOrg

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

Inserting revokeAllInOrg displaced the revokeDescendants comment. Two back-to-back JSDoc blocks confuse readers.

security3

high

No backfill migration — existing orgs lose machine key-to-key minting

packages/database/drizzle/0067_premium_terrax.sql

Migration sets is_master=false for all existing rows. Existing orgs have no master key post-migration. Any customer using machine key-to-key minting via /v1/api-keys will receive 403 not_master with no upgrade path. Silent breaking change.

medium

Error message for non-master reveals master-key architecture in 403 body

domains/core/src/api-key/api-key.errors.ts:246

notMasterKey() error message names the master key tier and suggests the dashboard. Verify public mapper does not include the message field in the external response body.

info

Partial unique index prevents second active master but not two concurrent rotating masters — by design

OCC version check serializes concurrent rotations. No multi-active-master path is reachable.

conventions5

high

granterIsMaster sourcing asymmetry — state-sourced for create, command-sourced for rotate

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

If decideCreate is refactored to read command.granterIsMaster (mirroring rotate), master-only enforcement silently stops because the create handler never sets that field. Document or unify.

medium

api-key.master.ts is not a canonical entity slot

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

Canonical form: .type .decisions .errors .queries .shells .mapper index. Move prepareMasterApiKey to .queries.ts, insertMasterApiKey to .shells.ts or cross-domain.

medium

insertMasterApiKey + prepareMasterApiKey exported outside ApiKeyFCIS namespace

domains/core/src/api-key/index.ts

Top-level named exports bypass ApiKeyFCIS namespace convention. Add to namespace or move to cross-domain.

low

Migration files missing trailing newline

packages/database/drizzle/0067_premium_terrax.sql

0067 sql and _journal.json end without newline. Permanent diff noise.

info

PublicApiKeyResourceSchema satisfies z.ZodType correctly — no drift risk

packages/api/src/schemas/public/api-key.public-schemas.ts

is_master added to both interface and schema with satisfies. No action needed.

tests12

high

IDOR harness uses non-master caller — write route probes now 403 for wrong reason

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

Harness mints callerKey without master credential. Write routes return 403 ApiKeyNotMaster before cross-org check. Cross-org coverage silently lost for create/rotate/revoke routes.

high

Kill-switch test never asserts the master key own final status

apps/platform/src/__tests__/integration/public-v1-api-keys.test.ts:258

Assertion loop iterates only child keys a and b. A regression leaving master in rotating/active state would pass this test.

high

Disposable orgs leak — afterAll deletes api_keys rows but not org or membership rows

apps/platform/src/__tests__/integration/public-v1-api-keys.test.ts:165

freshOrgWithMaster creates org+membership+master key rows. afterAll only cleans api_keys by org id. Org and membership rows accumulate across runs.

high

No handler-level test for DELETE 409 API_KEY_MASTER_NOT_DELETABLE

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

Contract declares 409 for master delete. No integration test fires DELETE /v1/api-keys/:id against the master and asserts 409 with API_KEY_MASTER_NOT_DELETABLE.

medium

No test for one-active-master-per-org unique index enforcement

domains/core/src/api-key/__tests__/api-key.integration.test.ts:333

Partial unique index is the last-resort guard against double-master. insertMasterApiKey called twice for same org should hit the constraint. Untested.

medium

No shell-level test: master rotation must preserve isMaster=true, createdByApiKeyId=null

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

Decision test covers pure function. No shell test confirms the resulting DB row has correct is_master and created_by_api_key_id values post-rotation.

medium

Cross-org isolation not directly tested for master key operations

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

orgA and orgB masters exist in fixture but no test uses orgB master to attempt operations against orgA keys.

medium

Non-master rotate 403 test does not assert error body

apps/platform/src/__tests__/integration/public-v1-api-keys.test.ts:344

Only asserts status 403. A swap from forbidden to insufficient_scope in the mapper would pass silently.

low

Integration test uses tx as never to call insertMasterApiKey

domains/core/src/api-key/__tests__/api-key.integration.test.ts:344

as never cast suppresses a type incompatibility. Use the shell or fix DbOrTx typing.

low

Cap test does not verify master is excluded from the active-key count

domains/core/src/api-key/__tests__/api-key.integration.test.ts:86

countActiveByOrg excludes masters but the exclusion is never exercised in the cap suite.

low

granterIsMaster=undefined (omit) path not explicitly unit-tested in decideRotate

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

Implicit in baseline tests. Add an explicit backward-compat assertion for omitted field.

info

Outbox events for insertMasterApiKey not asserted in integration test

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

insertMasterApiKey writes outbox but shell tests do not assert core.api_key.created event was written atomically.

improvement5

medium

Duplicate 403 forbidden literal in public api-keys mapper

apps/platform/src/api/mappers/public-v1/api-keys.mapper.ts:86

Same inline 403 forbidden object in mapPublicCreateApiKeyError and mapPublicRotateApiKeyError. File already has internalError() and notFound() helpers; add forbidden() to match.

low

tokenForOrg / tokenFor near-duplicate test helpers

apps/platform/src/__tests__/integration/public-v1-api-keys.test.ts:42

Differ only in org source and scopes parameterization. Collapse to one helper.

low

childWithScopes re-fetches master row from DB on every call

apps/platform/src/__tests__/integration/public-v1-api-keys.test.ts:117

Cache master row in beforeAll instead of re-querying per test invocation.

info

updated.orgId guard in revoke shell redundant given DB CHECK constraint

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

DB CHECK api_keys_master_is_org_scoped guarantees orgId non-null when isMaster. Harmless safety-net.

info

Two org-creation shells replicate identical master-seeding block

domains/core/src/organization/organization.shells.ts:69

Acceptable now; consolidate if both shells survive W3 convergence.

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:23current
  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