Skip to content

feat(auth): walled deployment's declared owner is email-verified at operator-provisioned creation (extends #11343 to production walled boots) #12751

Description

@os-zhuang

Part of objectstack-ai/cloud#1653 (via cloud#1677 — the walled-EE owner-verification half; the maintainer's ruling is recorded there, 2026-08-28).

Deliverable

On a walled deployment (audience invite_only/walled posture family), when the account whose email equals the declared platform owner (OS_PLATFORM_OWNER_EMAIL) is created through the OPERATOR's own bootstrap/provisioning path, stamp it email-verified at creation — the same trust shape as os#11343's dev-boot seeded-admin stamp (that precedent's own comment already argues the class: "provisioned by the deployment's own boot command with operator-known credentials — not an unknown self-registrant"). Today that stamp is NODE_ENV-gated to dev, so a production walled EE deployment whose .env.example ships no mail transport has an owner that can NEVER verify → NEVER elevate (WALLED_OWNER_NO_VERIFICATION_PATH warns and nothing else can happen).

Bounds (the contract, not suggestions):

  • ONLY the account matching the declared owner email, ONLY when created via the operator bootstrap/seed path — never self-registration, never other users, never a later email change (an email UPDATE to the owner address must not inherit the stamp).
  • Walled posture context; dev-boot behaviour (os#11343) unchanged.
  • The WALLED_OWNER_NO_VERIFICATION_PATH warning should stop firing for the case this closes and keep firing for genuinely dead-ended shapes (owner exists, unverified, created outside the operator path, no transport).
  • Consumer: apps/objectos-ee's signup-membership-policy e2e in the cloud repo has 2 red cases asserting exactly this dead end (diagnosis on cloud#1677); they must go green on a rig running your branch WITHOUT fixture changes to those cases.

Verify premises against origin/main first (locate the #11343 stamp, the bootstrap path, the warning's predicate); premise_still_valid:false + evidence is a first-class deliverable. Tests: the stamp matrix (owner-via-operator ⇒ verified; owner-via-self-serve ⇒ not; non-owner-via-operator ⇒ not; email-change ⇒ no inheritance; dev boot unchanged), plus the warning's two-sided behaviour. Ablation, mutation proven on disk. .changeset (never major).

Activity

  1. self-assigned this
    on Aug 27, 2026
  2. os-zhuang commented on Aug 27, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop (epic objectstack-ai/cloud#1653 via cloud#1677, maintainer direct-dispatch channel)
    Session: session_71836b57-5db6-459d-ae4d-c6d9d7dd2cc6
    Branch: claude/issue-12751-walled-owner-operator-verified
    Worktree: objectstack-issue-12751
    Domain: domain:services
    File surface: packages/plugins/plugin-auth/src/** (the #11343 stamp path, bootstrap provisioning, the WALLED_OWNER_NO_VERIFICATION_PATH predicate) (stop on breach; explain)
    Container & model: M, mode:subagent, model: claude-fable-5 — Clause-② content limb: changes auth accept behaviour (an account becomes verified/elevatable where it could not before)
    Clause-②: yes
    Serial constraints cleared: no open PR touches plugin-auth (checked this hour); the in-flight cloud#1677 cause-1 dev touches only apps/objectos-ee. mode:subagent tier not self-provable ⇒ needs:contract-review chain applies at ACCEPT.
    Audit: maintainer ruling 2026-08-28 (decision picker): 「运营方创建即视为已验证」; recorded verbatim on cloud#1677.

  3. added a commit that references this issue on Aug 27, 2026
  4. hotlong commented on Aug 27, 2026

    @hotlong
    Contributor

    os-dev-report

    {
      "issue": 12751,
      "status": "done",
      "branch": "claude/issue-12751-walled-owner-operator-verified",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12761",
      "premise_still_valid": true,
      "summary": "All four premises verified live at origin/main 1e3e66788: the 11343 dev-boot stamp (plugin-auth auth-plugin.ts maybeSeedDevAdmin), the walled elevation gate requiring a VERIFIED owner match (plugin-security bootstrap-platform-admin.ts, trimmed case-insensitive comparison mirrored), the WALLED_OWNER_NO_VERIFICATION_PATH predicate (walled-owner-verification-path.ts), and the creation-path taxonomy (audience-posture.ts). Implemented the ruled stamp: decision module walled-owner-operator-stamp.ts (walled family + declared-owner match + operator-provisioned class), staged at validateAudienceAdmission (the one admission seam holding the vendor method signal and the bootstrap probe), consumed once by a composed user.create.before hook so the row is BORN emailVerified — updates structurally cannot inherit it; elevation rides the existing create-replay. Qualifying paths, argued in the PR docs section: bootstrap carve-out, admin create-user (method admin), SCIM; never non-bootstrap self-registration (invitation included), never provider JIT, never non-owner, never unwalled. The 11343 verified allow-list moved to @objectstack/types (isEmailVerifiedUserRow) so gate and diagnostic read one resolution. The warning now probes the owner-account state: silent on a fresh walled boot and on a verified owner; still warns on owner-exists-unverified, spent bootstrap window, seed-armed-for-other-address dev boots, and unanswerable probes. Consumer acceptance on a dedicated detached cloud worktree with all 177 @objectstack links repointed to this branch build (dist verified BY CONTENT before trusting results): both red cause-2 cases now pass foundAdmin — the declared owner registers on a production-mode walled serve with no transport, is born verified, elevated, and receives the default-organization owner row; the residual failure in each is the second user's bare self-registration (SELF_REGISTRATION_CLOSED), which is cause 1, owned by cloud PR 1680. Cross-checked that branch tip 08adcb30 with this framework: 2 of 5 pass, all 3 failures are bare second-user self-registrations there — intermediate fixture state, plus the sequencing note in open_questions.",
      "tests": "walled-owner-operator-stamp.test.ts 21/21 (pure matrix; probe over real ObjectQL/better-sqlite3; end-to-end through real better-auth: owner-via-bootstrap born verified, owner-via-admin-create born verified with non-owner control, seed lane unchanged in effect, invited owner NOT stamped, unwalled NOT stamped, email-change inherits nothing — read-back via the elevation gate's own predicate). auth-plugin-walled-owner-verification-path.test.ts 24/24 including the flipped pin (fresh production walled boot SILENT). Full suites at fdffe4a32: plugin-auth 'Tests 1686 passed', plugin-security 'Tests 1578 passed', types 'Tests 377 passed'; typecheck exit 0 all three (captured pre-pipe). Ablation: decision short-circuited to false via anchored replace, mutation proven on disk (anchor grep 1 to 0, marker 0 to 1) BEFORE running — 7 failed / 14 passed, exactly the stamp cells red, never-cells green; restore proven by git hash-object equal to the HEAD blob 497e8d2 plus clean git diff HEAD plus marker count 0; re-run 21/21. Tests exercise package src via relative imports, so no dist rebuild leg applies — declared. A first ablation leg was a proven no-op (perl delimiter collision; anchor count unchanged) and was discarded unmeasured. Derived gates re-run at final HEAD fdffe4a32 (dispatch-gates.mjs, no paths passed, stderr names this repo at that commit): all green by their own verdict lines — cross-package-test-inputs, engine-double-contract, where-matcher, type-check-coverage, type-check-debt (31 ledger entries re-measured, none above its recorded number), i18n (OK, 9 packages, after building the workspace CLI), nul-bytes, auth-mount-ledger, changeset family, page-declaration-shape, published-files, test-source-alias, type-source-resolution, slot-lookup, route-envelope, objectql-double-limit, comment-mask-adoption, ci-filter-parity, plugin-teardown-shape, adr-0087-registration, changeset-no-major, empty-changeset, release-rehearsal self-test. check-half-states: report-only patrol, exit 0 with an authenticated token. Two docs-drift scripts are advisory and need PR context — CI runs them. os-verify-lock.sh on this macOS host declares UNLOCKED MODE (no usable flock): all heavy runs executed unserialised, declared per contract. No new feature-gated env inputs, so the bidirectional guard ledger is not applicable.",
      "mcp_calls": "0 — all GitHub traffic via gh CLI REST",
      "open_questions": [
        {
          "question": "Sequencing intel for cloud PR 1680 (cause 1): its new cloud-1509-shaped case (the FIRST registrant, not being the declared owner, is never elevated — and the owner registering AFTER them is) has the owner arriving AFTER a bystander on a no-transport walled boot. Under the ruled contract an invitation-admitted owner deliberately does NOT inherit the stamp, so that late owner cannot elevate via the invitation lane.",
          "options": [
            "A — that case wires a mail transport for its one boot (verification-link lane)",
            "B — that case makes the owner's late arrival an operator act (admin create-user through an authorized session), which IS stamped"
          ],
          "recommendation": "Hand to the cloud-1680 owner/PM rather than deciding here — it is their fixture's design choice; the framework contract (self-registration never qualifies) is ruled and not re-adjudicable."
        }
      ],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  5. os-zhuang commented on Aug 27, 2026

    @os-zhuang
    ContributorAuthor

    ACCEPT — PR #12761. Verified against the branch (11 files, zero packages/spec), CI 0 failing at review time. Landing on green.

    • The decision module IS the ruled contract, pure and bounded: walled family (read from the REQUESTED posture, mirroring the elevation gate's own fail-stricter choice) + declared-owner match (comparison MIRRORED from bootstrapPlatformAdmin, with the module doc stating why the two must agree) + operator-provisioned class. Consumed once at user.create.before so the row is born verified — a later email UPDATE structurally cannot inherit the stamp, which is stronger than checking for inheritance.
    • Two exclusions are better-argued than my dispatch asked for: invitation-admitted creation is NOT stamped (the carve-out admits the creation; it does not verify the mailbox), and provider JIT is NOT stamped because the IdP asserts its own emailVerified — overriding an authority that DECLINED to assert the address would manufacture verification nobody stands behind.
    • The warning became two-sided correctly: silent on a fresh walled boot and on a verified owner; still fires on owner-exists-unverified, spent bootstrap window, seed-armed-for-other-address, unanswerable probes. The [security] 围墙提权只按未验证邮箱匹配 —— #11184 落地后仍存在一条更窄的同形路径:抢在 owner 之前用其邮箱注册即可被提权 #11343 allow-list moved to @objectstack/types so gate and diagnostic read one resolution.
    • Consumer acceptance measured, not asserted: on a dedicated cloud worktree with all links repointed to this branch's build (dist verified by content), both original cause-2 EE cases pass their owner-elevation half; residuals are cause-1 shapes owned by cloud PR feat(service-analytics): resolve dimension display labels (select options / lookup names) #1680. 21/21 matrix + 24/24 warning suite + full plugin-auth/security/types suites green; ablation with disk-proven mutation red on exactly the stamp cells; a no-op first leg discarded unmeasured rather than counted.

    Contract review (needs:contract-review → PASS): the accept-set change is precisely the ruled one — an account becomes verified/elevatable in exactly the three-conjunct case and no other; no spec file moved; no key/error-code/authorable surface changed; changeset minor-family. Reviewer: this seat, machine-verified at CONTRACT_REVIEW_TIER (claude-fable-5, host transcript field). Cleared on both carriers with this comment as the record.

    Sequencing adjudication for the handed-back question (cloud#1677's remaining walled fixtures): Option A — the late-arriving-owner case wires the in-repo mail lane for its one boot and completes verification via the captured link. B is measured-dead on this app twice over (admin plugin defaults off → 501; and its endpoint binds with hard-coded policy:'auto'), and A models the SHIPPED remedy for a late owner on a walled box. The two remaining walled cases' second users take the same invitation lane PR #1680 already established. Dispatching that completion slice on #1677 once this PR is on main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions