Repository navigation
fix(plugin-auth)!: a cloud-managed environment refuses local self-registration, invitations (resends included), user creation and import, and sets passwords only on local accounts - #25399
Conversation
… admin user and password doors (#25083) Services half of ruling 6341690661, prepared before the engine lane's isCloudManaged() lands in @objectstack/core. Not buildable on main yet. Claude-Session: https://claude.ai/code/session_017XVJLoyGccDY1WPoqow5Ff Co-authored-by: Claude <noreply@anthropic.com>
…chain (#25083) Claude-Session: https://claude.ai/code/session_017XVJLoyGccDY1WPoqow5Ff Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XVJLoyGccDY1WPoqow5Ff Co-authored-by: Claude <noreply@anthropic.com>
…oud-managed-doors # Conflicts: # packages/plugins/plugin-auth/src/auth-manager.ts
…() after the merge, and the package entry exports the managed refusal for a host twin After merging main (#25306 landed `isCloudManaged()` and its memoization, #25273 moved the actor guards out of auth-manager.ts): - auth-manager.ts no longer imports `hasPlatformAdminStanding`, which moved with the actor guards and is unused here now. - The cloud-managed suite resets the memoized accessor on every switch (`resetCloudManagedForTesting()`), so each case reads its own setting. - The package entry exports `cloud-managed.js` (`cloudManagedRefusal` and its constants), so the cloud's per-environment twin of `set-initial-password` answers with the same status, code and message; pinned with `isPlatformAdminUser` from the same entry. - The changeset declares the widening beside the narrowing: `Clause-②: yes (narrowing)`. Co-authored-by: Claude <noreply@anthropic.com>
…s at the route, so a resend of a pending invitation is refused too better-auth answers `resend: true` for an invitee already pending before it consults `beforeCreateInvitation`: it extends the row's `expiresAt` and sends the invitation e-mail again. The console reaches that branch through `sys_invitation`'s `resend_invitation` action. The `before` hook now refuses `/organization/invite-member` under `isCloudManaged()` with the same 409 RESOURCE_CONFLICT; the hook arm stays as the second line. Pinned with a resend leg (409, expiry unchanged) and its unmanaged control (200, expiry extended). The changeset names the resend and the two body shapes (ADR-0112 envelope on the raw routes, better-auth's own body on invite-member). Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 19 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 38b5e2c062f3076b268b5213d5d45c886572fefa && git checkout 38b5e2c062f3076b268b5213d5d45c886572fefa
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3306cada26d05160dadd704ea31105b2db48a884 e5f87a19ca1158715631528b15e9fe3c81647229 && git checkout -B drift-repro 3306cada26d05160dadd704ea31105b2db48a884 && git merge --no-ff e5f87a19ca1158715631528b15e9fe3c81647229
node scripts/docs-audit/affected-docs.mjs --json 3306cada26d05160dadd704ea31105b2db48a884
|
…idation and handler, not its body parse Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewHead: Coverage:
Dispositions:
Declarations checked:
|
…oud-managed-doors # Conflicts: # packages/plugins/plugin-auth/src/admin-user-endpoints.ts # packages/plugins/plugin-auth/src/auth-plugin.ts
Fixes #25083
Clause-②: yes (narrowing)
Metadata-protocol: none
What this does
This is the services half of the maintainer's ruling 6341690661 on #25083 (objectstack-ai/cloud#3132: hosted environments are managed; self-hosted is unaffected). On a cloud-managed environment,
isCloudManaged()from@objectstack/core(#25306) narrows plugin-auth's local identity doors. With the variable unset, every door behaves exactly as onmain.OS_CLOUD_MANAGED=1)invite_only, at construction and on every mergedapplyConfigPatchdisableSignUparms are forced on and advertised. The bootstrap owner's first sign-up still enters, as under SSO-onlyorganization/invite-member409 RESOURCE_CONFLICT, refused at the route in thebeforehook, resends included. ThebeforeCreateInvitationarm stays as the second lineadmin/create-user,admin/import-users409after the admin check (a non-admin still hears403). Nothing is writtenadmin/set-user-passwordidp_provisionedtarget with409. Anenv_nativetarget is still answered, so the break-glass admin stays editableset-initial-passwordisPlatformAdminUser). Another signed-in caller gets409; an anonymous caller still gets401The four raw routes answer the ADR-0112 envelope through
cloudManagedRefusal(door).invite-memberanswers better-auth's own{ code, message }body, as every org-hook refusal in this package does.For the cloud's twin (objectstack-ai/cloud#3169, the per-environment
set-initial-passwordthe cloud mounts): the package entry now exportscloudManagedRefusal,CLOUD_MANAGED_REFUSAL_CODE,CLOUD_MANAGED_REFUSAL_STATUS,CLOUD_MANAGED_REFUSAL_MESSAGESandCloudManagedDoor.isPlatformAdminUserwas already exported. That twin takes the same arm with the same refusal. This export is the widening inClause-②: yes (narrowing); the door changes are the narrowing.Provenance
domain:servicesseat 1 card thread (claim 6349623693) onclaude/project-thread-i0vfmz, in commitsa6598847,7b0b758cand0366f01d. They paused only because the core accessor and the spec flag had not landed.main, and adds the following:auth-manager.ts's import block;hasPlatformAdminStandingis no longer imported there, because it moved with the actor guards in refactor(plugin-auth): move AuthManager's actor and organization guards into auth-manager-actor-guards.ts (#25224 cut 1) #25273;resetCloudManagedForTesting()on every switch, becauseisCloudManaged()memoizes;features.cloudManagedlanded in feat(spec, platform-objects): declare features.cloudManaged with a hide-when-on gate, and hide the six doors that add people while it is on #25309 and is not re-added here.Contract review (narrowing): PASS, with one should-fix fixed in this PR
The review was run at
CONTRACT_REVIEW_TIERon head5a3d27f6. Its findings and dispositions:invite-member { resend: true }for a pending invitee before it consultsbeforeCreateInvitation. It extendsexpiresAtand re-sends the e-mail, and the console reaches that branch throughsys_invitation'sresend_invitation. Thebeforehook now refuses the route under the managed mode. Pinned by a resend leg (409, expiry unchanged) and its unmanaged control (200, expiry extended). Ablating the new arm reds only the resend leg.set-user-passwordrefusessource === 'idp_provisioned'and admits other values. A nullsourceis answered as local, which matches the ruling's wording ("refusesidp_provisionedtargets").sys_user.sourcedefaults toenv_native.error.codeeverywhere.decideAudienceAdmissiononly. This diff does not touch that path.Verification (head
f37350ae)@objectstack/plugin-auth:check:test-typecheck): exit 0;src/cloud-managed.test.ts: 32/32.Ablations: each was committed first, run under a trap restore, and restored to HEAD.
set-initial-passwordarm offFull dogfood (owed by the
(narrowing)arm), on the final head after building the@objectstack/dogfood...closure (66/66 tasks): Test Files 289 passed | 3 skipped, Tests 2491 passed | 18 skipped.dispatch-gates.mjsderived 135 commands from this diff; 129 can run locally. 120 exit 0. The other 9 are not measured here and are left to CI:check-closing-target-claim,check-partof-closing-keyword,check-single-claim-paths,check-governed-queue-guard.check-partof-closing-keywordwas also run locally against this body; see below.check:dual-build-cjs-loads,check:published-readme-exports.check-issue-citationsruns (plain and--census) whose board read was refused with HTTP 403. The plain run was measured green on the previous head, and this head adds no new issue citation.select-shard-timings-run --self-test, which needs bashmapfile; macOS bash 3.2 lacks it.app-shell, auth: on a cloud-managed environment (features.cloudManaged), Setup → Users says where to invite and marks an unauthorized user, and the console hides invite, import and the recovery-password reminder (objectstack#25083 ruling) objectui#13330 (the console half) remains open and is being built separately. objectstack-ai/cloud#3169 remains open until this lands in the cloud's pin.
🤖 Generated with Claude Code