AUTH-BRIDGE-AUDIT-01¶
Arc: First-client-onboarding (head of arc).
Type: Audit — truth-finding, read-only. Ended at truth; Tim's verdicts recorded in §8.
Filed: 2026-05-29 by Alex.
Stance: Read-only — grep/Read on source + read-only MCP d1_database_query (SELECT/count). No substrate mutation, no writes, no deploys.
Anchors against: ADR-024 (canonical user-identity schema), ADR-025 §888 (customer-facing workers route identity via internal-api), ADR-027 (data parity not committed), CANONICAL-IDENTITY-VIA-UI-ONLY, Tim Decisions A/E (IMM-01 Phase 3c).
Related: decisions/IMM-01-Phase-3d-PREFLIGHT-PARK-DECISION-01.md — this audit corroborates the park decision from the auth direction; the gap's C-items are the same parked onboarding foundation.
1. Question¶
Define the target as production-true scoped login: a real person logs in → resolves to a canonical users row → resolves to the client_id (RTO) they act for, at the correct tier → and Studio receives that identity + scope and functions. This audit establishes, from code and substrate as they actually are, how much of that chain exists and where it breaks.
2. Brief-yourself finding — the auth reference is stale and off-target¶
docs/docs/architecture/auth-architecture.md carries a "superseded — do not update" banner (points to docs.ucca.online) and describes the UCCA OAuth ecosystem (mcp.ucca.online OAuth 2.1, rtopacks-mcp/api KV keys, keys.ucca.online) — not the workspace/Studio CF-Access → canonical-identity path. There is no current canonical auth doc for the Studio bridge; the live intended design exists only in ADR-024/025/027 + CANONICAL-IDENTITY-VIA-UI-ONLY + Tim Decisions A/E. (Resolved per §8 D-14 — the stale doc is retired in the same commit that files this audit.)
3. The truth, step by step (file:line)¶
-
Surface topology. Studio =
apps/workspace(app/(workspace)/studio/,app/api/studio/), workerrtopacks-workspace→my.rtopacks.com.au/*(prod) /rtopacks-workspace-dev→my.rtopacks.dev/*(dev) (apps/workspace/wrangler.jsonc:3,44–55). Binds SESSION_KV, OPS_DB, WORKSPACE_DB, INTERNAL_API service binding — no IDENTITY_DB bind (ADR-025 §888 ✓). The 14 identity-read sites from the IMM-01 Phase-3d audit are all apps/site — none are on the Studio path. -
Entry / authentication.
apps/workspace/middleware.ts:61–64gates on presence of thertp_sessioncookie → redirect/authif absent. No CF Access JWT/email check in the worker. CF Access is an edge/zone policy outside worker code;app/api/studio/session/route.ts:6–10confirms internal-api sits behind CF Access at the edge (the service binding bypasses it). Workspace-surface edge-Access coverage is not verifiable in code → §8 D-12. -
Identity resolution.
lib/auth.ts:9–33(getSessionFromKV) andapp/api/studio/auth.ts:31–67(authenticateWorkspace) both resolve identity from thertp_sessioncookie → SESSION_KVsession:{id}→RTPSessionblob. No identity-db / workspace-db / INTERNAL_API call at request time — identity is whatever was baked into KV at verify time. -
Client scope.
RTPSessioncarriesclient_id: string | null+tier(lib/session-types.ts:16–32, canonical per ADR-024). Populated byresolveUserTier(lib/tier-resolution.ts:28–58) →GET /identity/tier→ tier_grants. With 0 grants (prod reality), it returns{ tier: 'T4A', client_id: null }— the "authenticated but un-tiered" default (Tim Decision E, pendingIDENTITY-MANAGEMENT-UI-SPEC-01). -
Studio's requirement.
authenticateWorkspacehands Studio{email, user_id, org_id (= client_id), display_name}. Studio persistsstudio_sessionsto WORKSPACE_DB withorg_id = auth.org_id ?? 'unknown'(app/api/studio/session/route.ts:422) and lists/filters byorg_id(app/api/studio/sessions/route.ts:32–36→WHERE org_id IN ('unknown', LEGACY_SEED_ORG); same key inscope,preferences,proposedroutes). studio-spec demands authoritative RTO scope: the compliance-grade RTO context header — "United Central Colleges of Australia (RTO 45329)" — warning "a misattributed edit to the wrong RTO's qualification is a compliance incident" (docs/docs/workspace/apps/studio.md:132–136), scope "system-known from the moment the RTO is onboarded" (studio.md:78–80). Spec-vs-substrate divergence: the spec requires RTO scope; the substrate writes'unknown'. -
The seam — exists, canonical-routed.
app/auth/verify/route.ts(magic-link verify): magic token →POST /identity/user-upsert(INTERNAL_API,workspace-workersource) → canonicaluser_id→resolveUserTier→ buildRTPSession→ SESSION_KV → cookie. The bridge is magic-link → canonical, not CF-Access → canonical. The identity half is built; the scope half resolves to null.
4. The actual current flow¶
flowchart TD
P([Person]) --> EDGE{"CF Access @ edge<br/>on my.rtopacks.*?<br/>see D-12"}
EDGE --> MW["workspace middleware.ts<br/>checks rtp_session cookie PRESENCE only"]
MW -- no cookie --> AUTH["/auth -> POST /api/auth/magic-link<br/>email w/ token"]
AUTH --> VERIFY
subgraph SEAM["THE SEAM — /auth/verify/route.ts (EXISTS, canonical)"]
VERIFY["magic token -> email"] --> UPSERT["POST /identity/user-upsert<br/>via INTERNAL_API (workspace-worker)"]
UPSERT --> IDB[("identity-db users<br/>canonical user created — UI-only")]
UPSERT --> TIER["resolveUserTier -> GET /identity/tier"]
TIER --> TG[("tier_grants<br/>0 client grants -> T4A, client_id=NULL")]
TIER --> SESS["build RTPSession<br/>{user_id OK, tier=T4A, client_id=NULL}"]
SESS --> KV[("SESSION_KV session:{id}")]
end
KV --> COOKIE["set rtp_session cookie -> redirect /"]
COOKIE --> MW2["authenticated request -> middleware (cookie present)"]
MW2 --> STUDIO["Studio API — authenticateWorkspace reads SESSION_KV<br/>-> {user_id OK, org_id = client_id = NULL}"]
STUDIO --> WRITE[("studio_sessions @ WORKSPACE_DB<br/>org_id = client_id ?? 'unknown'<br/>ALL un-tiered users share 'unknown' bucket")]
STUDIO --> DATA["catalogue reads -> INTERNAL_API service binding<br/>(bypasses CF Access) -> rtopacks-db OK"]
HEADER["studio-spec RTO context header<br/>'RTO 45329' — compliance-grade, no source"] -.requires.-> WRITE
CLIENTS[("clients entity<br/>does not exist anywhere — per 3d pre-flight")] -.would feed.-> TG
5. Component vocabulary (reuse existing names; new names marked ✦)¶
| Component / seam | Canonical name | Where | Status |
|---|---|---|---|
| Edge perimeter gate | CF Access (operational outer auth) | zone policy (edge) | confirmed on internal-api; workspace coverage → D-12 |
| Inner session gate | rtp_session cookie |
middleware.ts, lib/auth.ts |
live |
| Session record | RTPSession (ADR-024) |
lib/session-types.ts → SESSION_KV |
live; carries tier + client_id |
| The identity bridge ✦ | magic-link verify seam | app/auth/verify/route.ts |
live, canonical |
| Canonical user provisioning | /identity/user-upsert (UI-only entry) |
internal-api identity.ts |
live |
| Scope resolver ✦ | resolveUserTier → /identity/tier |
lib/tier-resolution.ts |
live plumbing, empty data |
| Scope grants | tier_grants (ADR-020 T3/T4/T4A) |
identity-db | empty (0 client grants) |
| Client entity | clients |
— | absent (per 3d pre-flight) |
| Studio session reader ✦ | authenticateWorkspace |
app/api/studio/auth.ts |
live; parallel fetcher (dup of getSessionFromKV) → PARALLEL-SESSION-FETCHER-RETIREMENT-01 |
| Session scope key ✦ | studio_sessions.org_id |
WORKSPACE_DB | live; degrades to 'unknown' |
| Identity/data gateway | INTERNAL_API service binding |
all workspace API routes | live |
6. Substrate counts (read-only MCP, prod; full detail in the park-doc)¶
- IDENTITY_DB
rto-identity-db: users 1, tier_grants 1 (0 client-scoped, 0 T4/T4A — the lone grant is the T3 operator), portal_invites 0, noclientstable. - WORKSPACE_DB
engine-db-oc: tenants 1, users 4, user_tenant_roles 1, portal_invites 0, groups 0, user_groups 0. - OPS_DB
ops-db: orgs 2 (UCCA duplicate), org_memberships 1.
7. Gap statement, classified¶
| # | Item | Class | Note |
|---|---|---|---|
| 1 | CF Access edge gate on internal-api | A | works as-is |
| 2 | Magic-link → canonical user upsert (identity half of bridge) | A | verify/route.ts → /identity/user-upsert; ADR-025-compliant, UI-only |
| 3 | Tier/scope resolver plumbing (resolveUserTier → /identity/tier → tier_grants) |
A | wired end-to-end |
| 4 | RTPSession carries tier + client_id; Studio reads it |
A | canonical shape; Studio's contract satisfied structurally |
| 5 | Catalogue reads via INTERNAL_API service binding | A | correctly bypasses Access |
| 6 | client_id scope delivery — field, resolver, session, studio_sessions.org_id all wired |
B→C | plumbing exists (B); the producer of a client-scoped tier_grant does not (C) |
| 7 | Studio session isolation (WHERE org_id IN (...)) |
B | filters exist; collapse to shared 'unknown' until scope populated |
| 8 | clients entity |
C | no table anywhere (per 3d pre-flight) |
| 9 | tier-grant-create / client-provision write endpoints |
C | absent (per 3d pre-flight) |
| 10 | Onboarding UI that creates client + grants (the scope producer) | C | parked; populates #6/#7/#8/#9 |
| 11 | RTO context header data source (spec compliance-grade) | C | needs clients + scope link |
| 12 | Workspace-surface edge CF Access coverage | D | not verifiable in code → resolved by D-12 read |
| 13 | T4A-default-for-everyone pre-onboarding posture | D | resolved → see §8 |
| 14 | Stale auth-architecture.md |
D | resolved → see §8 |
Headline: the auth bridge's identity half is already built and canonical (magic-link → identity-db user → tier resolution → session). The gap is entirely the scope half, and it's purely onboarding-coupled — every C item is the same foundation the IMM-01 Phase-3d park decision parked. Nothing on the auth path needs pre-building ahead of onboarding; the plumbing waits for a scope producer.
8. Verdicts (Tim, 2026-05-29)¶
- D-13 — pre-onboarding posture: block un-scoped non-operators at the Studio door; kill the silent
'unknown'write. Operators (T3,client_idnull by design) stay in. → executed by STUDIO-SCOPE-GATE-01. - D-12 — workspace/admin edge Access posture: authorised read-only read of the CF Access config across admin + workspace (no changes). Findings appended where the read lands.
- D-14 — stale auth doc: retire
docs/docs/architecture/auth-architecture.md(points at the wrong system). This audit + ADR-024/025/027 are the auth reference. Retired in the commit that files this audit. - C-items (8/9/10/11): stay parked inside the first-client-onboarding arc (build-with-the-UI), per the IMM-01 Phase-3d park decision. This audit does not propose building them.
The auth bridge's identity half is built; the scope half is the onboarding foundation. Read-only; ended at truth. First tile of the existing-system blueprint.