← all branches

feat/one-api

needs attentionviewing older commit
90aa3d5 · incrementalpre-PRreviewed 2026-08-11 19:37 UTC1H · 5M · 3L · 6I
The branch
Purpose
Consolidate the Batu public API to a single canonical surface (one-api.md), remove the entity_relationships access path (BAT-301), harden the security model, and put canonical-form enforcement on a mechanical footing.
Goal
Production-ready public API v1 with complete security hardening: rate-limit bypass closed, telemetry reconcilable, IDOR harness covers all routes, shells never throw on Result paths, CI guards mechanically enforced.
Sub-goals
  • SG-1: Remove entity_relationships from the contract access path everywhere (BAT-301)
  • SG-2: Harden rate limiting — per-IP ceiling over bearer-rotating callers
  • SG-3: Wire durable telemetry for all token-mint paths (bk_ + sk_)
  • SG-4: Make the IDOR harness cover nested routers and denied probes correctly
  • SG-5: Fix shells to map 23505 at the transaction boundary (never throw on Result paths)
  • SG-6: Rename decideCreateService, extract command type to decisions layer
  • SG-7: Add CI gates (migration-journal-parity, wizard-tx-failure) to the non-DB suite
The changes (whole branch)
What
Per-IP ceiling added to withRateLimit; recordMint wired for sk_ and bk_ exchanges; FCIS allowlist documents two named sanctioned exceptions; IDOR harness made recursive with size floor; profile.shells 23505 catch moved to transaction boundary; decideCreateService renamed; entity_relationships steps removed from repair-dup-contracts; oauth/token route deduplicates clientIp; two CI tests added; rehearse-upgrade.sh now asserts data survival.
Why
Round 7 closes the two round-6 highs (ceiling bypass + mint telemetry gap) and hardens the guards that CI actually runs.
Areas
apps/platform+47304150packages/database+213087352domains/core+14293342packages/api+6831384domains/utility+244342.claude/rules+65837domains/cross-domain+721300.github/workflows+2128scripts+614370
Blast
266 files, +223k/−11k across the branch (includes generated DB snapshots); round-7 increment: 30 files, +445/−151. Touch points: public API mount, token-exchange routes, profile/api-key shells, IDOR harness, CI workflow, rate-limit middleware.
entity_relationships table dropped (BAT-301) — PR #389 accepted exception to expand-migrate-contract rule (zero live rows/users, documented) createServiceKeyShell has no live production caller yet — BAT-294 (EventBridge Connection OAuth) is the flip that re-adds it
CI· No PR open — branch is pre-PR; CI signals not availableCodeRabbit· No .coderabbit.yaml in repo

Findings · 16

correctness2

info

Anonymous reads: IP ceiling and per-credential bucket both track same key — harmless

apps/platform/src/app/api/v1/[...ts-rest]/route.ts:110

For unauthenticated reads (no bearer), key === ipKey. Both checkRateLimit calls run the same string in different namespaces. Per-credential budget (200/15min) is always the binding constraint.

info

contractKeys() dot-notation vs probes() camelCase — latent, self-revealing

apps/platform/src/__tests__/integration/public-v1-idor.test.ts:551

Walk returns 'bills.list'; probes() uses 'billsList'. Flat contract today = identical keys. Nesting would cause a noisy test failure — self-revealing, not silent.

security5

medium

Dual auth rate-limit namespaces allow ~20 token mints/minute

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

The /auth/oauth/token standalone route and the [...ts-rest] mount use different Redis key namespaces. An attacker gets 10/min from each independently. Verify whether both paths are live simultaneously and whether their quotas should share a bucket.

medium

clientIp trusts x-real-ip unconditionally — bypassable off Vercel

apps/platform/src/api/utils/public-v1-rate-limit-bucket.ts:16

clientIp() checks x-real-ip first; both headers are client-controllable unless the edge proxy strips and re-sets them. Safe on Vercel (edge sets x-real-ip) but should be documented as a deployment invariant.

low

ip:unknown bucket collapse — header-stripped clients share one ceiling slot

apps/platform/src/api/utils/public-v1-rate-limit-bucket.ts:16

When both x-real-ip and x-forwarded-for are absent, all traffic keys to ip:unknown. Unlikely on Vercel but could cause false 429s for legitimate clients behind proxies that strip headers.

info

1h JWT revocation window — accepted risk, runbook adequate

.claude/rules/one-api.md:661

sk_ revocation stops new mints but outstanding JWTs remain valid 1h. Runbook documents mitigations (wait TTL, rotate signing key). Accepted architectural tradeoff for zero-DB hot path.

info

Telemetry records publicIds only — no credential or PII leakage

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

recordApiRequest logs route, actor type, public principal id, public org id, status, and duration. No request/response bodies, no key material.

conventions1

info

CreateServiceKeyCommand correctly moved to decisions; naming follows conventions

domains/core/src/api-key/api-key.decisions.ts:440

Command types belong with the decision function. decideCreateService -> decideCreateServiceKey follows the decideXxx convention. Barrel exports correctly reorganized.

tests5

high

withRateLimit per-IP ceiling-exceeded path has no test

apps/platform/src/app/api/v1/[...ts-rest]/route.ts:109

The new per-IP ceiling branch (checkRateLimit on ipKey returning 429) is entirely untested. This is the main behavioral addition of round 7 and can be silently broken by any refactor without any test catching it.

medium

auth-kind bucket: ipKey not asserted equal to key

apps/platform/src/api/utils/__tests__/public-v1-rate-limit-bucket.test.ts:36

The auth-kind test asserts key value but never asserts ipKey === key. A change to the auth branch that separates these two fields would silently break the ceiling without any test catching it.

medium

auth-token handler: recordApiRequest not-called never asserted on failure paths

apps/platform/src/api/handlers/public-v1/__tests__/auth-token.service.test.ts:125

Failure-path tests assert signBatuJwt not called but none assert recordApiRequest not called. A regression moving recordMint above the auth gate would go undetected.

medium

uniqueConstraintName 23505 discriminator has no unit test

domains/core/src/profile/profile.shells.ts:193

No integration test simulates a 23505 on either the username or authId constraint. A typo in the constraint-name check or a Postgres rename would silently route the wrong error type.

low

contractKeys recursive walk has no standalone unit test

apps/platform/src/__tests__/integration/public-v1-idor.test.ts:551

The floor assertion (>=45) is the only guard. A walk bug at a specific nesting depth could still collect >=45 routes from flat entries while missing nested ones.

improvement3

low

Four divergent 23505-handler patterns — no shared utility

domains/core/src/profile/profile.shells.ts:193

profile.shells.ts, utility-contract.shells.ts, credit-metering.shells.ts, and contract-wizard.shells.ts each have their own isUniqueViolation / constraint-name extraction. The credit-metering variant is most complete (checks .constraint too). These should converge on one exported helper.

info

Stale entity_relationships references in utility-contracts schema file

packages/database/src/schema/utility-contracts.ts:11

Comments still reference entity_relationships as the access path. The table was dropped (BAT-301). No runtime risk but will mislead future readers.

info

rateLimitBucket auth case: key === ipKey intentional but undocumented

apps/platform/src/api/utils/public-v1-rate-limit-bucket.ts:53

A one-line comment would clarify that key and ipKey collapse for auth kind by design (bearer is attacker-controlled).

History · 47 commits

  1. 82bb5b9blockedincremental5H · 5M · 4L2026-08-12 01:48
  2. 90aa3d5needs attentionincremental1H · 5M · 3L2026-08-11 19:37current
  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: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