← all branches

feat/one-api

needs attentionviewing older commit
4701d11 · incrementalpre-PRreviewed 2026-07-27 04:44 UTC0H · 4M · 3L · 2I
The branch
Purpose
Consolidate service credentials (Batu backend callers) into the same api_keys table as customer keys, using a kind discriminator, retiring the shared static INTERNAL_API_KEY. Part of the one-api architecture (W2).
Goal
Ship W2 service keys: one table, kind=service/machine, platform-scoped service credentials with JWT auth, full audit trail, and proper isolation from org-scoped customer keys.
Sub-goals
  • SG-1a: platform-scoped service actor — JWT, AuthContext, meta-gate
  • SG-1b: ServiceKey entity + api_keys table with kind discriminator
  • SG-2: mint service tokens from the same /v1/auth/token endpoint
  • SG-3: internal mount dual-accepts service tokens
  • Refactor: service keys are api_keys rows (one table, kind column)
  • Fix: address review-panel findings (this commit)
The changes (whole branch)
What
This incremental commit hardens the service-key implementation based on prior review findings: (1) adds OrgScopedApiKey type narrowing eliminating the ?? '' coercion; (2) broadens meta-gate scope check from machine-only to service+machine (actorType !== 'user'); (3) enhances createServiceKeyShell with proper transaction, outbox event, actor context, and logging; (4) adds ApiKeyServiceRotationUnsupported error and guard in decideRotate; (5) removes erroneous type-check exclusions; (6) adds CI integration test step; (7) adds real service JWT mapping tests.
Why
Prior review panel flagged: no outbox event on service key create, no actor context, incorrect type-check exclusions, missing rotation guard, wrong public-id prefix (svk_ → apk_).
Areas
domains/core/src/api-key+38343packages/api/src/middleware+1461packages/api/src/auth+20521apps/platform/src/api+23215.github/workflows+150packages/database/src/schema+445.claude/rules+614
Blast
31 files, +1263/-103 lines across core domain, packages/api middleware/auth, platform API layer, and CI workflows. Schema: api_keys table with kind+orgId DB CHECK constraints. Auth: validateBatuPublicApiAuth and meta-gate scope enforcement. No dashboard UI changes.
service-credentials one-api-W2 auth-gate db-schema
typecheck· Not run in this reviewtests· CI results not available for pre-PR branch

Findings · 9

security2

info

Service key revoke handler implicitly guarded by null-orgId comparison rather than explicit kind check

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

revokeApiKeyHandler fetches by publicId (no kind filter) and guards cross-org with existing.orgId !== org.id. For service keys (orgId=null), null !== 'any-org' → 404 — safe. But the guard's intent is implicit. An explicit if (existing.kind === 'service') return notFound() would make the protection clear and survive future handler refactors that might change the cross-org check.

info

createServiceKeyShell lacks the advisory lock that createApiKeyShell uses before its pre-check

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

Customer createApiKeyShell acquires acquireOrgCreateLock before the duplicate-name pre-check to make count→insert race-free. createServiceKeyShell does not acquire a lock; the pre-check is raceable. The DB partial unique index correctly catches the race and the isUniqueViolation handler converts it to serviceNameTaken. This is the backstopped case documented in the comment — not a live vulnerability, but the most privileged credential type has weaker defense-in-depth than customer keys.

tests3

medium

No meta-gate test for service actor with insufficient scope — scope-gate fix branch is untested

apps/platform/src/api/utils/__tests__/public-v1-meta-gate.test.ts:86

The fix changed the scope-enforcement condition from actorType === 'machine' to actorType !== 'user' so service actors are also scope-checked. But every test route that accepts service actors has scopes:[] — so the scope-check branch for service actors is never exercised. A regression to the old condition leaves all tests green. Add a test contract with auth:['service'] + scopes:['catalog:write'] and assert that serviceAuth(['bills:read']) returns 403 insufficient_scope.

medium

Outbox event rows not cleaned up in service key integration tests

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

createServiceKeyShell now writes outbox events. The integration test cleans up apiKeys rows but not domainEvents. If FK is not ON DELETE CASCADE, events accumulate across test runs. The outbox-event assertion (expect(events).toHaveLength(1)) would fail on the second run since the query filters by aggregateId only. Verify CASCADE exists or add explicit cleanup: database.delete(domainEvents).where(eq(domainEvents.aggregateId, apiKey.id)).

low

Rotation test passes orgRotId for a service key — context mismatch in test command

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

rotateApiKeyShell is called with orgId: orgRotId for a service key (orgId: null). The guard checks kind === 'service' before using orgId, so the test passes — but it doesn't match the real call site semantics (a caller trying to rotate a service key wouldn't know which orgId to pass). The test would still pass if the guard were removed and the org-id were accidentally used. Minor — the domain guard is the real protection.

improvement1

low

isUniqueViolation defined three times across core domain under different names

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

The function is now defined locally in api-key.shells.ts (isUniqueViolation), in column-configuration.shells.ts (isPostgresUniqueViolation), and secret-configuration.shells.ts (isPostgresUniqueViolation). Additional inline variants exist in utility/cross-domain. Extract to domains/core/src/shared/ (which already exports outboxQueries and createShellLogger) under a stable name. The api-key variant has the correct cause-fallback; the utility/cross-domain variants that only probe top.code are missing it.

correctness+conventions2

medium

ApiKeyServiceRotationUnsupported maps to API_KEY_INVALID_NAME (wrong HTTP 422 vs declared 400, wrong semantics)

apps/platform/src/api/mappers/api-key.mapper.ts:78

The new ApiKeyServiceRotationUnsupportedError has statusCode:400 on the domain type, but the mapper routes it through fail('API_KEY_INVALID_NAME', ...) which codes.ts maps to HTTP 422. This is a direct convention violation: the domain-declared status (400) is silently overridden, and the public code conflates 'name string malformed' with 'operation unsupported on this credential class'. Callers cannot distinguish the two. Fix: register API_KEY_SERVICE_ROTATION_UNSUPPORTED: 400 in codes.ts and use it here.

low

findByOrg returns rows.map(toApiKey) as OrgScopedApiKey[] with no runtime guard

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

The cast is logically justified (eq(orgId, x) cannot match null-org rows), but it's a type assertion with no narrowing. If the query ever gains an OR branch, the cast silently lies. The downstream mapApiKeyToResponse now reads key.orgId directly with no coercion. A filter(k => k.orgId !== null) before the cast, or a type guard, would make the narrowing structurally enforced.

tests+improvement1

medium

CI integration test step hardcoded to one file; 35+ other integration suites still pass vacuously

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

The new step runs src/api-key/__tests__/api-key.integration.test.ts by exact path. There are 36 *.integration.test.ts files across core/utility/cross-domain/metrics domains — all still early-return without a DB in the validate job. Also, the step is gated on db-changes.outputs.changed == 'true': a logic-only change to api-key.shells.ts would skip it entirely. Fix: use a glob pattern (src/**/*.integration.test.ts per domain filter), and reconsider the condition to also include domain source files.

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