← all branches

feat/one-api

needs attentionviewing older commit
e1757b8 · incrementalpre-PRreviewed 2026-08-05 02:11 UTC0H · 3M · 6L · 5I
The branch
Purpose
Deliver the Batu public API (one-api): public v1 contract, service keys, machine RLS, api-key management, webhook infrastructure, monitoring reads, and contract access gating.
Goal
Harden the remaining FCIS gaps and test coverage on service-key creation and contract-access resolution, and eliminate the RLS proxy code duplication that was a drift hazard for the tenant boundary.
Sub-goals
  • SG-1: Extract decideCreateService to complete parity with all sibling decisions on api-key entity
  • SG-2: Add unit tests for decideCreateService (name grammar, duplicate, shape, event assertions)
  • SG-3: Add contract-access integration test exercising all 4 outcomes including the requires_site recovery path
  • SG-4: Deduplicate createRLSDb / createMachineRLSDb via rlsProxy shared helper
  • SG-5: Wire contract-access.test.ts into CI pr-checks.yml
The changes (whole branch)
What
Pure decideCreateService decision function extracted from createServiceKeyShell (completing FCIS parity). RLS proxy refactored to eliminate ~26-line code duplication between user and machine wrappers. Integration test added to build and assert the stranded-contract (requires_site) state that no seeder currently produces.
Why
Previous review rounds flagged that createServiceKey was the only operation on this entity whose validation required a database — the extraction closes that gap. The RLS duplication was a drift hazard: a hardening (timeout, search_path pin) applied to one copy would silently miss the other.
Areas
packages/database/src/rls.ts+2357domains/core/src/api-key/api-key.decisions.ts+1000domains/core/src/api-key/api-key.shells.ts+822domains/core/src/api-key/__tests__/api-key.decisions.test.ts+890apps/platform/src/__tests__/integration/contract-access.test.ts+1760.github/workflows/pr-checks.yml+10
Blast
6 files, +397/−79 lines. Scoped to api-key decisions/shells, the RLS module, and two test suites. No schema changes, no migrations, no UI changes.
event-unused-in-shell double-trim-split-ownership test-rerun-hazard
CI· gh auth unavailable in this run — CI status not fetchedCodeRabbit· No .coderabbit.yaml in repo

Findings · 15

correctness4

medium

decision.value.event is computed but never used — shell re-derives outbox inline

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

All four sibling operations (createApiKeyShell, rotateApiKeyShell, revokeApiKeyShell, deleteApiKeyShell) pass decision.value.event.eventData to the outbox insert. createServiceKeyShell ignores decision.value.event entirely and re-derives the outbox payload inline from the inserted row (publicId, orgId, name, prefix, scopes). The CreateServiceKeyDecision.event field is dead: any future change to the event shape in decideCreateService will silently not affect what is persisted. This breaks the FCIS guarantee that the decision is the sole source of what-to-write. Fix: use decision.value.event.eventData in the outbox call (post-filling aggregateId from inserted.id as the decision documents), or drop the event field from CreateServiceKeyDecision to make the omission intentional and visible.

medium

Shell double-trims: uses trimmed name for DB query but passes untrimmed command.name to decision

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

Line 452 computes `const name = command.name.trim()` and uses it for findActiveServiceByName (line 467). Line 472 passes `{ name: command.name }` (untrimmed) to decideCreateService, which re-trims internally. Both paths resolve to the same value today, but ownership of trimming is split between shell and decision — a fragile asymmetry not present in any sibling operation. The canonical FCIS pattern: either the shell normalises once and passes the normalised value everywhere, or the decision owns normalisation and the shell passes raw input. Pick one: pass `name` (already trimmed) to decideCreateService and remove the re-trim there, or keep the re-trim in the decision and have the shell query with the decision-resolved name.

info

rlsProxy is mechanically correct — is_local=true correctly scopes both GUCs to the transaction

packages/database/src/rls.ts:44

set_config('request.jwt.claims', ..., true) and SET LOCAL ROLE are both transaction-scoped. The refactoring from two 26-line copies to a shared helper changes no behavior. The Reflect.get fallback is standard Proxy practice. RLS drift between the two wrappers is now structurally prevented.

info

SERVICE_NAME_PATTERN regex is correct — 1-64 char range verified by tests

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

/^[a-z][a-z0-9-]{0,63}$/ encodes exactly 1-64 chars. The unit tests confirm 'a' (1), 'a'.repeat(64) (64) pass and 'a'.repeat(65) (65) fails.

security3

low

rlsProxy claims parameter is an open Record<string,string> — type does not constrain future callers

packages/database/src/rls.ts:36

Both current callers pass verified JWT values so there is no injection path. However, rlsProxy accepts any Record<string,string>, making it easy for a future caller to pass an untrusted string. A closed type (discriminated union of the two valid claim shapes, or separate typed overloads) would let the type system prevent misuse before runtime.

info

Service key scopes=['*'] is hardcoded — acknowledged as architectural debt

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

No active vulnerability: service keys authenticate on the internal mount only, and no HTTP route exposes createServiceKeyShell to external callers. The code comment acknowledges this will be revisited when a real permissions model lands.

info

rlsProxy non-transaction passthrough is documented — no new exposure

packages/database/src/rls.ts:36

The proxy only intercepts .transaction(); bare selects pass to the admin connection. This is documented in the module header and rls-checklist.md. No change in behavior from this diff.

conventions1

info

CreateServiceKeyPrepared.hashedSecret naming pulls infra term into decisions layer

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

The type is structurally clean (no infra imports), but naming a field 'hashedSecret' in decisions.ts leaks an implementation detail from the scrypt pre-computation step. Consider 'secretHash' or moving the PreparedInput type to api-key.shells.ts where it is produced. Not a functional issue.

tests6

medium

Hardcoded contract RPUs ('900000000001-3') will hit unique constraint on re-runs if cleanup is partial

apps/platform/src/__tests__/integration/contract-access.test.ts:66

The partial unique index uq_utility_contracts_one_active_per_rpu blocks multiple active rows with the same contract_number. mkContract inserts status='active' contracts with hardcoded contractNumbers. If beforeAll partially succeeds (e.g. sited contract inserted, then site insert throws), afterAll returns early via `if (!available) return` without cleaning up, leaving active contracts with those RPUs. The next run fails with a unique-constraint violation. Fix: suffix the contractNumbers with a timestamp (same approach used for publicIds) so each run gets unique RPUs — e.g. `` `900000000${Date.now().toString().slice(-3)}` `` — or make the partial-setup cleanup unconditional.

low

Grammar tests miss trailing-hyphen and consecutive-hyphen edge cases

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

SERVICE_NAME_PATTERN allows 'cfe-' (trailing hyphen) and 'cfe--pipeline' (consecutive hyphens). Neither is tested. If the spec intends stricter kebab-case (no trailing or consecutive hyphens), these should be reject cases; if the permissive behavior is intentional, they should be documented accept cases.

low

Whitespace-only service name (trim → empty) not tested

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

The trim test only covers ' cfe-pipeline ' (valid after trim). A whitespace-only input (' ') trims to '' and fails SERVICE_NAME_PATTERN correctly, but there is no test for this path. The sibling decideCreate test suite includes it.

low

contract-access.test.ts passes service-role db — RLS enforcement layer not exercised

apps/platform/src/__tests__/integration/contract-access.test.ts:140

The test passes the bare admin database (service-role) as rlsDb. RLS claims are not injected, so the transaction reads all rows. The business-logic gate is validated correctly, but a RLS policy regression that leaked cross-org rows would pass these tests. A companion assertion with createRLSDb(database, fixture.userA.authId) would complete the coverage.

low

Silent-pass pattern: `if (!available) return` gives false-green when DB is absent

apps/platform/src/__tests__/integration/contract-access.test.ts:139

Vitest counts early-return tests as passing (0 assertions) rather than skipped, producing a misleading green signal. Use `test.skipIf(!available)(...)` or `ctx.skip()` in beforeAll so the reporter surfaces the skip.

info

not_found sentinel is a 27-char ID vs the standard 30-char prefix_ULID format

apps/platform/src/__tests__/integration/contract-access.test.ts:163

'uct_01DOESNOTEXIST000000001' is 27 chars; standard public IDs are 30. findByPublicId does a simple WHERE clause so this works, but a realistic 30-char fabricated ULID would better model the production path.

improvement1

low

Dead blank line left after inline grammar check removal

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

After `const name = command.name.trim();` there is a leftover blank line where the removed inline validation block used to be. Remove it.

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