From 85eee388de579331860cfb954436b549196f23c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 16:20:28 +0000 Subject: [PATCH] docs(comments): correct four docblock sites that still describe the retired platform-admin elevation gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The #11663 platform-admin re-anchor (legs L2 and L4) retired the walled platform-admin elevation gate: standing is now derived per request, from an env-configured verified email OR the legacy unscoped grant row. Four comment sites still described the retired mechanism. - packages/types/src/email-verified.ts: the docblock named the elevation gate as one of exactly two consumers. It ships in the package's .d.ts, so the wrong consumer set reaches consumers. Replaced with the live consumer set, which now includes the authorization derivation itself. - packages/plugins/plugin-auth/src/last-admin-guard.ts, the docblock above PERMISSION_SET_STANDING_KEYS: three consequences of the same dead premise — "un-makes every platform admin at once", "never from the capabilities it carries", and "UNSCOPED sys_user_permission_set grants only". Comment-only. `active` deliberately stays in PERMISSION_SET_STANDING_KEYS and the corrected prose now says why: it is a safety-side over-approximation, and removing it would be a behaviour change. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs --- .../plugin-auth/src/last-admin-guard.ts | 43 +++++++++++++------ packages/types/src/email-verified.ts | 38 +++++++++++----- 2 files changed, 59 insertions(+), 22 deletions(-) diff --git a/packages/plugins/plugin-auth/src/last-admin-guard.ts b/packages/plugins/plugin-auth/src/last-admin-guard.ts index 2926688427..e45a2c6a39 100644 --- a/packages/plugins/plugin-auth/src/last-admin-guard.ts +++ b/packages/plugins/plugin-auth/src/last-admin-guard.ts @@ -634,8 +634,21 @@ export const GRANT_STANDING_KEYS = [ * to say so: `resolveAuthzContext` derived `platform_admin` from the name * alone and read no flag. It now drops a DEACTIVATED set before any * derivation, so `active: false` on `admin_full_access` un-makes every - * platform admin at once — the same end state as renaming or deleting the - * row, reached by a payload that touches neither. Enforcing the flag without + * GRANT-anchored platform admin at once — the same end state as renaming or + * deleting the row, reached by a payload that touches neither. ⚠️ Not + * "every platform admin": since the #11663 re-anchor (L2) standing has a + * SECOND anchor this write cannot reach — a config-anchored administrator + * (a declared `OS_PLATFORM_OWNER_EMAIL` address on a VERIFIED `sys_user` + * row) is derived at `resolve-authz-context.ts` §6b-config without + * consulting the set row or its `active` flag at all, and carries the + * shipped `ADMIN_FULL_ACCESS_CAPABILITIES` envelope rather than the stored + * set's. That is deliberately NOT a reason to drop `active` from this + * list: the write can still empty the GRANT anchor, which on every + * deployment that has declared no administrator emails is the whole + * population. Listing it is an over-approximation in the SAFE direction — + * it can cost an enumeration on a write that turns out to change no count, + * never the reverse — and taking it out would be a behaviour change, not a + * comment fix. Enforcing the flag without * listing it here would have left exactly one unguarded route to an * installation-wide lockout: the action is offered on every row with no * visibility or condition guard, the seeders deliberately never reconcile @@ -645,13 +658,16 @@ export const GRANT_STANDING_KEYS = [ * Everything else a permission-set write touches (`label`, `description`, the * four permission JSON blobs, provenance) is still invisible to "who is an * administrator" — `resolveAuthzContext` derives `platform_admin` from the NAME - * of an ACTIVE set, never from the capabilities it carries — so those writes - * still cost this guard no reads at all. Adding `active` does not walk that - * back: the projection is FACETS ONLY and deliberately never re-flips a - * record's on/off switch (`permission-set-projection.ts`, #4669), so every - * projection pass, every `os meta resync` and every ordinary Setup edit still - * misses this list entirely. What now pays for an enumeration is the write that - * actually toggles the switch — which is the write this list exists to judge. + * of an ACTIVE set or, since the #11663 re-anchor (L2), from the + * deployment-config anchor, and from the capabilities of neither (the config + * arm's envelope is the shipped `ADMIN_FULL_ACCESS_CAPABILITIES` declaration, + * not the stored row) — so those writes still cost this guard no reads at all. + * Adding `active` does not walk that back: the projection is FACETS ONLY and + * deliberately never re-flips a record's on/off switch + * (`permission-set-projection.ts`, #4669), so every projection pass, every + * `os meta resync` and every ordinary Setup edit still misses this list + * entirely. What now pays for an enumeration is the write that actually + * toggles the switch — which is the write this list exists to judge. * * `id` is deliberately NOT here even though the enumeration reads it. On this * engine `data.id` on an update ADDRESSES the row (it is what @@ -661,9 +677,12 @@ export const GRANT_STANDING_KEYS = [ * * `sys_position` gets no analogous list because it has no route into this * enumeration to guard: platform-admin standing is read from UNSCOPED - * `sys_user_permission_set` grants only (a position-bound `admin_full_access` - * never conferred it, in the resolver or here), and org-administrator standing - * is read from `sys_member.role`. Deactivating a position cannot empty either. + * `sys_user_permission_set` grants and from the deployment-config anchor (a + * declared `OS_PLATFORM_OWNER_EMAIL` address on a VERIFIED `sys_user` row, + * which `USER_STANDING_KEYS` below guards) — a position-bound + * `admin_full_access` reaches neither, in the resolver or here — and + * org-administrator standing is read from `sys_member.role`. Deactivating a + * position cannot empty any of them. */ export const PERMISSION_SET_STANDING_KEYS = ['name', 'active'] as const; diff --git a/packages/types/src/email-verified.ts b/packages/types/src/email-verified.ts index 5d6d5290a8..4be39ea3c3 100644 --- a/packages/types/src/email-verified.ts +++ b/packages/types/src/email-verified.ts @@ -10,16 +10,34 @@ * column as verified would re-open the exact hole this predicate closes for * every row that predates the column. * - * ONE resolution, two consumers, by design (#12751): the walled - * platform-admin elevation gate (`plugin-security` - * `bootstrapPlatformAdmin`, where the check REFUSES an unverified owner - * match) and the walled owner-verification boot diagnostic (`plugin-auth` - * `walled-owner-verification-path.ts`, where the check decides whether the - * declared owner's account is already past needing a verification path). - * Those two must answer "is this row verified?" identically — a drift means - * a boot warning that forecasts a refusal the gate will not make, or stays - * silent about one it will. `@objectstack/types` is the shared home both - * packages already resolve `OS_PLATFORM_OWNER_EMAIL` from (`env.ts`). + * ONE resolution, several consumers, by design (#12751) — and since the + * #11663 platform-admin re-anchor (leg L4) the walled platform-admin + * ELEVATION GATE this paragraph used to name first is RETIRED: under a + * walled posture `bootstrapPlatformAdmin` writes no grant row and elevates + * nobody, it reports. Standing is derived PER REQUEST instead — from a + * config-anchored verified email, or the legacy unscoped grant row — so the + * consumer set now includes the authorization derivation itself: + * + * - `matchesConfiguredPlatformAdmin` (`@objectstack/core` + * `security/platform-admin.ts`), read at the one derivation site + * (`resolve-authz-context.ts` §6b-config), where an UNVERIFIED account + * holding a declared address confers nothing — and, through it, + * `plugin-auth`'s last-admin guard, whose administrator enumeration must + * answer the same question the resolver does; + * - `resolvePlatformAdminStanding` (`plugin-security` + * `platform-admin-service.ts`), the read-only standing/audit answer the + * walled boot reports from, and `isVerifiedPlatformOwnerRow` beside it + * (`platform-owner-wall-bypass.ts`), the Layer 0 wall bypass; + * - the walled owner-verification boot diagnostic (`plugin-auth` + * `walled-owner-verification-path.ts`, where the check decides whether the + * declared owner's account is already past needing a verification path). + * + * They must all answer "is this row verified?" identically — a drift is no + * longer just a boot warning forecasting a refusal that will not be made, it + * is a diagnostic, an audit surface or a guard disagreeing with who actually + * resolves PLATFORM_ADMIN on the next request. `@objectstack/types` is the + * shared home every one of those packages already resolves + * `OS_PLATFORM_OWNER_EMAIL` from (`env.ts`). */ export function isEmailVerifiedUserRow(row: unknown): boolean { const v = (row as { email_verified?: unknown } | null | undefined)?.email_verified;