← all branches

feat/one-api

needs attentionviewing older commit
a7b2a9a · incrementalPR #326reviewed 2026-07-21 18:49 UTC0H · 9M · 12L · 4I
The branch
Purpose
Establish the One-API program: encode the decision and canonical contract grammar as .claude/ rules, build the W1 groundwork (machine-readable route metadata, meta gate enforcement, IDOR harness) that every later parity wave builds on.
Goal
RouteMeta registry on all 48 public-v1 routes + meta gate enforcement at the public mount + IDOR harness over all 48 routes (BAT-274, BAT-275) + admin gate for global catalog writes (BAT-273). Classification and enforcement only — no route behavior changes.
Sub-goals
  • SG-1: one-api.md rule SSOT + canonical-form.md rewording (already reviewed)
  • SG-2: scripts/one-api route classifier + machine-readable inventory (already reviewed)
  • SG-3: shared-kernel PublicIdPrefix registry + cfj fix (already reviewed)
  • SG-4: ADR distillation + docs/ADRs retirement (already reviewed)
  • SG-N: untrack .branch/ working files (already reviewed)
  • SG-5 (this review): RouteMeta on all 48 routes + withMetaGate enforcement + IDOR harness + admin gate fix
The changes (whole branch)
What
Added RouteMeta typed metadata to all 48 public-v1 route contracts; introduced withMetaGate() middleware that enforces auth kind + scopes at the public mount, stashing AuthContext for handler reuse; added publicV1GatedHandlers export; built 582-line IDOR harness covering all 48 routes with two-org fixture and canary detection; fixed admin gate on global catalog writes (BAT-273); added api-key decisions + schemas.
Why
The One-API rule is RATIFIED and the meta gate is the single enforcement point for visibility/auth/scopes — without it, publicized routes had no structural auth enforcement. The IDOR harness is the standing regression guard for the machine-JWT no-RLS threat model. The admin gate fix closes a BAT-273 gap where catalog write routes were accessible without platform admin.
Areas
apps/platform/src/api+100189.claude/rules+61210scripts/one-api+60360packages/api+26646domains/core+2610packages/shared-kernel+8422
Blast
~60 files, +8385/-13776 cumulative (majority -13k is docs/ADRs retirement from prior commits). This incremental window: 34 files, pure addition of enforcement layer and test harness — no existing handler logic changed.
BAT-276-open-debt session-scope-bypass-documented no-CI-runner-available
typecheck· 52/52 packages green per PR descriptionlint· Green on touched packages per PR descriptionunit-tests· 303/303 incl. 6 new registry-integrity tests per PR descriptionci-runner· No self-hosted runner CI data available in this runcoderabbit· No .coderabbit.yaml in repo

Findings · 24

correctness3

low

'service' RouteAuthKind unreachable in the gate — latent FORBIDDEN_ACTOR

apps/platform/src/api/utils/public-v1-meta-gate.ts:104

Gate maps actorType via `=== 'machine' ? 'machine' : 'session'`. AuthContext.actorType is 'user'|'machine', so 'service' can never be produced. A future service-key route declaring auth:['service'] would always return FORBIDDEN_ACTOR without a gate branch addition.

info

ULID_SYNTH contains Crockford-invalid characters — probes test 400-path not 404-path

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

Contains 'I' and 'O' excluded from Crockford base32. Handlers validate against publicIdRegex → 400. For 'denied' probes, 400 is accepted; security invariant holds. Slightly weaker proof of isolation (400-path, not not-found-404-path).

info

scopesSatisfied(granted, []) trivially passes — intentional by design

packages/api/src/meta/route-meta.ts:79

required.every(...) on [] returns true. Combined with meta.scopes.length > 0 short-circuit, routes with scopes:[] skip all scope enforcement. Explicitly intended — documented in code.

security4

medium

Session actors bypass scope checks — BAT-276 (membership RBAC) not yet built

apps/platform/src/api/utils/public-v1-meta-gate.ts:107

The gate skips scope checks for session actors intentionally ('membership RBAC governs them — BAT-276'), but BAT-276 is not yet built. A session JWT from org-A can call any route with auth:['session','machine'] with no scope enforcement. Real isolation rests entirely on handler-level org-id extraction from auth.memberships[0].orgId.

medium

INFRA_DEPENDENT routes: 500 body content on infra failures not fully ruled out

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

The harness relaxes status assertions for 5 routes and allows ≥500. assertNoLeak still runs against the canary. A production infra 500 containing internal ARNs, bucket names, or stack traces would not be caught — publicApiErrorHandler is the real backstop here.

low

withPublicApiAuth fallback re-verifies raw Bearer outside the gate

packages/api/src/middleware/public-api-auth.ts:76

If req.auth is absent (handler called outside publicV1GatedHandlers), withPublicApiAuth falls back to verifying the raw Bearer token. Dead code in production but no gate-level org/scope check would apply.

info

Machine scope '*' grants unrestricted API access — not documented in key issuance flow

packages/api/src/meta/route-meta.ts:72

A key issued with '*' can call every machine-permissioned route. Key issuance flow should document and ideally require explicit confirmation for wildcard scope grants.

conventions6

medium

New publicized routes skip the beta stability entry tier required by one-api.md

apps/platform/src/api/contracts/public-v1/asset-management.contract.ts:81

one-api.md §6 and the publicization definition-of-done require newly-publicized routes to enter as stability:'beta'. Asset-management routes (provision, makes, metric-types, etc.) are marked stability:'stable' directly. The guard test enforces no stability constraint, so CI won't catch this.

medium

Error code casing: gate/schema use lowercase; one-api.md canonical shows SCREAMING_SNAKE_CASE

apps/platform/src/api/utils/public-v1-meta-gate.ts:42

The gate and PublicApiErrorCodeSchema agree (lowercase). But one-api.md §Failure shapes shows uppercase. The inconsistency is in the rule doc — will mislead API consumers reading the spec.

low

Capability duplicated per-route instead of router-level per canonical form

apps/platform/src/api/contracts/public-v1/asset-management.contract.ts:77

one-api.md §Canonical contract form shows capability set once at the router level. Here capability is repeated in every routeMeta() call across all 46 routes, risking drift between capability and scope prefix.

low

withMetaGate fail-closed returns 401 for missing metadata instead of 500

apps/platform/src/api/utils/public-v1-meta-gate.ts:87

Intentional per comment. However 401 for a programmer error conflates a security signal with an infra fault — clients retry with different credentials rather than treating as an infra problem.

low

Admin gate before validateInput — minor pattern deviation in BAT-273 handlers

apps/platform/src/api/handlers/integrations.handler.ts:80

Canonical pattern is withAuth → validate → shell → map. BAT-273 handlers insert isPlatformAdmin between withAuth and validate. Reasonable optimization (avoids parsing body for unauthorized callers); consistent across all three handler files.

info

api-key.decisions.ts is fully FCIS-compliant

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

All four decision functions are synchronous, side-effect-free, and return Result<Decision, Error>. Naming follows canonical-form.md exactly. Confirmed correct.

tests5

medium

assertNoLeak ID-exclusion could mask 200-response IDOR when org-B IDs appear in probe args

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

assertNoLeak silences org-B ID checks for IDs in probe.args. If a buggy handler returns 200 echoing that exact ID, the ID check is silenced. Canary string is the backstop — works as long as every org-B row has canary-embedded string fields (holds for seeded entities, not for auto-generated ULIDs in response bodies).

medium

filesList over-classified as INFRA_DEPENDENT — S3 presign path unreachable in test fixture

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

filesList presigns only per-file. Org-A has no seeded bills/files so the result is empty and presigning never runs — the handler returns 200 not 500. INFRA_DEPENDENT is never triggered but silently allows a 500 to pass without a leak check. Should be a 'clean' probe expecting <300.

low

Meta gate unit test missing wrong-actor-kind test case

apps/platform/src/api/utils/__tests__/public-v1-meta-gate.test.ts:84

FORBIDDEN_ACTOR branch (line 105 of meta-gate) is never exercised — all 48 routes allow both machine and session. A one-line contract fixture addition (auth:['machine'] only) + session call would close this gap.

low

RouteMetaSchema unit test covers only one valid shape — other enum variants untested at unit level

packages/api/src/__tests__/meta/route-meta.test.ts:50

No coverage for stability 'beta'/'app-fast'/'coordinated', visibility 'app'/'infra', or auth kind 'service'. Covered implicitly by the guard test on 48 live routes, not at unit level.

low

SYNTH_RPU '990000000001' not asserted absent — parallel CI run collision risk

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

If two integration runs execute concurrently against the same preview DB, one run's jobsCreate could create this RPU before the other run's probe fires. Low risk on branch-isolated DBs but the sentinel is not documented as reserved.

improvement6

medium

Router-level metadata merging not used — 46 routes repeat visibility/auth/stability boilerplate

apps/platform/src/api/contracts/public-v1/bills.contract.ts:43

one-api.md canonical form shows using c.router's second metadata argument. ts-rest merges this into every route. All 46 authenticated public routes share the same visibility:'public', auth:['machine','session'], stability:'stable' triple inline. Extracting to router-level would cut each routeMeta to just {scopes:['bills:read']}.

medium

INFRA_DEPENDENT set will drift silently as new infra-touching routes are added

apps/platform/src/api/utils/public-v1-meta-gate.ts:76

Static Set<string> maintained by hand. RouteMeta already has optional markers (occ, binary, idempotency); an infraDependent?: true flag would let the harness derive this set from the contract itself.

medium

Gate↔withPublicApiAuth stash is an undocumented implicit protocol

apps/platform/src/api/utils/public-v1-meta-gate.ts:119

Gate writes ctx.request.auth and ctx.nextRequest.auth; withPublicApiAuth reads req.auth and fast-paths. No shared type or constant for the stash key. A refactor of the ts-rest context shape could break the stash silently. Export a shared AUTH_STASH_KEY constant used in both files as a minimum.

low

read/write op-level scope wildcards add grammar surface with no current issuer

packages/api/src/meta/route-meta.ts:71

scopeSatisfied supports *, read (all reads), write (all writes), {cap}:*, {cap}:{op}. No existing or planned key type issues bare 'read'/'write'. Unused vocabulary in a security primitive is a maintenance liability.

low

route-inventory.json is a generated artifact in git with no CI freshness check

scripts/one-api/route-inventory.json:1

The file has a 'do not hand-edit' comment but no CI job verifies it matches a fresh classifier run. Will drift silently as routes are added each wave.

low

withMetaGate type signature erases handler types internally

apps/platform/src/api/utils/public-v1-meta-gate.ts:76

Externally preserves T but internally casts to AnyHandler. Tightening the generic constraint against the contract shape would catch a handler mis-keyed to the wrong route at compile time.

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