Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-security): walled elevation requires a verified owner-email match, and the bootstrap replays on the verifying update - #11639
Conversation
…l match, and the bootstrap replays on the verifying update Closes the remainder #11211 left open (issue #11343): under walled postures the platform-admin elevation matched the env-declared owner email as a raw string, and email verification is off by default — so an account created with the owner's address before the owner registered would be elevated. Both halves land together, deliberately: 1. bootstrapPlatformAdmin: the walled owner-email match must be VERIFIED (fail-closed allow-list over email_verified representations; absent reads unverified). Unverified holder => refused with the new reason walled_owner_not_verified, logged loudly with the unblock in the line, never falling back — the same direction as the undeclared-owner refusal. 2. security-plugin bootstrap-replay middleware: also fires on sys_user updates touching email_verified / email (trigger set extracted as shouldReplayBootstrapFor, consumed by middleware and pins alike). Verification is an UPDATE; with the old insert-only replay, half 1 alone would refuse the genuine owner at sign-up and never look again, leaving the platform without any administrator. single posture is untouched in both directions. Pins cover both invariant directions: the unverified holder is refused AND the verified owner is elevated, including across the refuse-then-verify-then-re-run sequence. Fixes#11343 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
…rified-email-elevation
…tEngineUpdateDispatch; record the pinned double check:engine-double-contract named the new fake's bare update(); pin it to the producer's dispatch predicate and let the RETAINED ledger learn the new pinned coverage (--write; 0 lost, coverage grew). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
📓 Docs Drift CheckThis PR changes 2 package(s): 24 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 20 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0f80bbea5920790a445c827beed3cb9df624667b && git checkout 0f80bbea5920790a445c827beed3cb9df624667b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 266436a7f78b52a1c7a79d631ed6eea75118700d c9be0f18ced013be3a166f6d530444a21742e4f5 && git checkout -B drift-repro 266436a7f78b52a1c7a79d631ed6eea75118700d && git merge --no-ff c9be0f18ced013be3a166f6d530444a21742e4f5
node scripts/docs-audit/affected-docs.mjs --json 266436a7f78b52a1c7a79d631ed6eea75118700d
|
…rified-email-elevation
…-provisioned, not a self-registrant The Dogfood Regression Gate caught #11343's invariant end-to-end: two walled-posture fixtures (automation-toggle-tenant-scope :189, federated-rls-injectors :135) build their platform admin through the shared dev-admin seed, which drives the real sign-up pipeline and so left the account unverified — correctly refused by the restored invariant, i.e. the fixtures modeled a deployment state the platform no longer considers valid. The repair is the shared seed path, not the fixtures: maybeSeedDevAdmin now stamps email_verified on the account it just provisioned. The seeded dev admin is created by the deployment's own boot command with operator-known credentials — the same trust shape as a trusted-SSO insert — not an unknown self-registrant, which is the class the invariant refuses. Dev-only by the existing NODE_ENV gate; the generic sign-up factory is untouched, so unverified-user flows keep testing what they name. Fail-open on the stamp, fail-closed on elevation. Part of #11343 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
…ied stamp ships under the same card Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
os-sam
commented
Aug 24, 2026
PM review — PASS. Flipping ready and arming the merge queue at |
Fixes#11343
What this closes
Under walled postures (
group/isolated), the platform-admin elevation inbootstrapPlatformAdminmatched the env-declaredOS_PLATFORM_OWNER_EMAILagainst the raw email string onsys_user, with noemail_verifiedcondition — while email verification is off by default. PR #11211 was a real and large narrowing — it moved the hole from "whoever registers first" to "whoever knows the owner's email and registers first". This PR closes that remainder, exactly as card #11343 records it; no new reproduction detail is added here.Direction per the card's triage grading (maintainer-invoked, 2026-08-24): invariant restoration — the elevation match must be a verified email. No product fork.
Both halves, deliberately in one change
The card's own measurement is that the obvious fix taken alone creates a worse defect: the bootstrap re-run listened for
sys_userinsert only, and email verification is an update — so requiring verification without an update trigger would refuse the genuine owner at sign-up and then never look again, leaving a first boot with no administrator at all.bootstrap-platform-admin.ts— the elevation half. The walled owner-email match additionally requiresemail_verified, via a fail-closed allow-list predicate over driver representations (true/1/'1'/'true'); an absent field on an imported/legacy row reads as unverified. An unverified holder of the owner's address is refused with the new reasonwalled_owner_not_verified, logged loudly atwarnwith the unblock in the line, and never falls back — the same fail-closed direction as the existing undeclared-owner refusal.singleposture is untouched in both directions (first-user promotion gains no verification requirement; the owner-email variable is still never consulted there — both pinned).security-plugin.ts— the replay half. The bootstrap-replay middleware now fires onsys_userupdates touchingemail_verifiedoremail(the change-email flow rewrites both), in addition to insert/create. The trigger set is extracted asshouldReplayBootstrapForinbootstrap-platform-admin.ts, consumed by the middleware and its pins alike (theresolveEngineUpdateDispatchpattern), and deliberately does not fire on unrelated profile edits.Pins, in both directions
A suite pinning only the refusal would score green on a platform nobody can administer, so both directions are pinned in
bootstrap-platform-admin-walled-owner.test.tsandsecurity-plugin.test.ts:false, absent field, driver0) ⇒adminPromoted: false, reasonwalled_owner_not_verified, nosys_user_permission_setwrite, loud warn namingOS_PLATFORM_OWNER_EMAIL.1reads verified.Reverse verification (mutate → confirm on disk → run → restore byte-identically, trap'd): removing the verified filter reds exactly the 5 refusal-direction pins (15 others stay green); disabling the predicate's update branch reds exactly the 2 update-trigger pins plus the wiring pin (258 others stay green). Restoration proven by
git hash-objectagainst the HEAD blob. Both suites resolve the mutated modules via same-package relative imports (nodist/in the subject's path), so no rebuild was required for the ablation legs.Verification at head
cd8a891892All run through the shared verify lock, after
origin/mainwas merged in (clean merge; the #11610 edit tosecurity-plugin.tsis in a different region):pnpm --filter @objectstack/plugin-security test— 76 files, 1479 passed ·typecheck(tsc --noEmit) cleancheck:nul-bytes·check:changeset-gate-self-tests·check:cross-package-test-inputs·check:objectui-changeset·check:published-files·check:slot-lookup·check:test-source-alias·check:type-source-resolution· ADR-0087 registration · changeset-no-major · ci-filter-parity · empty-changeset · plugin-teardown-shape · affected-docs · release-rehearsal self-test ·check:query-options-erasure·check:type-check-coverage·check:engine-double-contract·check:where-matcher·check:i18n— all green (BATCH-RESULT: FAIL=0)check:type-check-debt(--re-measure, closure built): OK — 32 ledger entries re-measured, none above its recorded numberupdate()throughassertEngineUpdateDispatchand is recorded in the pinned ledger (--write; coverage grew, nothing lost)Repo-wide
pnpm lintwas not run locally — CI owns that run on every PR.Changeset:
.changeset/walled-elevation-verified-email.md(patch,@objectstack/plugin-security).Generated by Claude Code
Generated by Claude Code