Uh oh!
There was an error while loading. Please reload this page.
feat(auth): walled deployment's declared owner is email-verified at operator-provisioned creation - #12761
Conversation
…perator-provisioned creation The declared platform owner (OS_PLATFORM_OWNER_EMAIL), created through an operator provisioning path on a walled deployment — the bootstrap first account, admin create-user, SCIM — is stamped emailVerified at creation (maintainer ruling 2026-08-28, cloud#1677). The decision lives in walled-owner-operator-stamp.ts; AuthManager stages it at the admission gate and consumes it in the composed user.create.before hook, so the row is born verified and the existing create-replay elevates it. The WALLED_OWNER_NO_VERIFICATION_PATH warning now follows the probed owner account state: silent on a fresh walled boot (the stamp covers it), loud on an owner account already existing unverified or a spent bootstrap window. The 11343 verified predicate moves to @objectstack/types so the gate and the diagnostic read one allow-list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
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 89ef1b0e9c824b98c577eb7633fa9d5912c5cacc && git checkout 89ef1b0e9c824b98c577eb7633fa9d5912c5cacc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5700d83b3c8b1c9396de96f87611c6f128f26528 fdffe4a3291a4ac31eb0d01a120e14362250de33 && git checkout -B drift-repro 5700d83b3c8b1c9396de96f87611c6f128f26528 && git merge --no-ff fdffe4a3291a4ac31eb0d01a120e14362250de33
node scripts/docs-audit/affected-docs.mjs --json 5700d83b3c8b1c9396de96f87611c6f128f26528
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12751
Part of objectstack-ai/cloud#1653
What this does
On a walled deployment, the account whose email equals the declared platform owner (
OS_PLATFORM_OWNER_EMAIL) is stampedemailVerifiedat creation when it comes into existence through an operator provisioning path — the maintainer's ruling on cloud#1677 (2026-08-28, verbatim): 「运营方创建即视为已验证」. The trust anchor is the operator's env-var declaration plus the operator-executed creation, not a mailbox round-trip; SMTP stays required only for inviting others. This extends the #11343 precedent (dev-boot seeded admin) to production walled boots, whose declared owner previously could NEVER verify — and therefore never elevate — when the deployment wired no mail transport and no federated sign-in (the shipped EE.env.exampleshape).Mechanism
plugin-auth/src/walled-owner-operator-stamp.ts): walled posture family (postureEnforcesWallover the REQUESTED posture — the elevation gate's own input) + declared-owner email match (compared exactly the waybootstrapPlatformAdmincompares: trimmed, case-insensitive) + operator-provisioned creation class.AuthManager.validateAudienceAdmission): the one admission seam every creation path flows through, where the vendor'ssource.methodsignal and the bootstrap probe already live. An admitted creation matching the decision stages a one-shot stamp keyed by the lowercased address (TTL-pruned, mirroring the feat(spec,auth)!: one declared audience posture for who may self-register into an app — invite_only|email_domain|open, default invite_only #11739 staged-grant pattern).user.create.beforedatabase hook): the staged stamp is consumed once and the row is BORNemailVerified: true— the same at-creation shape as a trusted-SSO insert. A later email UPDATE to the owner address structurally cannot inherit it: the seam only a creation traverses.bootstrapPlatformAdmin(shouldReplayBootstrapFor,createarm), which now finds a verified owner match and elevates — the same proven chain a clicked verification link used to drive.plugin-security's private copy to@objectstack/types(isEmailVerifiedUserRow) so the elevation gate and the boot diagnostic read ONE resolution.plugin-securitybehaviour is unchanged (alias to the shared function).Which operator paths qualify, and why (for the EE docs)
The classification is deliberately the existing #11739 audience taxonomy (
classifyCreationMethod), not a second parallel reading:self-serveclass with zero human users — the very first account on a fresh install) — qualifies. On a walled deployment self-registration is closed; the one self-serve creation a fresh walled boot admits is the bootstrap carve-out, which exists precisely because the first account is presumed to be the operator standing the deployment up ("a fresh install must never lock its operator out"). The declared-owner match narrows that presumption to the single address only the operator's environment declaration names. Residual exposure — a stranger reaching the sign-up endpoint of a freshly booted walled deployment before the operator, typing the operator's own declared address — is the same first-account trust the carve-out already extends; the pre-existing outcome of that race was already a deployment dead for its owner (address squatted unverified, elevation refused forever), requiring operator intervention either way. The ruling accepts declaration + creation act as the anchor.method: 'admin') — qualifies. This creation only exists inside an authenticated admin session; it IS the provisioning mechanism the closed postures point operators at.method: 'scim') — qualifies. Provisioning executed by the operator-registered directory: registering the IdP is the operator declaring "this directory is my audience", and the provisioning request is that declaration acting.Never qualifying:
emailVerifiedat insert; overriding an authority that DECLINED to assert the address would manufacture verification nobody stands behind. A verified IdP claim already arrives verified without this change.The boot warning follows the store now
WALLED_OWNER_NO_VERIFICATION_PATHpreviously fired on every walled boot with no transport and no federated sign-in — including the fresh deployment this change makes healthy, and every later boot of a settled, verified deployment. The predicate now reads a probed owner-account state (probeWalledOwnerAccountState, mirroring the elevation gate's reads and the shared allow-list):Consumer acceptance (cloud EE e2e — the two red cause-2 cases)
Ran
apps/objectos-ee/test/signup-membership-policy.e2e.test.ts(cloud repo, origin/main fixtures, unmodified) on a dedicated detached cloud worktree whosenode_modules/@objectstack/*symlinks were repointed (177 links, prefix substitution) to THIS branch's full build — verified by content: the CLI-resolvedplugin-auth/dist/index.jscarries the stamp code (marker grep, 2 hits) before any result was trusted.foundAdmin— "the platform admin never received their default-organization owner row" (owner registers, unverified, elevation refused forever).foundAdmin— the declared owner registers through the bootstrap carve-out on a production-mode serve (real license,OS_TENANCY_POSTURE=isolated, no transport), is born verified, elevated, and receives the default-organization owner row. The residual failure in each case is the SECOND user's bare self-registration (SELF_REGISTRATION_CLOSED) — that is cause 1, owned by the in-flight fixture PR objectstack-ai/cloud#1680; cloud#1677 remains open until both halves land per its recorded sequencing.08adcb30, fixture + this framework): 2/5 pass; the 3 failures are all bare self-registrations of second users (including its new cloud#1509-shaped case, where the declared owner arrives AFTER a bystander) — an intermediate branch state. Sequencing note for that PR: under the ruled contract an invitation-admitted owner deliberately does NOT inherit the stamp, so a late-arriving owner in that new case must arrive through an operator act (or a wired transport) to elevate.Tests
plugin-auth/src/walled-owner-operator-stamp.test.ts(21): the pure decision matrix (every bound), the store probe over a real ObjectQL engine, and the wiring end-to-end through the real better-auth pipeline — owner-via-bootstrap born verified (the consumer lane), owner-via-admin-create born verified with a non-owner control, seed lane (walled dev boot) unchanged in effect, invited owner NOT stamped, unwalled NOT stamped, non-owner NOT stamped, email-change inherits nothing. Read-back uses the shared elevation-gate predicate, so green means "the gate would elevate this row".plugin-auth/src/auth-plugin-walled-owner-verification-path.test.ts(24): the warning's two-sided contract, including the flipped pin (fresh production walled boot now SILENT) and every keep-warning shape.@objectstack/types/src/email-verified.test.ts: the allow-list pinned representation by representation.false(anchored replace; anchor count 1 to 0, marker 0 to 1 verified by grep before running) — 7 failed / 14 passed, exactly the stamp cells red and the "never" cells green; restore proven byte-identical to the HEAD blob (git hash-objectmatch + cleangit diff HEAD+ marker count 0), re-run 21/21 green. Tests exercise package src via relative imports (no dist in the loop), so no rebuild leg applies — stated per contract. A first ablation attempt was a proven no-op (perl delimiter collision, anchor count unchanged) and was discarded without taking a measurement.fdffe4a32(scripts/pm/dispatch-gates.mjs, no paths passed): all path-derived and convention-derived families run green locally, includingcheck:cross-package-test-inputs,check:engine-double-contract,check:where-matcher,check:type-check-coverage,check:type-check-debt(its own line: "31 ledger entries re-measured … none above its recorded number"),check:i18n("OK (9 packages)"),check:nul-bytes, changeset gates. The two docs-drift-check scripts are advisory and need PR context — left to CI.check-half-statesis a report-only board patrol (exit 0 with an authenticated token).No new feature-gated env inputs are introduced (the change reads the existing
OS_PLATFORM_OWNER_EMAIL/OS_TENANCY_POSTURE), so the bidirectional feature-gate ledger is not applicable.Verification note: heavy runs were executed through
scripts/pm/os-verify-lock.shwhere possible; on this macOS host the entry point itself declares UNLOCKED MODE (no usable flock), so all runs above executed unserialised — declared here per the resource contract.Session: session_71836b57-5db6-459d-ae4d-c6d9d7dd2cc6
Generated by Claude Code