feat/one-api
blockedviewing older commitf8d341a · incrementalpre-PRreviewed 2026-07-29 00:00 UTC2H · 2M · 5L · 4I- Purpose
- Establish a unified public API for Batu Energy, eliminating dual internal/external surfaces. feat/one-api is the W1–W5 implementation program.
- Goal
- W2 scoped-key model: multi-named API keys per org with a scope grammar, feeding the auth gate (withMetaGate) that enforces per-route credential requirements.
- Sub-goals
- SG-1: api_keys schema with scopes column, SCOPE_PATTERN validation, CreateApiKeyCommand carrying scopes
- SG-2: signBatuJwt / verifyBatuJwt carry scopes claim in ES256 JWT
- SG-3: AuthContext.scopes populated from JWT; meta-gate enforces machine+service scope check
- SG-4: route-meta.guard.test pins scope-op-matches-verb invariant across entire contract tree
- SG-5: revert the 'one credential per org / master key' model that was introduced and reverted in this commit
- What
- Reverts the 'master-key / one-credential-per-org' model (isMaster, secretArn, createdByApiKeyId, cascaded revocation, reveal route) that was introduced in a prior commit, and restores the W2 multi-key scoped model. Adds scopes to JWT claims, AuthContext, API response, and event payload. Re-wires meta-gate scope enforcement and expands the test suite to cover scope satisfaction, session bypass, and service-actor scope checks.
- Why
- The master-key model introduced complexity (cascade revocation, advisory locks, Secrets Manager reveal path) that conflicted with the simpler multi-key scoped model the team agreed on. The revert focuses the branch back on the scope grammar and gate enforcement that is the actual W2 deliverable.
- Areas
- domains/core/src/api-key+450−380packages/api/src+220−80apps/platform/src/api+210−120.claude/rules+95−85packages/database/src/schema+47−60docs/security+304−0
- Blast
- 39 files changed in this incremental commit. Core auth stack (JWT, AuthContext, meta-gate), api-key domain (type, decisions, errors, shells, queries, mapper, events), database schema (3 columns removed, 2 indexes removed), and test suites across all layers.
Findings · 13
correctness2
No backward-compat fallback for missing `scopes` claim — all existing tokens get 401 on prod merge
packages/api/src/auth/public-jwt.ts
verifyBatuJwt returns err({_tag:'PublicJwtClaimsInvalid', statusCode:401}) when payload.scopes is absent. Tokens minted on main (before W2) have no scopes claim. Unlike the actor claim — which defaults to 'machine' for old tokens — scopes has no fallback. On prod deploy every in-flight customer token will be rejected until re-exchanged. Fix: `const scopes = Array.isArray(payload.scopes) ? payload.scopes.filter((s): s is string => typeof s === 'string') : ['*'];` — old tokens had full access, so ['*'] is the correct default. Only reject when !apiKeyId.
`revokeApiKeyShell` drops .catch — DB errors now escape as thrown Promises, violating FCIS contract
domains/core/src/api-key/api-key.shells.ts
The .catch handler was removed along with the cascade, but updateWithVersion and outboxQueries.insert can still throw (network errors, constraint violations, postgres.js boundary rethrows). The original comment in the removed .catch said: 'postgres.js rethrows the recorded error at the boundary, so without this the shell would throw a PostgresError out of a Promise<Result<…>> at a caller with no try.' That hazard is unchanged by simplifying the body. Fix: reinstate .catch((e: unknown) => { log.error('Revoke failed', e); return err(ApiKeyErrors.databaseError('update')); }) on the db.transaction() call.
security2
`actorType !== 'user'` scope-check bypass is fragile against future actor types
apps/platform/src/api/utils/public-v1-meta-gate.ts:106
The gate condition `auth.actorType !== 'user'` means any future actor type added to AuthContext (e.g. 'webhook', 'internal') would bypass the scope check without an explicit decision. Consider a positive allowlist `(auth.actorType === 'machine' || auth.actorType === 'service')` to make the invariant structurally explicit.
Broad shorthand scopes ('read', 'write') grant cross-capability access with no creation-time warning
packages/api/src/auth/public-jwt.ts
A key created with scopes=['read'] satisfies any cap:read requirement (bills:read, files:read, assets:read, etc.) via scopeSatisfied. This is correct per the grammar but creates a surprising credential: a customer who intends a narrowly scoped key for one integration gets a read-key for the entire API surface. Consider surfacing this in the creation response or UI — no code change required now, but note it for the key management UX work.
conventions3
Stale JSDoc 'v1 only allows the wildcard scope' contradicts the implemented scope grammar
domains/core/src/api-key/api-key.decisions.ts:44
Two consecutive JSDoc blocks precede SCOPE_PATTERN. The first ('v1 only allows the wildcard scope. Future expansion adds entries here.') directly contradicts the regex below it, which already accepts the full grammar (bills:read, monitoring:write, etc.). A developer reading it would wrongly conclude non-wildcard scopes are not yet live. Remove the first JSDoc block — the second one is the accurate description.
`cascadedFrom` removed from ApiKeyRevokedEvent — verify no external EventBridge consumers
domains/core/src/events/api-key.events.ts:60
cascadedFrom?: string was removed from ApiKeyRevokedEvent.eventData and its Zod schema. Grep inside the monorepo shows zero remaining references. Confirm no external Lambda ARNs or analytics pipelines subscribed to core.api_key.revoked rely on this field — they would silently receive undefined without breaking deserialization (field was optional).
`reveal` route removed — confirm no UI hooks still call POST .../reveal
apps/platform/src/api/contracts/api-keys.contract.ts
POST /organizations/:orgId/api-keys/:publicId/reveal removed from the contract. The diff removes RevealApiKeyResponseSchema import. Grep confirms no useApiKeys hooks or page components call it. Clean inside the monorepo — ensure any external Postman collections or docs referencing this endpoint are updated.
tests3
No test for `verifyBatuJwt` with a pre-W2 token (no `scopes` claim) — critical finding has no regression guard
packages/api/src/auth/__tests__/public-jwt.test.ts
Finding #1 (scopes backward compat) has no corresponding test. A test signing a minimal JWT without scopes and asserting the verify path (whether it should return err or default to ['*']) would pin the invariant and prevent it silently regressing. Given that the fix is one line, the test is also one test — add both.
No test for `revokeApiKeyShell` DB error path after .catch removal
domains/core/src/api-key/__tests__/api-key.shells.test.ts
After the .catch is reinstated (finding #2), a test that mocks db.transaction to throw and asserts the shell returns err(databaseError('update')) would pin the shell's never-throw contract. Without it, a future refactor can remove the catch again and CI won't catch it.
No test for ApiKeyCreatedEventSchema rejecting a payload without the new required `scopes` field
domains/core/src/events/api-key.events.ts:129
scopes: z.array(z.string()) was added as a required field to ApiKeyCreatedEventSchema. A parse test with scopes omitted would confirm the schema enforces the new field. Minor — type safety mostly covers this via the satisfies constraint, but a runtime Zod parse test at the schema layer is good practice for event contracts.
improvement3
Empty 'Helpers' section divider is dead scaffolding after isUniqueViolation moved inline
domains/core/src/api-key/api-key.shells.ts:61
The '// Helpers' section comment block is now empty — isUniqueViolation moved to a local function inside the service-key block. Remove the empty section header to reduce noise.
Stale 'argon2id' references in comments and test fixtures — actual algorithm is scrypt
domains/core/src/api-key/api-key.shells.ts:10
Module doc says 'argon2id hash' and test fixtures use '$argon2id$' prefix. The actual hash in api-key.lib.ts is Node's crypto.scrypt (not argon2id). The fixture strings never flow through verifySecret so there's no runtime bug, but security-critical comments should name the right algorithm. Change comments and fixture prefixes to 'scrypt:'.
Handler pre-flight findByPublicId causes a double-read on every mutating operation
apps/platform/src/api/handlers/api-keys.handler.ts
revoke, rotate, and delete handlers each call findByPublicId outside the transaction for org-ownership validation, then the shell calls it again inside the transaction. Two round-trips for the same row. The shell could accept a callerOrgId guard and return ApiKeyOrgMismatch — one read instead of two. Low priority; note for when the shell surface stabilises.
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:00current
- 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:22
- 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