← all branches

feat/one-api

needs attentionviewing older commit
f93bca9 · incrementalpre-PRreviewed 2026-08-10 17:51 UTC2H · 5M · 8L · 5I
The branch
Purpose
One-API consolidation: merge dual internal/public API surfaces into a single contract per capability, establish withMetaGate as the unified auth gate, and mechanically enforce FCIS boundaries, RLS safety, and security invariants with shrink-only ratchets.
Goal
Make the public API the canonical surface (public contract wins parity, dashboard retargets), remove the internal twin, and prove the security properties of the resulting single gate with automated tests that fail on regression.
Sub-goals
  • SG-1: One contract per capability — no -internal twin
  • SG-2: withMetaGate replaces withApiAuth as the single enforced auth gate
  • SG-3: FCIS boundary ratchets (D5/H1/P1) mechanically enforced in CI
  • SG-4: Security invariants (authuid wiring, timing equalization, multi-candidate iteration, rate-limit header) proven with dedicated tests
  • SG-5: Anon role locked down to lead_submissions INSERT only
  • SG-6: BAT-301 Context/EntityRelationship removal fully cleaned up (error classes, seeders, test docs)
The changes (whole branch)
What
Round-3 batch: moved emit() before rate-limit check so 429 is telemetered; added x-real-ip priority test; added wizard-authuid-wiring test; added api-key-verify timing/iteration tests; added uco_orgs_reaching DISTINCT pin; refactored FCIS boundary checker to per-rule keys and widened H1 scope; added KNOWN_UNUSED error code ratchet; migration 0076 locks anon role down to lead_submissions only; contract-access.test.ts updated to 3-outcome suite; Context error classes deleted.
Why
Review rounds 1+2 identified gaps that tests passed vacuously (DB skip as green tick, 429 not telemetered, authuid wiring not independently verified, anon role too permissive). Round 3 closes those gaps with mechanical enforcement.
Areas
apps/platform+7712domains/core+432packages/database+31packages/api+11domains/utility+21.claude/rules+33scripts+21
Blast
21 files in incremental window (+309/-116 lines net, excluding the 13k-line snapshot JSON); cumulative branch: 231 files +169,722/-10,379 across apps/platform (77), domains/core (48), packages/database (22), packages/api (22)
security-hardening test-infrastructure ratchet-mechanical bat-301-cleanup
ci· no CI checks on pre-PR branchcoderabbit· no .coderabbit.yaml in repo

Findings · 17

correctness2

high

contract-access.test.ts tests 142-187 lack `if (!available) return;` guards

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

If beforeAll partially fails (seedTwoOrgsWithUsers() succeeds but mkContract() throws), available stays false and sitedContractPublicId/foreignContractPublicId/orgAId remain undefined. The 5 tests at lines 142, 147, 152, 169, 180 have no `if (!available) return;` guard — they execute against undefined variables. Vitest does not fail-fast on a failing peer test, so the 'fixture built' sentinel does not protect them. Contrast with rls-machine-claim.test.ts which has the guard in every test. Fix: add `if (!available) return;` to each test, or use beforeEach to assert availability.

medium

KNOWN_UNUSED ratchet scan counts test-file references as production callers

apps/platform/src/api/contracts/__tests__/declared-status.guard.test.ts:175

The workspace walk includes __tests__ directories. A code referenced only in a test assertion (e.g. expect(error._tag).toBe('ENTITY_NOT_FOUND')) satisfies the 'has a caller' check and removes it from KNOWN_UNUSED, masking that the code has no production caller. If the intent is to detect codes with no production callers, __tests__ dirs should be excluded from the blob construction. Currently the 6 KNOWN_UNUSED codes may be dead in both production and tests — a test-only reference would silently shrink the list.

security6

medium

x-real-ip fix not applied to main v1 ts-rest rate limiter

apps/platform/src/app/api/v1/[...ts-rest]/route.ts:91

The x-real-ip priority fix (prefer platform-injected header over client-appendable x-forwarded-for) was applied only to the OAuth token standalone route. The main ts-rest mount's rateLimitBucket function still keys anonymous requests on x-forwarded-for's first hop only. An unauthenticated attacker can rotate rate-limit buckets by appending fake IPs to x-forwarded-for. Authenticated requests (keyed on Bearer token hash) are unaffected. The fix pattern is in route.ts lines 87-90.

medium

auth.handler.ts getClientIP has inverted x-real-ip / x-forwarded-for priority

apps/platform/src/api/handlers/auth.handler.ts:374

The getClientIP helper for /auth/forgot-password and /auth/reset-password rate limiters checks x-forwarded-for first, then x-real-ip — the inverse of the correct Vercel priority. A client can spoof x-forwarded-for to bypass per-IP rate limits on password-recovery endpoints. Pre-existing bug not introduced here, but round-3 fixed the OAuth route without porting the fix to auth.handler.ts.

low

ALTER DEFAULT PRIVILEGES without FOR ROLE clause: scope limited to migration runner

packages/database/drizzle/0076_anon_lockdown.sql:21

ALTER DEFAULT PRIVILEGES without FOR ROLE only revokes future auto-grants for tables created by the current migration runner role. If a future table is created by a different Postgres role, it would still auto-grant ALL to anon. Low severity because Supabase migrations consistently run as postgres superuser, matching the existing 0072 pattern.

low

lead_submissions anon INSERT: no DB-layer content validation

packages/database/drizzle/0076_anon_lockdown.sql:25

GRANT INSERT on lead_submissions to anon with WITH CHECK (true) RLS policy means any unauthenticated request can insert arbitrary data (within column types). Intentional design for the marketing lead form, but spam/abuse prevention is entirely the API handler's responsibility with no DB-layer fallback.

info

Timing equalization and multi-candidate iteration tests are genuine non-vacuous

apps/platform/src/api/handlers/public-v1/__tests__/api-key-verify.test.ts:421

Both invariants are correctly tested. The timing-equalizer test verifies verifySecret is called with the actual dummy hash string (not a Promise). The multi-candidate iteration test confirms a match on the 2nd row is found, killing the candidates[0] simplification. The equalization design is sound — the real scrypt hash format passes all format checks in verifySecret.

info

Supabase Realtime unaffected by anon table REVOKE

packages/database/drizzle/0076_anon_lockdown.sql:19

Realtime CDC delivery operates at WAL replication level using service-role connections — not subject to table-level grants. Per-subscriber RLS still evaluates under the subscriber's JWT role. The REVOKE does not break any active realtime subscription (cfe_jobs, utility_contract_overview both have auth.uid()-based RLS policies).

conventions4

high

Shrink-only H1 ratchet grown with same-branch new files

scripts/check-fcis-boundaries.mjs:47

canonical-form.md § Enforcement Status: 'its allowlist is a shrink-only ratchet of pre-existing deviations.' Both apps/platform/src/api/utils/event-publisher.ts and apps/platform/src/api/utils/public-v1-telemetry.ts were created on this branch and immediately added to the ALLOWLIST with H1 entries in the same PR. The script's own header says 'Shrink it; never grow it silently.' Even with documented justifications, self-allowlisting same-branch new files breaks the invariant. Correct path: route through existing FCIS queries modules, or add a separate architectural exception review.

medium

one-api.md falsely claims scopes enforcement is deferred

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

Line 30: 'One gate (withMetaGate wrapping withPublicApiAuth) enforces the route's declared auth (+ scopes, when the grants model returns).' The parenthetical implies scopes are not yet enforced. public-v1-meta-gate.ts lines 172-176 confirm scopesSatisfied() is called today on every machine/service request; routes like asset-management.contract.ts declare active scopes. What is deferred is the per-key scope restriction UI (all keys default to ['*']), not the enforcement machinery. Line 439 of the same doc contradicts this. Fix: 'enforces auth + scopes (per-key scope restriction via grants model deferred — all keys currently carry ["*"])'

low

KNOWN_UNUSED pins dead codes instead of deleting them from codes.ts

apps/platform/src/api/contracts/__tests__/declared-status.guard.test.ts:195

The test's own comment says 'delete the dead code from codes.ts instead' for unused codes. But KNOWN_UNUSED lists 6 codes (EMAIL_REQUIRED, ENTITY_NOT_FOUND, HAS_ASSETS, PAIR_EXISTS, PLAN_NOT_FOUND, SOURCE_NOT_FOUND) that remain registered. CONTEXT_NOT_FOUND survived BAT-301 by a week for exactly this reason. The KNOWN_UNUSED list should be empty — any code in it is deferred cleanup the ratchet was designed to force immediately.

info

metrics-explore-core.ts::H1 allowlist entry description borrows D5 sweep narrative inaccurately

scripts/check-fcis-boundaries.mjs:51

The entry says 'same class as the handler sweep of 2026-08-08' — but this is from the H1 scope widening (utils/**), not the D5 handler sweep. The description is misleading. The file did pre-exist but was previously invisible to the rule. The entry needs 'H1 scope widening revealed this existing file' wording.

tests2

low

wizard-authuid-wiring mock returns bare {} for RLS db — opaque failure on extension

apps/platform/src/api/handlers/__tests__/wizard-authuid-wiring.test.ts:23

vi.mock('@batu/database/rls', () => ({ createRLSDb: () => ({}) })) returns an object with no methods. If the handler is extended to call .transaction() on the RLS db, the test fails with a TypeError ('is not a function') rather than a meaningful assertion failure. A stub like { transaction: vi.fn(async (fn) => fn({})) } would give a better signal. Non-blocking today.

info

contract-access list-filter negative test no longer exercises its stated invariant

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

The test claims to pin that the list filter has no entity_relationships branch, but since entity_relationships was dropped (BAT-301), the negative fixture (contract reachable only via ER) cannot be constructed. The test now verifies basic cross-org isolation (already covered elsewhere). The assertion is correct but its stated purpose is no longer achievable. Update the comment to reflect what is actually tested.

improvement3

low

Rollback-via-thrown-sentinel pattern will drift without a shared helper

domains/utility/src/utility-contract-overview/__tests__/utility-contract-overview.integration.test.ts:640

The `class Rollback extends Error {} + .catch(e => { if (!(e instanceof Rollback)) throw e; })` pattern encodes rollback intent as an exception. Repeating it per test will drift (callers forget the .catch, or use a different sentinel class). A shared `withRollback(async (tx) => {...})` helper owned once by the test suite is the right home.

low

describeDb guard pattern repeated across integration test files

apps/platform/src/__tests__/integration/rls-machine-claim.test.ts:39

The two-line `hasTestDb + describeDb = describe.skipIf(!hasTestDb)` pattern almost certainly appears in multiple integration test files. The env-var names (POSTGRES_URL, POSTGRES_URL_DIRECT) should be maintained in one place. If a third env-var is added, every file copying this snippet needs updating.

low

FS walk duplicated inside declared-status test body vs check-fcis-boundaries.mjs

apps/platform/src/api/contracts/__tests__/declared-status.guard.test.ts:166

The recursive walk() and blob-construction loop in the KNOWN_UNUSED test are near-identical to the walk() in check-fcis-boundaries.mjs. The exclusion lists (node_modules, dist, .turbo, .next) are maintained separately and will diverge. Extracting a shared walkSourceFiles() helper would unify them.

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