feat/one-api
needs attentionviewing older commit90aa3d5 · incrementalpre-PRreviewed 2026-08-11 19:37 UTC1H · 5M · 3L · 6I- 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
- 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+4730−4150packages/database+213087−352domains/core+1429−3342packages/api+683−1384domains/utility+244−342.claude/rules+658−37domains/cross-domain+721−300.github/workflows+212−8scripts+614−370
- 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.
Findings · 16
correctness2
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.
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
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.
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.
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.
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.
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
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
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.
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.
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.
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.
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
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.
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.
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
- 82bb5b9blockedincremental5H · 5M · 4L2026-08-12 01:48
- 90aa3d5needs attentionincremental1H · 5M · 3L2026-08-11 19:37current
- 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:15
- 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