feat/one-api
needs attentionviewing older commit62ec3f7 · incrementalpre-PRreviewed 2026-08-10 22:51 UTC2H · 5M · 6L · 6I- Purpose
- Consolidate Batu's API surface into a single public contract tree (one-api / W1-W5 roadmap), while hardening security: RLS enforcement for machine callers, removal of dead webhook infrastructure, per-request telemetry, and a series of security review round-fixes.
- Goal
- Ship W2 (machine auth + per-request telemetry) and W2.5 (instrument before converging) while closing every security hole found by the panel's 5 review rounds.
- Sub-goals
- SG-1: Machine JWT with batu_org_id claim — database-enforced tenant isolation
- SG-2: Per-request telemetry (api_request_events) — usage measurable before W3
- SG-3: Remove webhooks (zero users, 6 defects) — BAT-318 will rebuild canonically
- SG-4: Close RLS squat holes on utility_contracts (0077-0080 migrations)
- SG-5: Anon lockdown on lead_submissions
- SG-6: FCIS boundary checker + D0 default-deny ratchet
- What
- Round-5 batch: closes UPDATE-side squat (0078 drops contracts_update_admin policy with withCheck:true), pins the ALLOWLIST_SIZE_PIN sentinel in fcis-boundaries, finishes the anon lockdown on lead_submissions (0079/0080). Adds isUniqueViolation mapper in wizard shells, fixes isRlsRefusal to check both drizzle wrapper and driver message levels, removes hasSite from utility-contract wire contract, upgrades metric-collection-job type-check to AssertEqual, and pins MCP tools count to exact 30.
- Why
- Closing the final security holes identified in review round 5 before the branch is ready for merge review. The UPDATE squat (contracts_update_admin withCheck:true) would have let any authenticated session rewrite an own-bonded contract's contract_number to an RPU belonging to another org.
- Areas
- apps/platform+4235−4027domains/core+1382−3342packages/database+917−351packages/api+681−1380domains/cross-domain+570−299domains/utility+182−326.claude/rules+592−33scripts+657−303docs+514−0domains/metrics+27−13
- Blast
- 255 files, +10206/-10564 lines across the full branch; incremental batch (rounds 4-5) touches 56 files across security guards, migrations, test coverage, and documentation.
Findings · 19
correctness2
hasSite removed from mapper/schema but integration test still asserts it — will fail CI
apps/platform/src/__tests__/integration/utility-contracts-api.test.ts:319
hasSite was removed from UtilityContractListItemResponseSchema, the API type, and the mapper in this diff. The integration test at line 319 still asserts expect.objectContaining({ hasSite: true }). The API response no longer emits this field, so the assertion will fail. This file was not touched in the diff — the removal was incomplete.
Stale hasSite mock in cdc-harness.ts
apps/platform/src/lib/hooks/realtime/__tests__/cdc-harness.ts:364
makeOverviewContractsListResponse still sets hasSite: false in its mock objects. The return type is Array<Record<string, unknown>> so no compile error, but the field is now undefined contract surface. Harmless since no current consumer reads hasSite from this harness.
security3
Documented product risk: RPU bonding without ownership verification (BAT-298)
domains/cross-domain/src/contract-wizard.shells.ts:1399
Any org knowing a 12-digit RPU can bond it via the wizard and read collected bills — no ownership-verification challenge. Explicitly acknowledged in the diff as a KNOWN OPENNESS on purpose. BAT-298 tracks this. No action needed, recorded for the security review record.
Service actors bypass RLS on public-v1 reads — documented deliberate posture
apps/platform/src/api/utils/public-v1-rls.ts:67
rlsDbFor returns bare service-role for 'service' actorType. Intentional — service credentials are platform-scoped. The rlsDbFor dispatch test pins this. Any future 'service'-scoped route that does a per-org read would silently cross org boundaries without an additional application-layer filter.
Rate-limit key prefers x-real-ip — security improvement noted
apps/platform/src/app/api/v1/[...ts-rest]/route.ts:1074
The change correctly prefers x-real-ip (platform-set, cannot be forged) over x-forwarded-for (client-appendable) for rate-limiting. This closes a trivial bypass on the scrypt endpoint. Positive change.
conventions2
console.log in wizard handler error path leaks internal discriminators
apps/platform/src/api/handlers/utility-contracts.handler.ts:718
Line 718 `console.log('[Wizard] Shell returned error:', error._tag)` emits raw domain error tags to stdout. The canonical pattern uses createShellLogger for structured logging. Pre-existing but touched by this diff.
Wizard error mapper uses if-chain, not exhaustive switch on _tag
apps/platform/src/api/handlers/utility-contracts.handler.ts:718
The canonical form requires exhaustive switch on error._tag. The wizard handler uses sequential if (error._tag === '...') chains; the final arm falls through on statusCode. Pre-existing, but the diff removes two error types from the union without converting the if-chain.
tests8
utility-contracts-api.test.ts still asserts hasSite:true after field removal
apps/platform/src/__tests__/integration/utility-contracts-api.test.ts:319
Same as correctness finding — the hasSite field was removed from the wire contract but the integration test still expects it. This will cause a CI failure.
No RLS SELECT isolation test for utility_contracts after security hardening
apps/platform/src/__tests__/integration/rls-isolation.test.ts:285
The diff adds RLS tests for INSERT/UPDATE/DELETE on utility_contracts (closing 0077/0078 squat holes). However, there is no withRLS test verifying that org B cannot SELECT contracts belonging to org A (the contracts_select_member policy). A policy regression in contracts_select_member would not be caught at the raw RLS layer — only at the HTTP IDOR harness level.
rateLimitBucket x-real-ip preference in main v1 catch-all route has no test
apps/platform/src/app/api/v1/[...ts-rest]/route.ts:95
The security fix (prefer x-real-ip over x-forwarded-for) is tested for the OAuth standalone route but has no equivalent test for the main v1 catch-all route. The scrypt /auth/token endpoint passes through this route and the x-real-ip spoofing fix for it is untested.
isRlsRefusal dual-level message check has no unit test for the nested-driver case
domains/cross-domain/src/contract-wizard.shells.ts:118
The diff fixes isRlsRefusal to check both the drizzle wrapper message AND the driver's nested message. No unit test constructs the specific error shape ({ code: 'X', message: 'Failed query', cause: { code: '42501', message: 'row-level security policy violated' } }) to pin that the nested check fires. A regression that reverts to the old ?? pattern would pass integration tests if drizzle happens to propagate the phrase in that environment.
resolveContractAccess cross-org test uses service-role db — RLS not exercised
apps/platform/src/__tests__/integration/contract-access.test.ts:148
resolveContractAccess wraps its query in .transaction() for RLS. The integration test passes the raw service-role database connection (line 28: `const database = db as unknown as Database`). The cross-org isolation test relies entirely on the SQL WHERE clause — RLS is bypassed. A missing WHERE would pass the test but fail in production.
rls-isolation.test.ts missing DB skip guard — inconsistent with siblings
apps/platform/src/__tests__/integration/rls-isolation.test.ts:45
No describeDb/skipIf guard for POSTGRES_URL. Without it, running without a DB gives connection errors rather than clean SKIPPED. CI always has a DB but developer experience (local runs) is impacted. Sibling files (contract-access.test.ts, rls-machine-claim.test.ts) all have this guard.
Double-skip guard in api-telemetry-persistence can mask misconfigured-DB failures
apps/platform/src/__tests__/integration/api-telemetry-persistence.test.ts:95
Tests inside describeDb also guard with `if (!available) return;`. When DB is configured (POSTGRES_URL set) but unreachable, describeDb does NOT skip but !available causes every test to silently pass. A misconfigured-but-set DB will show all tests as PASSED. Pre-existing pattern extended to new blocks.
MCP tools/list smoke test upgraded to exact pin — adding a tool requires updating the count
packages/mcp-server/src/tools-list.smoke.test.ts:1752
Changed from toBeGreaterThanOrEqual(30) to toBe(30). This is an improvement that catches dropped tools. The sync checklist in .claude/rules/mcp-server.md should cross-reference this test file for discoverability.
improvement4
Two structurally identical getAccessibleContracts helpers in bills.handler and csv/route
apps/platform/src/api/handlers/bills.handler.ts
After removing the entity_relationships branch from both, getAccessibleContracts in bills.handler.ts and csv/route.ts are now structurally identical (both call listContractPublicIdsByOrgId → findByPublicIds → findMonitoredContractIds). Extracting a shared helper would make the next change (RLS-wrapping these reads) a one-place change.
Migration files 0077 and 0079 missing trailing newlines
packages/database/drizzle/0077_tired_venus.sql
Both 0077_tired_venus.sql and 0079_deep_jean_grey.sql are missing the POSIX-required trailing newline (diff shows \\ No newline at end of file). Drizzle runner is unaffected, but this causes noisy git diffs and trips POSIX linters. Sibling 0080 ends with a newline.
mapWizardTxFailure passes empty string for missing contractNumber
domains/cross-domain/src/contract-wizard.shells.ts:192
The isUniqueViolation branch calls WizardErrors.contractExists(contractNumber ?? ''). The empty-string fallback is misleading: if the shell ever calls mapWizardTxFailure without a contractNumber, the error payload carries an empty RPU number which is invisible in logs. Either remove the ? (make it required) or use a sentinel like '<unknown>'.
isRlsRefusal and isUniqueViolation parallel pattern — shared pgHasErrorCode helper opportunity
domains/cross-domain/src/contract-wizard.shells.ts:114
Both functions independently check cause.code and cause.cause?.code in the same two-level drizzle-wrapper pattern. A single pgHasErrorCode(cause, code) helper would remove duplication. Very low priority.
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:51current
- 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: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