feat/one-api
needs attentionviewing older commitf93bca9 · incrementalpre-PRreviewed 2026-08-10 17:51 UTC2H · 5M · 8L · 5I- 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)
- 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+77−12domains/core+4−32packages/database+3−1packages/api+1−1domains/utility+2−1.claude/rules+3−3scripts+2−1
- 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)
Findings · 17
correctness2
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.
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
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.
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.
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.
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.
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.
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
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.
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 ["*"])'
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.
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
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.
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
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.
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.
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
- 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:51current
- 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:00
- 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