← all branches

feat/one-api

needs attentionviewing older commit
0c8a7f5 · incrementalpre-PRreviewed 2026-08-04 18:15 UTC0H · 4M · 9L · 2I
The branch
Purpose
Implement a secure, tenant-isolated public API — machine (API-key) callers get database-enforced RLS, all reads run under the caller's Supabase session, and webhook signing secrets are sealed at rest.
Goal
One authenticated public API surface that routes both user-session (JWT) and machine (bk_ API key) callers through RLS-wrapped database connections, eliminating the service-role bypass for reads.
Sub-goals
  • SG-1: Machine callers get database-enforced tenant isolation (createMachineRLSDb)
  • SG-2: Record which credential internal callers present (internal-auth telemetry)
  • SG-3: Request telemetry — one record per request at the public API mount
  • SG-4: Run asset-management and public reads under row-level security
  • SG-5: Seal webhook signing secrets at rest (AES-256-GCM)
  • SG-6: Fix sealing key exposure in CloudFormation templates + guard purge migration
The changes (whole branch)
What
This incremental commit (0c8a7f59): removes the sealing key from CloudFormation-resolved env vars (unsafeUnwrap) and moves it to Secrets Manager runtime fetch with IAM grant; adds a PL/pgSQL guard to migration 0066 preventing silent deletion of live webhook endpoints; tests the guard in rehearse-upgrade.sh; clarifies documentation that resolveContractAccess stays deliberately broader than the RLS list filters.
Why
Two security/safety issues found by review: (1) unsafeUnwrap() baked the plaintext sealing key into the CFN template and CDK asset, readable via lambda:GetFunctionConfiguration; (2) the bare DELETE migration ran unattended and could destroy live customer webhook configuration with no recovery path.
Areas
packages/event-bus/src/handlers+6514infra/cdk/src/stacks/events+4122packages/database/drizzle/0066_*+371scripts/db/rehearse-upgrade.sh+240.claude/rules + domains/utility/CLAUDE.md+306packages/api/src/auth + apps/platform/src/api/utils+232pnpm-lock.yaml+3734
Blast
11 files in this commit (+257/−79), branch total: 94 files +5,574/-407 (excl. lockfile); touches Lambda infra, a deployed migration, CDK stack, and project rules — medium blast radius.
migration-change infra-change security-fix
typecheck· not run — no CI access in this review modetests· not run — no CI access in this review modecoderabbit· no .coderabbit.yaml in repo

Findings · 15

correctness2

medium

Transient SM error at cold start permanently caches null — all webhook deliveries silently skipped

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

When GetSecretValueCommand throws (transient network blip, IAM not yet propagated after CDK deploy), the catch block logs the error and leaves key null. cachedSealingKey = null is then stored, permanently caching the failure. Every subsequent invocation short-circuits and returns null — all deliveries silently skipped for the container's lifetime with no recovery path other than a cold start. Consider caching null only on decode failures (wrong key length), not on network/SDK exceptions, so the next invocation retries.

low

SEALING_KEY_ID env var not set by CDK — default 'k1' is implicit load-bearing coupling

infra/cdk/src/stacks/events/events.stack.ts:158

The Lambda reads process.env['SEALING_KEY_ID'] ?? 'k1'. This env var is absent from the CDK environment block, silently defaulting to 'k1'. If CURRENT_KEY_ID in the platform ever changes without adding a matching CDK env var, the Lambda's Map would hold the correct key bytes under the wrong ID and all sealed-secret opens would fail.

security3

low

console.error logs raw AWS SDK error — secret name leaks to CloudWatch on every fetch failure

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

AWS SDK errors from SecretsManagerClient include the SecretId in their $metadata. console.error(..., err) logs the full secret path (batu/{env}/public-api/sealing-key) to CloudWatch on any transient failure. Anyone with cloudwatch:GetLogEvents on this function's log group can discover the secret path. Prefer: err instanceof Error ? err.message : String(err), consistent with how delivery failures are logged elsewhere.

low

fromSecretNameV2 grantRead produces wildcard ARN — covers AWSPREVIOUS secret version

infra/cdk/src/stacks/events/events.stack.ts:139

fromSecretNameV2 constructs an ARN with a 6-char random suffix wildcard. grantRead() therefore grants GetSecretValue on all secret versions including AWSPREVIOUS. This is consistent with the codebase pattern and the threat model is sound, but the grant is wider than strictly necessary. fromSecretCompleteArn would pin to the exact ARN but requires an out-of-band lookup at synth time.

info

SEALING_KEY_ID not set by CDK — silent coupling between platform constant and Lambda default

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

Both sides hardcode 'k1' independently. Would silently fail on key ID rotation if CDK is not updated simultaneously.

conventions1

low

Module-level cache lacks test-reset seam — inconsistent with platform sealing-key.ts precedent

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

The platform-side equivalent (apps/platform/src/lib/secrets/sealing-key.ts) exports __resetSealingKeyCacheForTests() to prevent cache bleed across test invocations. The Lambda handler exports no such reset seam. Low urgency now (no tests exist), but the pattern is established and should be followed when tests are added.

tests6

medium

Stage 2a cannot distinguish which migration refused — false-positive risk if any earlier migration fails

scripts/db/rehearse-upgrade.sh:105

Stage 2a runs all pending migrations with output fully suppressed. If any migration before 0066 fails for an unrelated reason, the script reports '✓ refused, as designed' — a false positive that masks a real failure. Should grep stderr for the guard's specific string ('REFUSING to purge') to confirm 0066 specifically was the one that refused.

medium

rehearse-upgrade.sh not wired into any CI pipeline — guard could silently disappear

scripts/db/rehearse-upgrade.sh

The script is LOCAL ONLY and no .github/workflows file references it. The guard it tests (migration 0066 refusing when endpoints exist) protects production from silent data loss, but its correctness is only verified by manual developer invocation. A rebase or edit that removes the DO $$ block would reach production undetected.

medium

loadSealingKey has no unit tests despite critical, non-trivial logic with three failure-prone paths

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

Three paths have no coverage: (1) the .trim() call described as 'load-bearing' to strip trailing newlines that produce a 33-byte buffer failing key-length validation; (2) the JSON-parse vs bare-string fallback; (3) null caching on misconfigured environments. A missing key silently skips all webhook deliveries with no retry. Add vitest unit tests with mocked AWS SDK matching the pattern in eventbridge-client.test.ts.

low

Stage 2a error output fully suppressed — guard's verbose operator message is invisible during rehearsal

scripts/db/rehearse-upgrade.sh:105

>/dev/null 2>&1 discards both stdout and stderr. The RAISE EXCEPTION message (which instructs the operator on recovery steps) is silently eaten. Redirecting only stdout (>/dev/null) and letting stderr through would make the guard's message visible — which is the whole point of making it verbose.

low

Module-level cachedSealingKey prevents test isolation — future tests must use vi.resetModules()

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

Once set (null or a keyring), the cache persists for the lifetime of the module in the test runner. Tests exercising loadSealingKey() across different environment-variable configurations will contaminate each other unless vi.resetModules() is used between cases. Should export a reset seam like the platform sealing-key.ts does.

info

No integration test for webhook dispatcher covering the new Secrets Manager key-fetch path

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

The handler now fetches the key at runtime via loadSealingKey. No existing test exercises handler() end-to-end. Would catch future regressions in the key-loading path before they reach production and silently drop deliveries.

improvement3

low

SecretsManagerClient instantiated inside try block — should be module-level singleton like EventBridgeClient

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

The client is created at most once per container (due to the cache), but its instantiation inside the try block makes the lifecycle implicit. Hoisting it to module scope alongside cachedSealingKey mirrors the EventBridgeClient pattern in this package. Note: the dynamic import must stay inside the function due to externalModules bundling.

low

JSON/bare-string dual parsing: no console.warn on JSON parse failure makes format mismatches invisible

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

The JSON parse failure is silently caught and the bare-string path handles it — intentional but silent from an operator's perspective. A console.warn (not error) when JSON.parse fails would surface format mismatches (operator stored bare base64 instead of JSON) without treating them as fatal.

low

SEALING_KEY_ID implicit coupling — source from secret JSON or set explicitly in CDK

infra/cdk/src/stacks/events/events.stack.ts

Both the Lambda and platform independently hardcode 'k1'. Either (a) CDK should inject SEALING_KEY_ID explicitly alongside SEALING_KEY_SECRET_NAME so the env-var path is actually exercised, or (b) store the key ID inside the secret JSON ({"key":"...","keyId":"k1"}) so the ID and key rotate atomically. Without one of these, the next key rotation is a silent divergence waiting to happen.

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:25
  12. 3300a60needs attentionincremental2H · 4M · 7L2026-08-04 19:06
  13. 0c8a7f5needs attentionincremental0H · 4M · 9L2026-08-04 18:15current
  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