← all branches

feat/one-api

needs attentionviewing older commit
62ec3f7 · incrementalpre-PRreviewed 2026-08-10 22:51 UTC2H · 5M · 6L · 6I
The branch
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
The changes (whole branch)
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+42354027domains/core+13823342packages/database+917351packages/api+6811380domains/cross-domain+570299domains/utility+182326.claude/rules+59233scripts+657303docs+5140domains/metrics+2713
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.
security-hardening rls-fixes webhook-removal round-5
CI· no CI status available (no PR open)CodeRabbit· no .coderabbit.yaml in repo

Findings · 19

correctness2

high

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.

low

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

info

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.

info

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.

info

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

low

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.

info

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

high

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.

medium

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.

medium

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.

medium

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.

medium

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.

low

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.

low

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.

info

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

medium

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.

low

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.

low

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>'.

info

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

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