chore(flags): retire phoenix-admin-console — requireAdmin becomes the sole gate (#3671)#3728
Conversation
… sole gate (#3671) The admin-console shell flag (#2740) serves on@100% in prod, so rip the rollout gate and keep only the served-on path. Authorization is unchanged and stays server-authoritative. The `admin.probe` resolver kept two gates: the flag AND `requireAdmin` (`Admin.over(platform)`). Only the flag conjunct is removed — the capability check, whose denial is the invisible `Denied`, was always the authorization and remains the sole gate. Same shape in `overridesAuthorized`, where the flag ANDed into the admin arm: the arm collapses to `dev || Admin.over(platform)`, so a non-admin's `phoenix_flag_overrides` cookie stays inert on a deployed stage. The `baseline` parameter survives — `baseline.environment` is a live non-flag conjunct. Removing the flag also removes the reason `overridesAuthorized` had to read its gate against the baseline (no-cookie) context: the verdict now derives only from the environment and the actor's stored platform-admin relation, never from anything the request carries, so a cookie cannot self-authorize by construction. Client-side, `useAdminProbe` drops `useFlag`; the grant is the server verdict alone and the console chunk is still lazy-imported only past it. Deletes the IaC declaration (key const + DECLARED_FLAGS entry, flag config + factory, alchemy stack wiring) and the flag's invariant test. Repoints the orphaned `user-admin.invariant.test.ts` cross-reference at the durable `member-mute.invariant.test.ts`, and the flags-module enumeration assertion at MEMBER_MUTE. Closes #3671 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🚀 Preview deployed
|
|
review-code: PASS @ 51afecb — merge-ready Verified PR #3728 against the acceptance criteria of #3671, one at a time.
Security-critical check — the
|
|
review-design: PASS @ 51afecb — merge-ready Design-gated PR #3728 over its preview deploy, judging the locally captured bytes (ADR 0165). Dispatch rationale. Preview freshness (load-bearing). The sticky preview comment stamps commit Surfaces captured (Playwright over the preview via the
Rendered-surface delta is nil for every reachable visitor on the preview. The The six prohibitions (ADR 0162)
Render-exception check (#2594)PASS, deterministic. Golden-deviation class (calibration B, #2945)N/A. The committed Evidence noteThe screenshot upload to the GitHub user-attachments endpoint returned HTTP 400 ( No blocking violation, no render exception, no golden deviation. This PR is merge-ready on the design class. review-design does not merge — |
Retires the released
phoenix-admin-consoleflag (admin-console shell, #2740), which serveson@100%in prod. Only the served-on path remains.Closes #3671
Authorization — unchanged, still server-authoritative
This gates an admin surface, so the central question is what still gates it. The flag was only ever an additional rollout gate on top of a real capability check, at both of its two authorization-relevant sites:
1.
admin.proberesolver (worker/features/admin-console/queries.ts). Had two gates: the flag ANDrequireAdmin(Admin.over(platform)). Only the flag conjunct is removed.requireAdminsurvives as the sole gate —yield* Adminkeeps the probe row unreachable without a discharged platform-admin grant, and its denial is still the invisible künyeDenied, so a non-admin cannot distinguish "not an admin" from "not signed in" (ADR 0107 / ADR 0098 §2). This is the same shape as PR #3699 (requireFunnelAccess+Denied) and PR #3706 (server-sideisModerator).2.
overridesAuthorized(worker/features/flagship/override-authz.ts). The flag ANDed into the admin arm:dev || (consoleOn && Admin.over(platform)). The arm collapses todev || Admin.over(platform)— theAdminconjunct survives, so a non-admin'sphoenix_flag_overridescookie stays inert on a deployed stage and an anonymous request still resolvesfalse.Removing the flag also removes the reason
overridesAuthorizedhad to read its gate against the baseline (no-cookie) context: the verdict now derives only from the environment and the actor's stored platform-admin relation, never from anything the request carries. The "a cookie cannot self-authorize the gate" invariant (#622) is now structural rather than sequenced.Client side,
useAdminProbedropsuseFlag. The grant is the server'srequireAdminverdict and nothing else; the hook cannot widen it, and the console chunk is still lazy-imported only past the gate. Net effect: no admin surface becomes reachable without a real permission check.Gate-parameter decision
overridesAuthorized(baseline)keeps its parameter —baseline.environment === "development"is a live non-flag conjunct (the #622 dev convenience). This is the PR #3725 case, not the PR #3714 case.CI-vs-prod flag default (#3720)
defaultVariationwas"off"(dark-ship), so integration/e2e stages have been asserting the flag-OFF world. Traced blast radius:tests/e2e/31-kullanicilar-darkship.spec.ts— the one e2e spec touching the admin console. It asserts/adminrenders noadmin-consoleelement. It runs anonymous (page.goto("/admin"), no session), and its own docblock states it does not depend on a signed-in admin. With the flag gone the probe still denies an anonymous caller viarequireAdmin, so the assertion holds — for a different (and stronger) reason. Docblock reworded to say so rather than rewriting the assertions. Nophoenix_flag_overridescookie in this suite.override-authz.unit.test.ts— theprod, admin + flag OFF ⇒ NOT authorizedcase tested a world that no longer exists; deleted. The other four rows survive with the flag knob removed.shell-boot-parity.unit.test.ts— theadminConsoleOnstub knob was the only ON signal for the gate; removed, theFlagshipstub now returns the supplied default for every key.src/admin/flags/flags-module.test.ts— assertedDECLARED_FLAGScontainsphoenix-admin-console; repointed (below).One genuine CI-only behavior change, called out for the reviewer: a signed-in non-admin now issues an
admin.proberequest that returnsDenied, where previously the flag short-circuited the wire. In prod this is a no-op (flag was on); no spec exercises a signed-in non-admin against/admin. Nothing was preemptively rewritten beyond the observed cross-references.Cross-reference repoints
Deleting
admin-console.invariant.test.tsorphaned one docblock:user-admin.invariant.test.ts's "mirrors" pointer, repointed to the durablemember-mute.invariant.test.ts(#3112).flags-module.test.ts's enumeration assertion repointed toMEMBER_MUTE. Neither target is slated for retirement.Removed
flags/keys.tsconst +DECLARED_FLAGSentry;flagship/resources.tsconfig + factory + import;alchemy.run.tsimport + stack wiring; the flag's invariant test. Stale docblocks reworded acrossqueries.ts,probe-view.ts,fate-module.ts,FlagsContext.ts,request-flags-context.ts,fate/layers.ts,fate/route.ts,App.tsx,AdminConsoleRoute.tsx,useAdminProbe.ts, and the.glossary/TERMS.md"admin console" row.git grepshows zero surviving references to the flag string / const / factory underapps/web/. The only repo-wide mention left is the deliberate retirement-history sentence in the glossary row.Verification
pnpm typecheck --force— 33/33 successful,0 cached(no Turbo replay)pnpm lint— cleanpnpm vitest run --project unit— 285 files / 2352 tests passed🤖 Generated with Claude Code