feat/one-api
needs attentionviewing older commit0c8a7f5 · incrementalpre-PRreviewed 2026-08-04 18:15 UTC0H · 4M · 9L · 2I- 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
- 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+65−14infra/cdk/src/stacks/events+41−22packages/database/drizzle/0066_*+37−1scripts/db/rehearse-upgrade.sh+24−0.claude/rules + domains/utility/CLAUDE.md+30−6packages/api/src/auth + apps/platform/src/api/utils+23−2pnpm-lock.yaml+37−34
- 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.
Findings · 15
correctness2
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.
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
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.
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.
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
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
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.
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.
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.
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.
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.
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
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.
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.
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
- 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:15current
- 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