← all branches

feat/one-api

needs attentionviewing older commit
3845205 · incrementalpre-PRreviewed 2026-07-29 04:47 UTC3H · 6M · 4L · 2I
The branch
Purpose
Establish a durable, auditable baseline of external API usage before the dashboard converges onto the same surface — making customer traffic and product traffic indistinguishable retrospectively.
Goal
One public API surface with RLS-enforced machine-caller isolation, durable per-request telemetry, sealed credentials at rest, and the test infrastructure to rehearse migrations safely.
Sub-goals
  • W1: unified public-API mount, service-key JWT, route registry
  • W2: webhook secret sealed at rest, public-API telemetry (log + DB sink)
  • W2.5: durable api_request_events table + rehearse-upgrade tooling + machine-caller isolation design
  • W3 (pending): RLS machine isolation migration + staging soak
The changes (whole branch)
What
This commit documents the telemetry persistence work in the access-control narrative (L6 gap partially closed), adds a design proposal for machine-caller RLS isolation via widening get_user_org_ids() additively, and adds migration rehearsal guidance to rules/migrations.md.
Why
The access-control narrative now has evidence that per-request records exist; the machine-caller isolation design is the next prerequisite before the dashboard convergence work can begin safely.
Areas
domains/core+1365139apps/platform+106739packages/api+44743docs/security+3180scripts/db+2530packages/database+20010.claude/rules+1726docs/design+1490.github/workflows+542packages/event-bus+311infra/cdk+111
Blast
73 files across 11 areas, +4067/-241 total. Core domain and platform dominate; docs and tooling are the majority of this incremental commit.
no-pr-yet design-doc-not-implemented
ci· no open PR — CI not triggeredcoderabbit· no .coderabbit.yaml

Findings · 14

correctness2

high

persist() has no observable signal on persistent write failure

apps/platform/src/api/utils/public-v1-telemetry.ts

All errors are intentionally swallowed — the right call for not blocking requests. But a persistent DB write failure (connection pool exhaustion, schema drift after a migration) is completely invisible: no counter, no log line, no metric. The test proved two silent bugs already existed. Consider emitting a single `console.warn(JSON.stringify({ event: 'api.telemetry.write_failed' }))` in the outer catch — no payload, just a detectable signal.

medium

durationMs stored with floor-truncation — integer column, no Math.round()

apps/platform/src/api/utils/public-v1-telemetry.ts

The schema column is `integer`. If `durationMs` is derived from `performance.now()` subtraction, the float is floor-truncated by Postgres — not rounded. Explicit `Math.round(durationMs)` at the call site would make the intent clear and avoid systematic under-reporting by up to ~1 ms per row.

security3

medium

Proposed get_user_org_ids() cast raises exception on malformed claims

docs/design/machine-caller-isolation.md

The proposed function casts `current_setting('request.jwt.claims', true)::json ->> 'batu_org_id')::uuid` without guarding against non-UUID strings. A valid-JSON but non-UUID claim value (e.g. `{"batu_org_id": "not-a-uuid"}`) raises `invalid_text_representation` rather than silently yielding NULL. Wrap in `BEGIN ... EXCEPTION WHEN others THEN RETURN; END` or use `pg_try_cast` equivalent before the migration is written.

info

rehearse-upgrade.sh localhost guard is string-based, not IP-resolved

scripts/db/rehearse-upgrade.sh

The guard checks if POSTGRES_URL contains '127.0.0.1' or 'localhost'. A hostname that resolves to loopback via /etc/hosts would bypass it. For a script that drops the database, consider also requiring `--confirm` as a secondary flag.

info

get_user_admin_org_ids() not-widened boundary should be structurally enforced in future migration

docs/design/machine-caller-isolation.md

The design correctly identifies that the admin helper must not be widened (so machine callers can't read api_keys). When the migration is written, add a regression test that specifically asserts a machine credential cannot SELECT from api_keys — documenting this as a test invariant prevents a future 'cleanup' from silently removing the protection.

tests6

high

Silent skip via `if (!available) return;` passes with 0 assertions

apps/platform/src/__tests__/integration/api-telemetry-persistence.test.ts:65

When the database is unavailable, each test body returns undefined and Vitest marks it PASSED — not SKIPPED. Zero assertions ran, so a broken schema that happens to pass `LIMIT 1` would make all three tests green. Use `it.skipIf(!available)(...)` or call `ctx.skip()` / `vi.skip()` inside the body so the runner surfaces them as SKIPPED.

high

afterAll cleanup misses `_refused` principal row on test failure

apps/platform/src/__tests__/integration/api-telemetry-persistence.test.ts:114

The second test deletes the `${PRINCIPAL}_refused` row inline at the end of its body. If `waitForRow` times out or an assertion fails first, that row is orphaned. Move cleanup into `afterAll` using a LIKE prefix match (`WHERE principal LIKE '${PRINCIPAL}%'`) so it runs unconditionally.

medium

No test for `client_error` outcome branch (4xx non-auth statuses)

apps/platform/src/__tests__/integration/api-telemetry-persistence.test.ts

The suite covers `ok` (status 200) and `refused` (status 401) but not `client_error` (e.g. 400, 422, 404) or `server_error` (5xx). `outcomeOf` has four branches; only two are exercised end-to-end.

medium

No assertion that `null` org round-trips as SQL NULL (not string 'null')

apps/platform/src/__tests__/integration/api-telemetry-persistence.test.ts

The second test passes `org: null` but never asserts `row.orgPublicId === null`. Given the history of the column-name spread bug, confirming the nullable column round-trips correctly is a meaningful regression guard.

medium

waitForRow masks DB connection errors with misleading timeout failure

apps/platform/src/__tests__/integration/api-telemetry-persistence.test.ts

If the DB connection drops mid-poll, the `while` loop swallows the query error and spins for the full 15 s before returning null. CI failures slow and the final error message ('no telemetry row was persisted') is misleading. Let unexpected errors propagate so the failure is immediate and diagnostic.

low

The `after()` production path has no integration coverage — only fallback is tested

apps/platform/src/__tests__/integration/api-telemetry-persistence.test.ts

`next/server` is unavailable in Vitest, so `persist()` always takes the inline fallback path. The `after()` scheduling branch — the production code path — is untested. This is acceptable now but worth an e2e smoke test or a manual confirmation after deploy.

improvement3

medium

Inline comments in persist() describe WHAT, not WHY — violates convention

apps/platform/src/api/utils/public-v1-telemetry.ts

Several inline comments annotate code mechanics (e.g. 'Mapped FIELD BY FIELD, never spread', 'No request context — fall through and write inline', 'Module resolution or connection failure'). The convention is WHY only; WHAT comments are noise to readers who can read the code. The field-mapping risk is better addressed by a compile-time assertion or a narrow type than by a prose warning inside a catch-all sink.

low

byTime solo index is likely redundant given both composites cover time

packages/database/src/schema/api-request-events.ts

Every real query in api-usage.sql filters by org or route in addition to time, so Postgres will prefer `byOrg` or `byRoute` over the solo `byTime` index. The only pure-time scan is the retention DELETE, which can use either composite index by ignoring the leading column. Dropping `byTime` removes write amplification on every insert with no practical read cost.

low

Inline cleanup in second test case is asymmetric with afterAll pattern

apps/platform/src/__tests__/integration/api-telemetry-persistence.test.ts:114

First test case relies on afterAll for cleanup; second test cleans up inline. Consolidating into afterAll with a prefix-LIKE match would be consistent and failure-safe.

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