Repository navigation
fix(platform-objects): sys_organization admin actions follow the membership reach (update/delete rows) - #24399
Conversation
…grade and their doors Claude-Session: https://claude.ai/code/session_01JkPx97R4MbdoSQcC5HgGdb Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkPx97R4MbdoSQcC5HgGdb Co-authored-by: Claude <noreply@anthropic.com>
…ership reach; change_slug leaves the platform object Claude-Session: https://claude.ai/code/session_01JkPx97R4MbdoSQcC5HgGdb Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkPx97R4MbdoSQcC5HgGdb Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkPx97R4MbdoSQcC5HgGdb Co-authored-by: Claude <noreply@anthropic.com>
…ates Claude-Session: https://claude.ai/code/session_01JkPx97R4MbdoSQcC5HgGdb Co-authored-by: Claude <noreply@anthropic.com>
…decision A top-level action bound to sys_organization from another stack is not attached to the served object, so moving change_slug to the composition that serves its door has no working path today. Claude-Session: https://claude.ai/code/session_01JkPx97R4MbdoSQcC5HgGdb Co-authored-by: Claude <noreply@anthropic.com>
…g-admin-actions-reach
📓 Docs Drift CheckThis PR changes 2 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 143 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 d19a65fe5e9dda099408514a7ad417e5f53a6f32 && git checkout d19a65fe5e9dda099408514a7ad417e5f53a6f32
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d8e11b58e784bb7c2208e3533ab4710921012b1e dde114aba6539f8456d6bce0a6f8b988e96aff62 && git checkout -B drift-repro d8e11b58e784bb7c2208e3533ab4710921012b1e && git merge --no-ff dde114aba6539f8456d6bce0a6f8b988e96aff62
node scripts/docs-audit/affected-docs.mjs --json d8e11b58e784bb7c2208e3533ab4710921012b1e
|
Contract reviewServed-tier: ① Derived judgmentsThe two reach rows — right. better-auth 1.7.3 The enum — derived, not hand-listed. The lowered predicates — right, within the ruled mechanism. Unchanged actions — truly unchanged. The hunks touch The dogfood pins — real controls. The expected sets are written out per grade ( ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 ·
|
| Item | Reading |
|---|---|
| Shape | Draft PR → main. The body opens Fixes #24384 and says #24318 remains open. 9 files, +288 / −11. Not governed (check-governed-merges.mjs --pr 24399: 0 of 9 paths hit the register; 299 changed lines). |
| Scope | packages/spec/src/identity/membership-reach.ts: two reach rows, update_organization (/organization/update, organization:update, owner and admin) and delete_organization (/organization/delete, organization:delete, owner), declared to domain:spec (6254547379). sys-organization.object.ts: update_organization and rename_organization declare requiresMembershipReach: 'update_organization', and delete_organization declares 'delete_organization'. The page's header comment is corrected (comment only). Pins in platform-objects, and a new dogfood file declared to domain:cli (6255721514, 6255725489). The generated action.mdx, declared to domain:devx (6255732064, 6255735381). One changeset each for @objectstack/spec and @objectstack/platform-objects. change_slug, invite_user, leave_organization, set_active_organization and create_organization have no hunk. |
| Seat's own read | At the head: both rows read as above. MEMBERSHIP_REACH_NAMES is Object.keys(MEMBERSHIP_REACH) and feeds ActionSchema's z.enum (action.zod.ts:1508), so the accept set follows the table. ActionSchema lowers the grade term first and multiOrgEnabled second (:2174). positions come from the active organisation's membership (resolve-authz-context.ts:1414). |
| Measured | On a booted multi-organization showcase, before the fix: a plain member was offered Edit, Rename and Delete, and an admin was offered Delete; every click answered 403 from better-auth. After the fix: an owner is offered everything; an admin is offered create, set active, leave, invite, edit and rename; a member is offered create, set active and leave. The doors were measured on the same boot. Update: member 403, admin and owner 200. Delete: member and admin 403 with the row kept, owner 200 with the row gone. change_slug still answers 404 and is held out by name. |
| Review face | The published schema. CONTRACT_REVIEW_TIER record 6255881407 on this head is PASS. The seat verified its central claims at the head (the row above). |
| Declaration | Clause-②: yes (widening) on the claim, the PR body and both changesets. The PR body's stale "possibly a feature key" was trimmed after the record (a body edit, no new head). Record ② finds the spec minor exact. It finds the platform-objects minor over-declared, since 17.7.0 shipped #21795's same shape as a patch, but harmless; it stays as is. |
| Pins and ablation | The lowering matrix's three rows carry the grade term. Rename and update pin the same gate. plugin-auth's reach-table test iterates every row against better-auth's door unedited (27 cases). The dogfood file writes out each grade's expected set and calls both doors. Ablation 1 (the delete gate removed) turned the matrix row and 2 dogfood cases red. Ablation 2 (delete widened to owner and admin) turned the vendor-equality pin red. Each restore read back as the HEAD blob. |
| Gates | Dev at dde114aba6: 112 derived, 112 run, 0 NOT MEASURED. Typecheck exit 0 for spec, platform-objects and the dogfood package. check:generated: all 14 artifacts up to date. |
| CI | 41 check-runs on dde114aba6: 36 success, 5 skipped, 0 failed. The skips are the rostered path and opt-in skips. Test Core 1/6–6/6, every Type Check lane, Lint & Repo Gates, Check Changeset and Governed Surface Queue Guard are green. |
Disposition:
open_questions:change_slug's shape: B by the four axes, behind spec(object-extension): the extension refusal saysdefineStack()attaches a top-level action withobjectName: '<target>', but for another package's object it is never attached and the served object lacks it #24400, the attach mechanism measured missing. Release 6255698861 moved platform-objects sys_organization: change_slug targets a cloud-only route, so on an open-source multi-organization composition the record page offers Change Slug and the click answers 404 #24318 topm:blocked. This PR rightly leaveschange_slugas it is onmain.- No reach term for
change_slugmeanwhile: accepted. Its door is cloud's, outside better-auth, and the reach table books vendor doors only.
deviations: all accepted, as record ③ judges them:Fixes #24318dropped, per os-dev;- the detached shared checkout, with no edit made there;
- the B-now commits reverted, so the net diff carries neither;
- the throwaway probe, in no commit;
- the model-free trailer pair;
- the pushes counted as one stroke;
- ablation 1 run on an intermediate tree whose mutated line is identical on the head.
out_of_scope_findings: the cross-package bound action is filed as spec(object-extension): the extension refusal saysdefineStack()attaches a top-level action withobjectName: '<target>', but for another package's object it is never attached and the served object lacks it #24400 (gate ①, class b).- Record ③, below the filing gate:
positionsare projected from the active organisation, while both doors check the membership in the record's organisation. A principal who holds different grades in two organisations can therefore be offered a button on the other organisation's row that the door refuses, or not offered one it admits. This is the same mechanisminvite_userand the ten existing rows carry (ADR-0108 D1). It was read in code only and no public door was measured, so it has noreach:and is no card. It goes into the seat post as a below-gate pointer.
Landing: ordinary queue (pr_ready + automerge_enable through the relay). Fixes #24384 then closes the card; #24318 stays pm:blocked.
Generated by Claude Code
Fixes #24384
#24318 remains open: this PR does not change
change_slug. Its shape needs a decision, set out under "#24318" below.Clause-②: yes (widening) (
@objectstack/spec's published reach table gains two rows, while the console offers fewer actions;CONTRACT_REVIEW_TIERrecord 6255881407, PASS, on the published-schema face)What changes
packages/spec/src/identity/membership-reach.ts: the reach table (MEMBERSHIP_REACH) gains two rows, both read off better-auth's own default role statements:update_organization:/organization/update, statementorganization: ['update'], grades owner and admin.delete_organization:/organization/delete, statementorganization: ['delete'], grade owner only.membership-reach-table.test.tsiterates every row, so both new rows are held equal to the installed vendor door with no edit to that test: the endpoint'shasPermissionstatement, and the grades whose registered statements pass it.packages/platform-objects/src/identity/sys-organization.object.ts:update_organizationandrename_organizationdeclarerequiresMembershipReach: 'update_organization'.delete_organizationdeclaresrequiresMembershipReach: 'delete_organization'.multiOrgEnabledstays the last term of each predicate.invite_useris unchanged.leave_organization,set_active_organization,create_organizationandchange_slugkeep no grade term.packages/platform-objects/src/pages/sys-organization.page.ts: the stale header-actions comment now names the header as it is laid out (fold-in note 6249926047). Comment only.platform-objects.test.ts: the lowering matrix's three rows now carry the grade term.sys-organization-rename-action.test.ts: the rename's gate equalsupdate_organization's.packages/qa/dogfood/test/org-record-admin-reach.dogfood.test.ts(below).content/docs/references/ui/action.mdx, throughcheck:generated --fix.@objectstack/specminor,@objectstack/platform-objectsminor.No door changed, no error code was added, and
leave_organizationstays ungated.Measured: M1 before and after (booted showcase,
multiTenant: 'posture-only')The new dogfood file boots a multi-organization showcase. An owner creates the organisation through
organization/create; anadminand a plainmemberjoin it, and each principal activates it. For each principal the file reads the served session (positions), the served/auth/configfeatures (multiOrgEnabled: true) and the servedGET /meta/object/sys_organization, then evaluates each servedvisiblewith@objectstack/formula'scelEngine. It then calls the doors.Offered per grade (the expected sets are written out in the test):
1eae21f, dist built from it)change_slugwas offered to every grade before and is still offered to every grade (unchanged; see #24318).The doors' answers. Before this run they were NOT MEASURED; they are measured now, on the same boot:
POST /auth/organization/update:403 YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION.200, and the stored name changed.POST /auth/organization/delete:403 YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION, and the row still exists.200, and the row is gone.Every
type: 'api'target the record serves, called once as the plain member with an unknownorganizationId:create_organization400 VALIDATION_ERRORupdate_organization400 VALIDATION_ERRORdelete_organization400 USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATIONset_active_organization403 USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATIONinvite_user400 VALIDATION_ERRORleave_organization400 MEMBER_NOT_FOUNDrename_organization400 VALIDATION_ERRORchange_slug404 ENDPOINT_NOT_FOUNDRuns:
change_sluganswers 404. The two door tests pass.dde114aba: 3 files (org-record-admin-reach,org-admin-affordance-reach,organization-rename-door), 23 of 23 pass. The final file holdschange_slugout by name, asUNDECIDED, so the day its shape changes, the test changes with it.Ablations (one per mechanism,
scripts/ablation-replace.mjs, dist preflighted)Mutation landing, restore and dist presence were checked on disk for each.
requiresMembershipReach: 'delete_organization'from the object (anchor 1 → 0), then rebuildplatform-objects.ablation-dist-preflight --absentpasses.delete_organizationrow ofplatform-objects.test.tsturns red:'features.multiOrgEnabled != false'against the composed predicate.git diff HEADis empty. The rebuilt dist's preflight finds the marker in 6 files.change_slugwas briefly removed on this branch; the delete gate it mutates is byte-identical on the final head.delete_organization's grades toOWNER_ADMIN, then rebuildspec.membership-reach-table.test.tsturns red:delete_organization: the grades equal …,[ 'owner', 'admin' ]against the door's[ 'owner' ], 1 of 27.["owner"].change_slug's 404. The before run is its reading. This PR does not change it.Verification record (head
dde114aba, after mergingorigin/main584b4225e)pnpm --filter @objectstack/spec test: 695 files, 21238 passed, 1 todo.pnpm --filter @objectstack/platform-objects test: 76 files, 1176 passed.pnpm --filter @objectstack/plugin-auth exec vitest run src/membership-reach-table.test.ts src/public-feature-registry.test.ts: 32 passed.pnpm --filter @objectstack/spec check:generated: all 14 artifacts up to date, after a rebuild on the merged tree.node scripts/pm/dispatch-gates.mjs --commandson this head: 112 commands, each run and each exit captured before any pipe; all 112 exit 0.check:skill-examplesandcheck:dual-build-cjs-loads. After building the named packages, both exit 0.check:slot-lookupwas cut off by the batch timeout once; it ran alone and exits 0.--ranreconciliation: 112 derived, 112 run, 0 NOT-MEASURED (a derived zero, since every line carries its exit code).check:dev-prereqs0,check:skill-frame-freshness0.#24318: why
change_slugis not changed hereMeasured:
/api/v1/cloud/organizations/{id}/change-slug. On this composition it answers404 ENDPOINT_NOT_FOUND.PUBLIC_AUTH_FEATUREScarries 11 keys, none of them about cloud, and nothing else in/auth/configdoes either.objectName: 'sys_organization',locations: ['record_header']. The servedGET /meta/object/sys_organizationdid not carry it; it listed the platform object's own actions only.mergeActionsIntoObjects(stack.zod.ts) attaches a bound action only to objects in the same stack. So the "cloud declares its own action" shape has no working route today: removingchange_slugfrom the platform object would take the button off cloud's page, and cloud would have no declared way to put it back.objectExtensionsrefusesactionsby design.The three shapes on the card, by the four axes:
PUBLIC_AUTH_FEATURESkey that only the orchestrator's composition sets. Business need: the one consumer is cloud. Long term: it puts a cloud-only key into the open-source contract, and it needs a plugin-auth producer and a drift-guard row as well as the spec row. AI safety: it is loud only if cloud remembers to set it. Startup scope: it is a new gate, which defaults to no unless the maintainer names it.Recommendation: (B), with the attach mechanism as its prerequisite. The maintainer picks, because (A) adds a gate and (B) adds a platform mechanism. Until then
change_slugkeepsmultiOrgEnabledalone. It gets no reach term either: its door is cloud'sorg-admin, outside better-auth, so no reach row describes it.Acceptance notes
packages/specisdomain:spec's surface, declared to it on [PM seat] domain:spec — 🟢 os-warren #6017 by the seat.set_active_organization,leave_organizationandcreate_organizationcarry no grade term. Every grade may switch, leave or create; the doors were measured answering their own refusals, not a 404.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01JkPx97R4MbdoSQcC5HgGdb