← all branches

feat/one-api

needs attentionviewing older commit
7a762fa · incrementalpre-PRreviewed 2026-08-05 01:25 UTC2H · 5M · 5L · 9I
The branch
Purpose
feat/one-api is a comprehensive branch that adds a public-facing external API (public v1) to the Batu platform, introduces machine-caller isolation with database-enforced tenant isolation, implements webhook signing secret encryption at rest, adds request telemetry, and migrates RLS policies away from the entity_relationships access path (SG-19 closure).
Goal
Ship a secure, auditable public API layer with proper machine-caller isolation, encrypted secrets at rest, and closed RLS gaps from the entity_relationships access pattern.
Sub-goals
  • SG-1: Public API key authentication and HMAC-signed webhooks
  • SG-2: Seal webhook signing secrets at rest (AES-256-GCM)
  • SG-3: Database-enforced machine-caller tenant isolation (RLS)
  • SG-4: Request telemetry — one record per request at the API mount
  • SG-5: Close SG-19 (entity_relationships access path removed, RLS rescoped to sites.org_id)
  • SG-6: Sealing-key managed via Secrets Manager (not baked into Lambda/env vars)
The changes (whole branch)
What
This incremental commit (7a762fa9) centralises CURRENT_SEALING_KEY_ID into sealed-secret.ts so the sealer and opener cannot drift; flips the sealing-key resolution order so Secrets Manager is primary and SEALING_KEY env var is local-dev-only fallback; and updates stale SG-19 comments across 4 handler files now that migration 0036 closed the entity_relationships gap.
Why
The duplicate 'k1' literal in platform and Lambda was a silent failure mode — a mismatch means open() returns null and every webhook delivery is skipped. The env-var-first order was a security antipattern for a key that opens every customer's webhook secret. The stale comments recorded an already-resolved security gap as if still open.
Areas
domains/core+20050apps/platform/src/api+350200apps/platform/src/lib+5020packages/api/src/auth+1010packages/event-bus+155
Blast
12 files changed in this commit across: platform handlers (bills, bill-files, cfe-jobs, monitoring, utility-contracts, csv export), sealing-key infrastructure (platform + Lambda), sealed-secret core, accessible-contracts utility. No schema changes. No new endpoints.
sealing-key-security sg-19-closed comment-cleanup test-gaps
github-ci· no GitHub auth available in this runnercoderabbit· no .coderabbit.yaml found

Findings · 20

correctness4

medium

Null sealing key permanently cached on SM transient failure at cold start

apps/platform/src/lib/secrets/sealing-key.ts

If Secrets Manager returns null (transient error) AND SEALING_KEY is absent, decodeKey('') → 0-byte buffer → null, cached = null. The process-wide cache means a Lambda that hits a transient SM error on its first invocation will permanently return null until it is killed. The old code shared this flaw, but this commit removes the env-var fast path that could have bypassed SM entirely. Consider not caching negative results so transient errors trigger a retry.

info

showAll is NOT a dead parameter — PATH 2 removal just changed its meaning

apps/platform/src/api/contracts/bills.contract.ts

showAll is still actively read in listBillsHandler, billAggregatesHandler, billFacetsHandler, and CSV export — it controls the isMonitored=true default filter. The contract comment update in this diff correctly reflects this. No dead parameter issue.

info

ContextFCIS removal is clean

apps/platform/src/api/handlers/bills.handler.ts

ContextFCIS is not referenced anywhere in bills.handler.ts or csv/route.ts. Remaining uses in contexts.handler.ts and utility-contracts.handler.ts are for separate capabilities. Clean removal.

info

Deep import @batu/core-domain/lib/sealed-secret is valid

packages/event-bus/src/handlers/webhook-dispatcher.lambda.ts

The sub-path ./lib/sealed-secret is in @batu/core-domain's package.json exports field. The dynamic import resolves correctly at Lambda runtime.

security4

low

No CloudTrail alert on sealing-key Secrets Manager reads

apps/platform/src/lib/secrets/sealing-key.ts

The comment correctly notes that SM gates reads behind IAM and records them in CloudTrail. Worth confirming a CloudTrail-to-alerting pipeline exists for the sealing-key secret ARN specifically, since this key opens every customer's webhook signing secret.

info

SEALING_KEY_ID env var override in Lambda could silently break all webhook deliveries

packages/event-bus/src/handlers/webhook-dispatcher.lambda.ts

The Lambda still reads process.env['SEALING_KEY_ID'] ?? CURRENT_SEALING_KEY_ID. If an operator sets SEALING_KEY_ID to any wrong value, open() returns null and every delivery is silently skipped. Since the constant is now centralized, this override has no legitimate use case in deployed environments — consider removing it or documenting in the CDK stack that it must never be set.

info

SM failure is silently swallowed — no log for operators to distinguish config vs transient error

apps/platform/src/lib/secrets/sealing-key.ts

readFromSecretsManager() catches all exceptions and returns null, including IAM permission errors and throttling. There is no logging in the catch block. An operator cannot distinguish 'SM is not configured' from 'SM errored' without a CloudWatch metric or log line.

info

Resolution order change is strictly more secure — confirmed

apps/platform/src/lib/secrets/sealing-key.ts

Secrets Manager first is confirmed correct: SM gates reads behind IAM and CloudTrail. decodeKey('') safely returns null (0-byte buffer). Fail-closed on both absent is correct behavior for a key that protects every customer's webhook secret. The org isolation chain for service-role bills/contracts reads is sound: ID set built server-side from org → sites → SUC, never from query params.

conventions5

medium

Stale @param profileId in getAccessibleContracts JSDoc

apps/platform/src/api/handlers/bills.handler.ts

The JSDoc still reads `@param profileId - User's profile ID` but the function signature is `getAccessibleContracts(orgId: string)` — it never accepted a profileId. The doc is actively wrong on the parameter name.

low

CURRENT_KEY_ID alias adds indirection with no clarity

apps/platform/src/lib/secrets/sealing-key.ts

const CURRENT_KEY_ID = CURRENT_SEALING_KEY_ID is a local alias used one line below. The comment misdescribes it as a re-export. Using CURRENT_SEALING_KEY_ID directly is cleaner and self-documenting.

low

Eight near-identical SG-19 comment blocks — high repetition, single edit point risk

apps/platform/src/api/handlers/bills.handler.ts

The 5-line SG-19 CLOSED block appears ~8 times across four handler files. The WHY is non-obvious so a comment is warranted, but a one-liner at each site plus a single canonical explanation in domains/utility/CLAUDE.md § RESOLVED would reduce future maintenance burden.

info

accessible-contracts.ts docstring is accurate

packages/api/src/auth/accessible-contracts.ts

The updated @returns correctly describes: one path (sites → SUC); PATH 2 removed with production evidence; service-role is a migration step not a constraint. Accurate and appropriately detailed for a security-adjacent authorization file.

info

showAll contract comment accurately updated

apps/platform/src/api/contracts/bills.contract.ts

The comment was updated from 'include PATH 2 (entity_relationship) contracts and all isMonitored values' to 'drop the default isMonitored=true filter (full history view)'. This matches the actual remaining behavior. Accurate.

tests4

high

Platform sealing-key resolution order is completely untested

apps/platform/src/lib/secrets/sealing-key.ts

__resetSealingKeyCacheForTests is exported but called by no test file. None of the three critical scenarios are verified: (a) SM returns a key → env var is ignored; (b) SM returns null → env var IS used; (c) both absent → returns null (fail-closed). A future refactor could flip the priority order back without any test catching it.

high

showAll / isMonitored filter branching in bill handlers has no tests

apps/platform/src/api/handlers/bills.handler.ts

The showAll branching that gates whether unmonitored contracts appear in results is untested. No test exercises showAll=true or showAll=false on listBillsHandler, billAggregatesHandler, or billFacetsHandler. Behavioral regression here would go undetected.

medium

No round-trip test asserting seal and open share the same CURRENT_SEALING_KEY_ID

domains/core/src/lib/sealed-secret.ts

The constant 'k1' is now the single source of truth, but no test exercises the full cross-boundary path: platform seal() using CURRENT_KEY_ID → Lambda loadSealingKey() resolving the same CURRENT_SEALING_KEY_ID. A mismatch ('k1' vs 'K1') would mean open() returns null and every delivery is silently skipped.

low

Lambda dynamic import of CURRENT_SEALING_KEY_ID not verified against package exports map

packages/event-bus/src/handlers/webhook-dispatcher.lambda.ts

await import('@batu/core-domain/lib/sealed-secret') resolves in Vitest (TypeScript source) but could fail at Lambda runtime if @batu/core-domain's package.json exports map does not expose lib/sealed-secret as an entry point. Worth a quick verify.

improvement3

medium

Stale inline comment at listBillsHandler still references removed PATH 2

apps/platform/src/api/handlers/bills.handler.ts

Line ~262 reads `// When showAll=true, also fetch PATH 2 (entity_relationship) contracts` immediately before the call to `getAccessibleContracts(orgResult.value.orgId)` which no longer touches entity_relationships. showAll still exists (for the isMonitored default), but the PATH 2 side-effect is gone. Should read: `// showAll controls the isMonitored default below — PATH 2 was removed`.

low

decodeKey(process.env.SEALING_KEY ?? '') implicitly relies on zero-byte failure

apps/platform/src/lib/secrets/sealing-key.ts

When SEALING_KEY is unset, '' is passed to decodeKey and relies on Buffer.from('', 'base64') being 0 bytes → failing the length check. Explicit guard: `process.env.SEALING_KEY ? decodeKey(process.env.SEALING_KEY) : null` is clearer and immune to any future relaxation of the length check.

info

Dynamic import in webhook-dispatcher is the established Lambda pattern here

packages/event-bus/src/handlers/webhook-dispatcher.lambda.ts

Consistent with existing await import('@batu/core-domain/webhook-endpoint') and await import('@batu/database') in the same file — this is the declared esbuild bundling pattern. Reason for dynamic vs static import is not documented; a one-line comment would help future readers.

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