← all branches

feat/one-api

needs attentionviewing older commit
b843d8a · incrementalpre-PRreviewed 2026-08-09 04:05 UTC1H · 7M · 9L · 5I
The branch
Purpose
Consolidate Batu's fragmented API surface into one canonical form — one ts-rest contract tree, one auth gate, one validator layer, one telemetry seam — with every hop's ownership mechanically enforced.
Goal
Ship a clean public-v1 surface (one-api.md compliance) with FCIS import boundaries gated in CI, entity_relationships dead code dropped, and the EventBridge Connection OAuth endpoint to route Step Functions calls through the internal mount.
Sub-goals
  • SG-1: Drop entity_relationships and Contexts (BAT-301) — tables dropped, 10 routes removed, ~3840 lines deleted
  • SG-2: FCIS import boundary gate (check-fcis-boundaries.mjs) added to CI with 16-shell shrink-only ratchet
  • SG-3: OAuth token endpoint (BAT-294) — POST /api/v1/auth/oauth/token for EventBridge Connection, service keys only, shared verification core
  • SG-4: Tinybird telemetry sink (BAT-297) — third sink via after(), new api_request_events datasource, EXPAND phase
  • SG-5: Canonical request-path table written into canonical-form.md — hop ownership codified
  • SG-6: RLS fixes — transient sealing-key failure not cached, integration test wiring for previously-dead CI gates
The changes (whole branch)
What
Branch adds OAuth endpoint + Tinybird sink (SG-3/4), drops Contexts + entity_relationships (SG-1), adds FCIS boundary CI gate (SG-2), fixes sealing-key transient failure caching (SG-6), codifies the canonical request path (SG-5). This incremental window (ee2007aa + 0a5cd3fa) contributes SG-2, SG-3, SG-4, and part of SG-5.
Why
The one-api consolidation requires the internal mount to be the sole entry point for Step Functions callers. EventBridge Connections need their own OAuth minting surface (BAT-294), and every layer from import boundaries to telemetry must align with the canonical form.
Areas
.claude/rules+58828.github/workflows+2118apps/platform/src/__tests__+12981559apps/platform/src/api+9521578apps/platform/src/app/api+27726apps/platform/src/lib/secrets+21011domains/core+24847domains/cross-domain+9512domains/utility+6118infra/tinybird+450packages/api+91104packages/database+33835+1 more
Blast
182 files, +9250/-7403 (net -2153 — mostly deletion of Contexts/entity_relationships). Touches the full API stack: contracts, handlers, shells, queries, CI gates, domain rules, DB migrations, and infra/tinybird.
New unauthenticated HTTP surface (oauth/token) needs rate limit before prod Tinybird token scope widened — APPEND added to existing metrics_runtime read token
CI/GitHub checks· No open PR — CI status not available for pre-PR branchCodeRabbit· No .coderabbit.yaml in repo

Findings · 25

correctness5

medium

Tinybird occurred_at captured post-response, not at request time

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

shipToTinybird schedules the append via after(), which runs after the response is sent. occurred_at is then captured as new Date().toISOString() inside the callback — potentially milliseconds to seconds after the actual request. The durationMs in the same record was captured at request time, so occurred_at and durationMs will disagree. Fix: capture occurred_at before calling shipToTinybird and pass it as a field on the record.

low

Duplicate client_secret in form body: last value wins silently

apps/platform/src/app/api/v1/auth/oauth/token/route.ts

Object.fromEntries(new URLSearchParams(text)) keeps only the last value for duplicate keys. Per RFC 6749 §3.3, duplicate parameters MUST be treated as malformed. Consider detecting duplicates and returning invalid_request (400). URLSearchParams.getAll(key).length > 1 is the check.

low

oauth/token fire-and-forget catch silently swallows all errors

apps/platform/src/app/api/v1/auth/oauth/token/route.ts

apiKeyQueries.updateLastUsedAt(...).catch(() => {}) discards all failures including connection errors. The parallel call in auth-token.handler.ts logs the error with the apiKeyPublicId for diagnostic traceability. The oauth/token handler should match that pattern.

info

DUMMY_HASH module-level Promise is correct

apps/platform/src/api/handlers/public-v1/api-key-verify.ts

hashSecret() returns Promise<string>. Awaiting a settled Promise multiple times in JavaScript correctly returns the same value — no correctness issue. The scrypt work is done once at module init.

info

Sealing key null+non-transient correctly cached

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

When readFromSecretsManager returns { key: null, transient: false } and SEALING_KEY env is empty, decodeKey('') returns null (0-byte Buffer fails the 32-byte check). Caching proceeds correctly — null returned on every subsequent call for an unconfigured environment.

security5

high

No rate limiting on OAuth token endpoint

apps/platform/src/app/api/v1/auth/oauth/token/route.ts

The standalone Next.js route at /api/v1/auth/oauth/token is outside the ts-rest mount and does not inherit its IP-based rate limiting. An unauthenticated attacker can send unlimited POST requests to brute-force service key prefixes. Even with scrypt cost, at scale this is a sustained CPU/DB amplification vector. A middleware-level or edge-level rate limit is needed before this endpoint is reachable in production.

medium

Tinybird metrics_runtime token now grants APPEND to api_request_events

infra/tinybird/datasources/api_request_events.datasource

The existing metrics_runtime token was scoped to read operations on metrics datasources. Wiring the telemetry sink through the same token means a token leak now grants APPEND access to api_request_events — recording route, actor, org_public_id, principal, and outcome for every API call. An attacker could inject fabricated records, corrupting analytics and obscuring real usage anomalies. A dedicated write-only APPEND token should be used.

medium

Timing oracle: bk_ fast-rejection leaks key-type information

apps/platform/src/app/api/v1/auth/oauth/token/route.ts

SERVICE_KEY_SECRET_FORMAT.test(clientSecret) rejects customer keys (bk_) immediately before the DB lookup and scrypt verify that run for sk_ keys, creating a measurable timing difference. The endpoint is service-only and undocumented which reduces risk, but routing bk_ keys through verifyPresentedApiKey (or burning a dummy verify before returning invalid_client for format mismatches) would eliminate the oracle.

low

DB error messages in api-key-verify.ts may leak schema details in logs

apps/platform/src/api/handlers/public-v1/api-key-verify.ts

console.error logs e.message directly. PostgreSQL error messages can include column names, constraint names, or partial query fragments. Consider filtering to error code only (e.g. pgError.code) rather than the full message string if logs are forwarded to a third-party aggregator.

info

Fire-and-forget updateLastUsedAt using service-role DB is appropriate

apps/platform/src/app/api/v1/auth/oauth/token/route.ts

Service-role DB is appropriate for a last-used-at update keyed by the verified internal matched.id (not user input). Fire-and-forget is appropriate — failure should not block token issuance.

conventions4

medium

OAuth route telemetry gap undocumented — indistinguishable from accidental miss

apps/platform/src/app/api/v1/auth/oauth/token/route.ts

The route bypasses the ts-rest mount, so recordApiRequest is never invoked. The exemption is intentional but undocumented in the route file itself. Without a comment explaining why it is exempt, the next reviewer cannot distinguish intentional from accidental. Add an inline comment or instrument a manual recordApiRequest call.

low

H1 grey-area: api-key-verify.ts imports @batu/database (client) in handler layer

apps/platform/src/api/handlers/public-v1/api-key-verify.ts

H1 bans drizzle-orm and @batu/database/schema in handlers. This file imports @batu/database (the client module) — not a literal violation, but the same class of infrastructure coupling. Consider passing db as a parameter or owning it in a query layer.

low

429 forward-declaration in auth.contract.ts needs declared-status guard verified

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

Declaring 429 before the rate-limiter is wired is acceptable per canonical form. But if the handler never returns 429, declared-status.guard.test.ts may flag it as unreachable drift. Confirm the guard test is updated or suppressed.

info

D5 rule in check-fcis-boundaries.mjs correctly aligned with canonical form

scripts/check-fcis-boundaries.mjs

The script bans any @batu/database import except @batu/database/rls in shells, correctly implementing the canonical form's fifth sanctioned exception.

tests5

medium

Timing-equalization logic in api-key-verify.ts has no dedicated test

apps/platform/src/api/handlers/public-v1/api-key-verify.ts

The security-critical timing-equalization path (burning a dummy scrypt verify on zero-candidate lookups) is only tested transitively through the OAuth route test, which mocks verifySecret to return a plain boolean. The property that verifySecret is called exactly once on the zero-candidate path is untested. The multi-candidate iteration is also only tested with a single-element mock.

medium

DUMMY_HASH mock hoisting dependency is load-bearing but undocumented

apps/platform/src/app/api/v1/auth/oauth/token/__tests__/route.test.ts

The test mocks hashSecret: async () => 'scrypt:dummy:dummy' inside vi.mock. In api-key-verify.ts, DUMMY_HASH = hashSecret(...) is evaluated at module load. Vitest hoists vi.mock before imports so this works today, but the load-order dependency is unexplained. The sealing-key test explicitly documents its vi.resetModules() rationale; the same discipline is warranted here.

low

String-search boundary in after()-callback test is anchor-fragile

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

The test slices source between 'after(async' and '// Not in a request scope'. If a second after(async block is added above the Tinybird one, start points at the wrong block and the assertion would miss any @batu/database import in the actual Tinybird callback. Fragility worth noting — works today given the file is short.

low

event-publisher.ts singleton has no test and no cache-reset seam

apps/platform/src/api/utils/event-publisher.ts

The module-level publisher singleton lacks a reset seam. Unlike sealing-key.ts which exports __resetSealingKeyCacheForTests(), this module will leak its cached instance across test suites if a future test exercises a handler that calls getImmediatePublisher(). Adding a __resetForTests export proactively matches the sealing-key precedent in this same diff.

info

OAuth route test does not assert updateLastUsedAt called on success

apps/platform/src/app/api/v1/auth/oauth/token/__tests__/route.test.ts

The success test does not verify that updateLastUsedAt was called with the matched key's internal id. Low-priority gap — the route mirrors auth/token which presumably has this assertion.

improvement6

medium

Dual-casing Code/code in describeSecretsError likely dead code for SDK v3

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

In AWS SDK v3, error codes surface as err.name not err.Code (SDK v2 PascalCase). The Code branch appears to be dead code and adds noise. If any SDK v2 compatibility path exists it should be documented; otherwise drop the Code arm.

medium

admin-required security annotation buried inside compound comment

apps/platform/src/app/api/v1/auth/oauth/token/route.ts

The admin-required comment is a CI-checked security gate. Mixing it mid-sentence with the fire-and-forget rationale makes it easy to miss by both CI grep and reviewers. It should be on its own line in the conventional '// admin-required: <reason>' form.

low

readParams silently defaults missing content-type to form-encoded

apps/platform/src/app/api/v1/auth/oauth/token/route.ts

A missing content-type header silently defaults to form-encoded parsing. If the body is actually JSON without a content-type header, readParams returns {} and produces a confusing downstream error. A comment explaining this is the intentional RFC default would reduce future confusion.

low

Timing-equalization security comment removed from point of decision

apps/platform/src/api/handlers/public-v1/api-key-verify.ts

The detailed timing-equalization rationale moved from the DUMMY_HASH declaration to the module docblock. A one-liner cross-reference at the DUMMY_HASH line ('// timing-equalization — see module docblock') would preserve in-place discoverability of the security intent.

low

as any cast in event-publisher.ts should use a narrower escape hatch

apps/platform/src/api/utils/event-publisher.ts

Casting db as any to satisfy createImmediatePublisher's loose config type discards all type safety. A cast to the specific expected type (or a TODO linking to a type-fix issue) is preferable to an open-ended as any.

info

Shared RATCHET_D5 string for 16 allowlist entries is the right call

scripts/check-fcis-boundaries.mjs

Using one canonical string documents the exit path uniformly, prevents per-entry rationale drift, and the shrink-only enforcement is the real guard. No change recommended.

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