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.ts—OPERATOR_SCOPE = 'org_rtopacks_ops'(REUSED, not minted — the value already hardcoded instudio-collab-do+internal-apiand present on existing operator sessions; dual-role flagged in-code).authenticateWorkspaceresolves org scope once —client_id ?? (tier === 'T3' ? OPERATOR_SCOPE : null)— and now surfacestier. NewresolveStudioScope()returns{orgId}(string-narrowed) or a clean 403not_provisionedwall.- 6 gated routes (session, sessions, scope, preferences ×2, proposed, session/[id] ×2) — wall placed immediately after the
!auth401; every?? 'unknown'replaced withscope.orgId. The literal'unknown'is eliminated from the studio write/read paths. me— deliberately not walled (the UI needs the un-provisioned state); now returnstier.session/[id]:67ownership 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 nulluser): all 8 gated route+method combos → 403not_provisioned;me→ 200;proposedPOST 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 yieldsOPERATOR_SCOPE, non-null →resolveStudioScopereturns{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_sessionscreated by walled users on dev = 0 — proof no write leaked past the wall.
3. Gates¶
- Pre-flight — code-site map + mechanism decided (operator scope reuse, no T3 exemption), read-only.
- Staged conversion — 8-file diff staged.
- Bytes — full diff + §4 inventory pasted; tsc/eslint clean.
- Land — commit
c7e8534c; dev deploy + smoke; prod deploy (parity / deploy-on-surface-touch). - 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_allowlistis empty. IMM-01 Phase 3c repointed the allowlist read (ops-dbaccess_allowlist→ canonicalmagic_link_allowlist) but never carried the data. The operator (admin@rtopacks.com.au), thertopacks.com.audomain,tim@ucca.edu.au,compliance@ucca.edu.auandjimmy@jimmykuo.com.auare all still active in the superseded ops-dbaccess_allowlist, while the canonical table is empty → prod workspace login is closed for everyone. Half-migrated: the operator'susersrow + 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-dbaccess_allowlistrows 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-dbT3 operator grant65163496-12a1-45dc-9a69-4a567511777a+ user00000000-…-001(admin@). - Sweepable prod residue: workspace-db (
engine-db-oc) — 4 users, 1user_tenant_role, 23studio_sessions(all created 2026-04-15→30, predatingc7e8534c— pre-wall residue, not post-wall writes slipping the gate), 1studio_preferences, 2studio_proposed_scope; ops-db — 2 UCCA-duplicateorgs, 1org_membership. No'unknown'rows exist anywhere. - ⚠
org_rtopacks_opsdual-role: it is both the operator-scope constant and an ops-dborgsrow 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→ canonicalmagic_link_allowlist, open prod workspace, exercise the operator-pass leg live. PARALLEL-SESSION-FETCHER-RETIREMENT-01—authenticateWorkspaceremains a duplicate oflib/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-01cite close reports asbriefs/closed/…; the actual path isdocs/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).