Skip to content

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)

  1. Surface topology. Studio = apps/workspace (app/(workspace)/studio/, app/api/studio/), worker rtopacks-workspacemy.rtopacks.com.au/* (prod) / rtopacks-workspace-devmy.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.

  2. Entry / authentication. apps/workspace/middleware.ts:61–64 gates on presence of the rtp_session cookie → redirect /auth if 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–10 confirms 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.

  3. Identity resolution. lib/auth.ts:9–33 (getSessionFromKV) and app/api/studio/auth.ts:31–67 (authenticateWorkspace) both resolve identity from the rtp_session cookie → SESSION_KV session:{id}RTPSession blob. No identity-db / workspace-db / INTERNAL_API call at request time — identity is whatever was baked into KV at verify time.

  4. Client scope. RTPSession carries client_id: string | null + tier (lib/session-types.ts:16–32, canonical per ADR-024). Populated by resolveUserTier (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, pending IDENTITY-MANAGEMENT-UI-SPEC-01).

  5. Studio's requirement. authenticateWorkspace hands Studio {email, user_id, org_id (= client_id), display_name}. Studio persists studio_sessions to WORKSPACE_DB with org_id = auth.org_id ?? 'unknown' (app/api/studio/session/route.ts:422) and lists/filters by org_id (app/api/studio/sessions/route.ts:32–36WHERE org_id IN ('unknown', LEGACY_SEED_ORG); same key in scope, preferences, proposed routes). 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'.

  6. The seam — exists, canonical-routed. app/auth/verify/route.ts (magic-link verify): magic token → POST /identity/user-upsert (INTERNAL_API, workspace-worker source) → canonical user_idresolveUserTier → build RTPSession → 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, no clients table.
  • 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_id null 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.