feat/one-api
needs attentionviewing older commit8338a9a · incrementalpre-PRreviewed 2026-08-04 00:33 UTC5H · 14M · 14L · 4I- Purpose
- feat/one-api establishes a unified public-v1 API surface with machine caller isolation (service-key/API-key auth unified under one table, kind discriminator), per-request telemetry, and row-level security for the full read path — enabling multi-tenant safe machine-to-machine access to the Batu platform API.
- Goal
- Bring the public API and internal mounts to a state where machine callers (service tokens and API keys) get database-enforced tenant isolation via RLS, with reads migrated one capability at a time after removing the blockers. This commit removes the entity_relationships access path — the blocker that made bill/contract reads impossible to run under RLS without silently returning fewer rows.
- Sub-goals
- SG-1a: platform-scoped service actor — JWT, AuthContext, gate
- SG-1b: ServiceKey entity + service_keys table
- SG-2: mint service tokens from /v1/auth/token
- SG-3: internal mount dual-accepts service tokens (W2 expand)
- Seal webhook signing secret at rest
- Measure public API — one telemetry record per request
- Persist request telemetry so the baseline survives
- Machine callers get database-enforced tenant isolation (RLS)
- Run public reads under row-level security
- Run asset-management reads under row-level security
- Delete entity_relationships access path (this commit) — enables bill/contract reads under RLS
- What
- Removes the entity_relationships secondary access path from 4 call sites: accessible-contracts.ts (public-v1 SSOT), bills.handler.ts (getAccessibleContracts), utility-contracts.handler.ts (contractFacetsHandler), and csv/route.ts (bill export). Updates docs (.claude/rules/rls-checklist.md, domains/utility/CLAUDE.md, auth-implementation/SKILL.md, cfe-portfolio-diagnostic/SKILL.md) to mark the SG-19 gap as CLOSED and document that the entity_relationships path reached zero live contracts in production.
- Why
- Migration 0036_rescope_stage_a.sql rescoped the RLS policies for bills/bill_files/utility_contracts to resolve tenancy through site_utility_contracts → sites.org_id only — the application filter was still UNIONing a second entity_relationships path the policies never covered. This made the app filter BROADER than the database policy, which is what caused the RLS wrapping attempts to silently return fewer rows. Production audit: 10,847 of 10,908 contracts resolve via sites; the 2 live contracts only reachable via entity_relationships were failed onboarding attempts and were soft-deleted. PATH 2 then reached zero live contracts and was removed.
- Areas
- apps/platform+1672−65domains/core+1365−139packages/database+68334−11packages/api+651−84.claude/rules+315−9docs/security+318−0docs/design+191−0domains/utility+46−10.github/workflows+90−5
- Blast
- 94 files across apps/platform, domains/core, packages/api, packages/database (migrations + RLS), .claude/rules (updated), docs/security. The incremental commit touches 9 files: 4 handlers/utils for the PATH 2 removal and 5 doc/rule files. +361 lines of meaningful code (excluding 68k migration adds) and -161.
Findings · 37
correctness5
contract-access.ts still consults entity_relationships for per-contract mutations
apps/platform/src/api/utils/contract-access.ts:70
resolveContractAccess() still calls hasContractRelationshipForOrgs. A user cannot list a PATH-2-only contract but CAN reach it via a direct publicId mutation probe. Zero live contracts today, but the path is live.
checkContractExistsHandler wizard probe still uses entity_relationships
apps/platform/src/api/handlers/utility-contracts.handler.ts:620
A user would be told 'Este RPU ya está registrado en tu organización' for an RPU they cannot read via list/bill endpoints, causing a confusing dead-end in the onboarding wizard.
Stale comment at bills.handler.ts:264 says showAll=true fetches PATH 2 contracts
apps/platform/src/api/handlers/bills.handler.ts:264
Downstream comment not updated with the PATH 2 removal 12 lines earlier.
Multiple SG-19 comments in bills.handler.ts describe closed gap as still open
apps/platform/src/api/handlers/bills.handler.ts:177
Lines 177-182, 204-208, 340, 447 reference the old entity_relationships rationale.
getAccessibleContractPublicIds @returns JSDoc still mentions PATH 2
packages/api/src/auth/accessible-contracts.ts:46
@returns tag says 'across PATH 1 + PATH 2' — PATH 2 is gone.
security6
Stale showAll contract comment: still documented as enabling PATH 2
apps/platform/src/api/contracts/bills.contract.ts:199
showAll documented as 'include PATH 2 (entity_relationship) contracts'. A developer could re-introduce entity_relationships access under showAll to 'restore the documented behavior'.
contract-access.ts entity_relationships fallback open for per-contract mutations
apps/platform/src/api/utils/contract-access.ts:70
User cannot SEE a PATH-2-only contract in any list, but CAN reach it via a direct publicId probe through mutation surface. Moot with zero live contracts today.
Stale doc comment in utility-contract.queries.ts references PATH 2 as active
domains/utility/src/utility-contract/utility-contract.queries.ts:563
findPublicDetailsByPublicIds comment mentions 'PATH 2 entity_relationships contracts' as expected input.
Service-role bypass documented and intentional — no exploit path
packages/api/src/auth/accessible-contracts.ts
orgId derived from JWT membership in all callers. No exploit path given current derivation.
2 soft-deleted contracts: accessible via direct publicId if bills exist
apps/platform/src/api/utils/public-v1-bill-read.ts:342
findByPublicId applies isNull(deletedAt). Both contracts had zero bills so moot in practice.
Tenant isolation chain correctly scoped in all 4 changed call sites
packages/api/src/auth/accessible-contracts.ts
PATH 1 chain: findByOrgIds scopes by org_id; listContractPublicIdsBySiteIds narrows to site IDs. No cross-org leakage found.
conventions12
Stale @returns JSDoc in accessible-contracts.ts still mentions entity_relationships and PATH 2
packages/api/src/auth/accessible-contracts.ts:46
@returns says 'across PATH 1 + PATH 2. Returns [] when the org owns no sites and has no entity_relationships rows'. Both are wrong after this commit.
Stale SG-19 comments in bills.handler.ts describe the closed gap as still open
apps/platform/src/api/handlers/bills.handler.ts:177
Lines 177-182 say 'wrapping in createRLSDb returns empty results until the pipeline is fixed'. That rationale is now CLOSED per rls-checklist.md. Same stale text at lines 204, 208, 340-343, 446-449.
Orphaned + stale JSDoc blocks before contractFacetsHandler in utility-contracts.handler.ts
apps/platform/src/api/handlers/utility-contracts.handler.ts:130
Two consecutive JSDoc blocks before contractFacetsHandler: one orphaned from listContractsHandler, one for facets that still says 'Merges PATH 1 and PATH 2'. Both need cleanup.
Multiple stale SG-19 comments in utility-contracts.handler.ts describe the closed gap as open
apps/platform/src/api/handlers/utility-contracts.handler.ts:171
Lines 171-174, 590-591, 926-928 describe the entity_relationships gap as open. The chapters handler comment especially misleads — explaining why it cannot wrap in rlsDb with the now-closed rationale.
Stale showAll comment in listBillsHandler says 'also fetch PATH 2 contracts'
apps/platform/src/api/handlers/bills.handler.ts:264
Comment directly contradicts the code — showAll no longer triggers PATH 2.
enableMonitoringHandler JSDoc still describes PATH 2 contracts as the handler's purpose
apps/platform/src/api/handlers/utility-contracts.handler.ts:753
Says 'Enables monitoring for a contract that doesn't have a site yet (PATH 2 contracts)' and 'Removes the direct org → contract entity_relationship'. Both stale.
Dead parameter includeAllPaths in csv/route.ts getAccessibleContracts
apps/platform/src/app/api/bill-export/csv/route.ts:159
includeAllPaths is accepted but silently ignored. Caller passes body.showAll ?? false into a dead param.
Dead ContextFCIS import in bills.handler.ts and csv/route.ts
apps/platform/src/api/handlers/bills.handler.ts:15
ContextFCIS imported but no longer used in bills.handler.ts and csv/route.ts after PATH 2 removal. Will produce lint/build warnings.
Double blank lines in 3 files after PATH 2 deletion
packages/api/src/auth/accessible-contracts.ts:77
Whitespace scar from removing PATH 2 block — 3 occurrences in accessible-contracts.ts, bills.handler.ts, csv/route.ts.
accessType: 'bart' comment in createBillHandler is stale terminology
apps/platform/src/api/handlers/bills.handler.ts:137
'BART' was the legacy name for the entity_relationships path.
RLS checklist 'Contract-derived' predicate description is accurate
.claude/rules/rls-checklist.md:63
Informational — no change needed.
Large comment block in accessible-contracts.ts appropriate per the WHY rule
packages/api/src/auth/accessible-contracts.ts:54
Security-relevant audit trail. Length is justified.
tests7
CSV export: includeAllPaths accepted but silently ignored — no test
apps/platform/src/app/api/bill-export/csv/route.ts:159
showAll=true is passed to a dead param that no longer does anything. A caller expecting wider contract access gets silently narrower results. No test covers this route at all.
contractFacetsHandler: no test verifying entity_relationships-only contracts are excluded
apps/platform/src/api/handlers/utility-contracts.handler.ts:148
No integration test seeds a PATH-2-only contract and asserts absence from the facets response.
showAll=true behavioral change in bills.handler.ts is untested
apps/platform/src/api/handlers/bills.handler.ts:261
showAll previously triggered PATH 2; now only controls isMonitored default filter. No test verifies it no longer expands the contract set.
listContractPublicIdsBySiteIds does not filter soft-deleted contracts — no test
domains/utility/src/site-utility-contract/site-utility-contract.queries.ts:218
Query joins siteUtilityContracts → utilityContracts with no isNull(deletedAt) predicate. No test seeds a soft-deleted site-linked contract and asserts exclusion.
No test asserts bills/utility_contracts reads work correctly under RLS
apps/platform/src/__tests__/integration/rls-machine-claim.test.ts
Commit message says 'reads can now run under RLS' but no test validates the positive path. Prior attempt 976c2a90 failed silently by returning empty rows.
Positive read-isolation test correctly present in IDOR harness
apps/platform/src/__tests__/integration/public-v1-idor.test.ts:537
Test at lines 537-543 directly calls getAccessibleContractPublicIds for both orgs, asserting cross-org isolation. Good coverage for the primary regression risk.
Stale inline comment at bills.handler.ts:264 describes removed PATH 2
apps/platform/src/api/handlers/bills.handler.ts:264
Correctness risk for future maintainers, not a test gap.
improvement7
Dead parameter includeAllPaths in csv/route.ts getAccessibleContracts
apps/platform/src/app/api/bill-export/csv/route.ts:159
Should be removed from signature and call site.
Stale JSDoc @returns on getAccessibleContractPublicIds still mentions PATH 2
packages/api/src/auth/accessible-contracts.ts:46
Module-level comment was updated but function-level JSDoc was not.
Multiple admin-required: SG-19 comments give wrong rationale in bills.handler.ts
apps/platform/src/api/handlers/bills.handler.ts:177
Lines 177-182, 204-208, 338-342 reference old entity_relationships rationale.
Dead import: ContextFCIS unused in bills.handler.ts and csv/route.ts
apps/platform/src/api/handlers/bills.handler.ts:15
Will produce lint/build warnings.
Leftover double blank line in utility-contracts.handler.ts
apps/platform/src/api/handlers/utility-contracts.handler.ts:179
After removing entityContracts PATH 2 block.
Three divergent getAccessibleContracts implementations — consolidation opportunity
packages/api/src/auth/accessible-contracts.ts:50
All three now do the same PATH 1 core. Module comment correctly notes they're not unified. A shared helper would reduce duplication.
Comment verbosity in accessible-contracts.ts function body appropriate given stakes
packages/api/src/auth/accessible-contracts.ts:54
10-line production-stats comment explaining WHY PATH 2 was removed. Security-relevant audit trail. Length justified.
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: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:33current
- 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