Skip to content

STUDIO-SCOPE-GATE-01 — Close Report

Brief: STUDIO-SCOPE-GATE-01 — block authenticated-but-unscoped non-operators at the Studio door; kill the silent 'unknown' org_id write. First build drip of the first-client-onboarding arc; executes the D-13 verdict from AUTH-BRIDGE-AUDIT-01. Filed by: Tim 2026-05-29. Executed: 2026-05-29 — single session, Gates 1–5. Read-only inventory + code change (mutating, both envs). Dev test-data seed + sweep handled in-session; the prod-residue sweep and the prod allowlist gap are explicitly held for a deliberate migration-completion brief — not patched here.

Commit (local main, pending push): - c7e8534c — the wall. 8 files: apps/workspace/app/api/studio/auth.ts + 6 gated routes + studio/me. tsc clean (exit 0), eslint clean (exit 0).

Deploys — both on c7e8534c:

Worker Env Version Proof
rtopacks-workspace-dev dev b4ca9c23… deployed + smoked; full wall coverage verified
rtopacks-workspace prod e35f8790-ea1f-4f75-b3cd-fe8a1df10de3 deployed; /_build built_at 2026-05-29T06:27:56Z (post-commit 05:08:24Z, up from 2026-05-28T02:14:59Z); inert (empty allowlist → no sessions hit it)

(Version provenance: recorded from the prior-session deploy outputs. The CF MCP workers_get_worker exposes only the worker script id (c627fdbc…), not the active deployment version, and there's no deployments/versions read available — so the prod version is not independently re-confirmed against the live worker here.)

Status: Closed. Wall live on both envs. Dev smoke seed swept clean. Prod-residue sweep + the migration-completion allowlist gap held for a follow-up brief.


1. What landed

  • auth.tsOPERATOR_SCOPE = 'org_rtopacks_ops' (REUSED, not minted — the value already hardcoded in studio-collab-do + internal-api and present on existing operator sessions; dual-role flagged in-code). authenticateWorkspace resolves org scope once — client_id ?? (tier === 'T3' ? OPERATOR_SCOPE : null) — and now surfaces tier. New resolveStudioScope() returns {orgId} (string-narrowed) or a clean 403 not_provisioned wall.
  • 6 gated routes (session, sessions, scope, preferences ×2, proposed, session/[id] ×2) — wall placed immediately after the !auth 401; every ?? 'unknown' replaced with scope.orgId. The literal 'unknown' is eliminated from the studio write/read paths.
  • me — deliberately not walled (the UI needs the un-provisioned state); now returns tier.
  • session/[id]:67 ownership guard untouched → operators are scoped normally (no T3 exemption, per Tim's #4). Cross-client "view-as" is a clean seam for a forward logged-impersonation brief, not built here.

Net effect: Studio is honestly operator-only until the onboarding pipeline can grant client scope. The silent 'unknown' sentinel can never be written again.

2. Verification

  • Dev wall smoke (un-scoped T4A/client_id null user): all 8 gated route+method combos → 403 not_provisioned; me200; proposed POST walled before any mutation. Dev identity-db being empty (0 grants) made every dev login the un-scoped case — ideal coverage.
  • T3-operator-pass leg — confirmed by code inspection (auth.ts:71 → T3 yields OPERATOR_SCOPE, non-null → resolveStudioScope returns {orgId} → no wall → route proceeds). Not runtime-exercised: dev has no T3 grant, and prod login is closed by the allowlist gap (§5). Per Tim, code-read + the dev un-scoped runtime closes this leg; live exercise deferred.
  • Prod — inert deploy (empty allowlist, no sessions). studio_sessions created by walled users on dev = 0 — proof no write leaked past the wall.

3. Gates

  1. Pre-flight — code-site map + mechanism decided (operator scope reuse, no T3 exemption), read-only.
  2. Staged conversion — 8-file diff staged.
  3. Bytes — full diff + §4 inventory pasted; tsc/eslint clean.
  4. Land — commit c7e8534c; dev deploy + smoke; prod deploy (parity / deploy-on-surface-touch).
  5. Close — this report.

4. Dev test-data sweep (done this session — gone-is-gone)

Seeded into dev rto-identity-db-staging to exercise the magic-link path: - operator user 00000000-…-001 (FK satisfier for added_by), allowlist row smoke-ssg01-client-dev. Tim's dev login then upserted client@rtopacks.dev (f7fe5798-8d57-4fc9-b949-910c6b32e08c).

Swept child-first (3× changes: 1): allowlist row → zero-UUID user → client@ user. Dev identity-db restored to dormant — users 0, allowlist 0, tier_grants 0. Dev session tokens TTL out of SESSION_KV on their own.

5. Held — NOT patched (→ migration-completion brief)

  • Prod magic_link_allowlist is empty. IMM-01 Phase 3c repointed the allowlist read (ops-db access_allowlist → canonical magic_link_allowlist) but never carried the data. The operator (admin@rtopacks.com.au), the rtopacks.com.au domain, tim@ucca.edu.au, compliance@ucca.edu.au and jimmy@jimmykuo.com.au are all still active in the superseded ops-db access_allowlist, while the canonical table is empty → prod workspace login is closed for everyone. Half-migrated: the operator's users row + T3 grant were carried; the allowlist entry was not (MIGRATION-COMPLETION-DISCIPLINE).
  • Held per Tim: do not seed admin@ as a one-off. Opening prod workspace + retiring the orphaned ops-db access_allowlist rows belongs in a deliberate migration-completion brief. The operator-pass runtime test rides that brief.

6. Prod inventory carried forward (Tim's sweep, his hand — likely folds into the migration-completion brief)

  • KEEP: prod rto-identity-db T3 operator grant 65163496-12a1-45dc-9a69-4a567511777a + user 00000000-…-001 (admin@).
  • Sweepable prod residue: workspace-db (engine-db-oc) — 4 users, 1 user_tenant_role, 23 studio_sessions (all created 2026-04-15→30, predating c7e8534c — pre-wall residue, not post-wall writes slipping the gate), 1 studio_preferences, 2 studio_proposed_scope; ops-db — 2 UCCA-duplicate orgs, 1 org_membership. No 'unknown' rows exist anywhere.
  • org_rtopacks_ops dual-role: it is both the operator-scope constant and an ops-db orgs row in the UCCA-duplicate set (ORGS-DUPLICATE-UCCA-RECORD-CLEANUP-01). It is load-bearing for operator scope — that sweep must not delete it blind.

7. Surfaced follow-ups

  • Migration-completion brief (named above): carry/retire ops-db access_allowlist → canonical magic_link_allowlist, open prod workspace, exercise the operator-pass leg live.
  • PARALLEL-SESSION-FETCHER-RETIREMENT-01authenticateWorkspace remains a duplicate of lib/auth.ts:getSessionFromKV (untouched, as scoped; the wall was placed without entrenching or unwinding it).
  • session/[id] PATCH has no ownership check (only GET does) — pre-existing, surfaced during the diff; flagged for a future tightening, out of SSG-01 scope.
  • Doc touch-up: ADR-028's changelog line and IMM-01-Phase-3d-PREFLIGHT-PARK-DECISION-01 cite close reports as briefs/closed/…; the actual path is docs/docs/ops/briefs/closed/…. Under-qualified (files exist) — resolve on the next ADR/decisions touch.

The wall is live on both envs; Studio is honestly operator-only until scope can be granted. The scope half of the auth bridge remains the parked onboarding foundation (AUTH-BRIDGE-AUDIT-01 §5, IMM-01-Phase-3d-PREFLIGHT-PARK-DECISION-01).