feat/one-api
needs attentionviewing older commita7b2a9a · incrementalPR #326reviewed 2026-07-21 18:49 UTC0H · 9M · 12L · 4I- 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
- 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+1001−89.claude/rules+612−10scripts/one-api+6036−0packages/api+266−46domains/core+26−10packages/shared-kernel+84−22
- 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.
Findings · 24
correctness3
'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.
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).
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
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.
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.
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.
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
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.
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.
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.
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.
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.
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
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).
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.
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.
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.
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
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']}.
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.
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.
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.
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.
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
- 82bb5b9blockedincremental5H · 5M · 4L2026-08-12 01:48
- 90aa3d5needs attentionincremental1H · 5M · 3L2026-08-11 19:37
- 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:49current
- 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