Repository navigation
Commit f573976
fix(platform-objects): sys_organization admin actions follow the membership reach (update/delete rows) (#24399)
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_TIER` record 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`, statement
`organization: ['update']`, grades owner and admin.
- `delete_organization`: `/organization/delete`, statement
`organization: ['delete']`, grade owner only.
- plugin-auth's `membership-reach-table.test.ts` iterates every row, so
both new rows are held equal to the installed vendor door with no edit
to that test: the endpoint's `hasPermission` statement, and the grades
whose registered statements pass it.
- `packages/platform-objects/src/identity/sys-organization.object.ts`:
- `update_organization` and `rename_organization` declare
`requiresMembershipReach: 'update_organization'`.
- `delete_organization` declares `requiresMembershipReach:
'delete_organization'`.
- `multiOrgEnabled` stays the last term of each predicate. `invite_user`
is unchanged. `leave_organization`, `set_active_organization`,
`create_organization` and `change_slug` keep 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.
- Pins:
- `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 equals
`update_organization`'s.
- New `packages/qa/dogfood/test/org-record-admin-reach.dogfood.test.ts`
(below).
- Generated: `content/docs/references/ui/action.mdx`, through
`check:generated --fix`.
- Changesets: `@objectstack/spec` minor, `@objectstack/platform-objects`
minor.
No door changed, no error code was added, and `leave_organization` stays
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`; an `admin` and a
plain `member` join it, and each principal activates it. For each
principal the file reads the served session (`positions`), the served
`/auth/config` features (`multiOrgEnabled: true`) and the served `GET
/meta/object/sys_organization`, then evaluates each served `visible`
with `@objectstack/formula`'s `celEngine`. It then calls the doors.
Offered per grade (the expected sets are written out in the test):
| grade | before (tree `1eae21f`, dist built from it) | after |
|:--|:--|:--|
| owner | everything | everything |
| admin | everything, **including Delete Organization** | create, set
active, leave, invite, edit, rename |
| member | create, set active, leave, **edit, rename, delete** | create,
set active, leave |
`change_slug` was 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`:
- member: `403 YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION`.
- admin and owner: `200`, and the stored name changed.
- `POST /auth/organization/delete`:
- member and admin: `403
YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION`, and the row still
exists.
- owner: `200`, and the row is gone.
- Every `type: 'api'` target the record serves, called once as the plain
member with an unknown `organizationId`:
| action | answer |
|:--|:--|
| `create_organization` | `400 VALIDATION_ERROR` |
| `update_organization` | `400 VALIDATION_ERROR` |
| `delete_organization` | `400 USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION`
|
| `set_active_organization` | `403
USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION` |
| `invite_user` | `400 VALIDATION_ERROR` |
| `leave_organization` | `400 MEMBER_NOT_FOUND` |
| `rename_organization` | `400 VALIDATION_ERROR` |
| **`change_slug`** | **`404 ENDPOINT_NOT_FOUND`** |
Runs:
- Before (fix not applied): the new file fails 4 of 7. Admin is offered
delete; member is offered update, rename and delete; `change_slug`
answers 404. The two door tests pass.
- After, on `dde114aba`: 3 files (`org-record-admin-reach`,
`org-admin-affordance-reach`, `organization-rename-door`), 23 of 23
pass. The final file holds `change_slug` out by name, as `UNDECIDED`, 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.
1. **The gate.** Delete `requiresMembershipReach: 'delete_organization'`
from the object (anchor 1 → 0), then rebuild `platform-objects`.
- `ablation-dist-preflight --absent` passes.
- The `delete_organization` row of `platform-objects.test.ts` turns red:
`'features.multiOrgEnabled != false'` against the composed predicate.
- The dogfood file turns red, 2 failures: admin and member are now
offered delete.
- Restore: blob equals HEAD and `git diff HEAD` is empty. The rebuilt
dist's preflight finds the marker in 6 files.
- This ablation ran while `change_slug` was briefly removed on this
branch; the delete gate it mutates is byte-identical on the final head.
2. **The row.** Widen `delete_organization`'s grades to `OWNER_ADMIN`,
then rebuild `spec`.
- plugin-auth's `membership-reach-table.test.ts` turns red:
`delete_organization: the grades equal …`, `[ 'owner', 'admin' ]`
against the door's `[ 'owner' ]`, 1 of 27.
- Restore: blob equals HEAD. After the rebuild, the dist reads
`["owner"]`.
3. **`change_slug`'s 404.** The before run is its reading. This PR does
not change it.
## Verification record (head `dde114aba`, after merging `origin/main`
`584b4225e`)
- `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.
- Dogfood: 3 files, 23 passed, as above.
- Typecheck exit codes: spec 0, platform-objects 0, dogfood 0.
- `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 --commands` on this head: 112
commands, each run and each exit captured before any pipe; all 112 exit
0.
- Two first refused with exit 3, PREREQUISITE NOT MET (unbuilt
packages): `check:skill-examples` and `check:dual-build-cjs-loads`.
After building the named packages, both exit 0.
- `check:slot-lookup` was cut off by the batch timeout once; it ran
alone and exits 0.
- `--ran` reconciliation: 112 derived, 112 run, 0 NOT-MEASURED (a
derived zero, since every line carries its exit code).
- Local-only families: `check:dev-prereqs` 0,
`check:skill-frame-freshness` 0.
## #24318: why `change_slug` is not changed here
Measured:
- No package in this repository serves
`/api/v1/cloud/organizations/{id}/change-slug`. On this composition it
answers `404 ENDPOINT_NOT_FOUND`.
- No existing public signal tells a composition with the orchestrator
apart from one without it. `PUBLIC_AUTH_FEATURES` carries 11 keys, none
of them about cloud, and nothing else in `/auth/config` does either.
- **Measured probe:** the showcase booted with one extra top-level
action, `objectName: 'sys_organization'`, `locations:
['record_header']`. The served `GET /meta/object/sys_organization` did
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: removing `change_slug` from the
platform object would take the button off cloud's page, and cloud would
have no declared way to put it back. `objectExtensions` refuses
`actions` by design.
The three shapes on the card, by the four axes:
- **(A) A new `PUBLIC_AUTH_FEATURES` key 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.
- **(B) The composition that serves the door declares the action, and
the platform object stops declaring it.** It matches "one route, one
owner" and adds no key. But it needs a platform mechanism that attaches
a bound action to an object another package owns, which does not exist
(the probe above). Without that, cloud loses the button.
- **(C) The open-source composition serves the route.** That would widen
scope into the orchestrator's sys_domain and reservation semantics.
Startup scope: no.
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_slug` keeps `multiOrgEnabled` alone. It
gets no reach term either: its door is cloud's `org-admin`, outside
better-auth, so no reach row describes it.
## Acceptance notes
- The two reach rows are a cross-domain exception, carried under the
claim: `packages/spec` is `domain:spec`'s surface, declared to it on
#6017 by the seat.
- `set_active_organization`, `leave_organization` and
`create_organization` carry 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](https://claude.ai/code/session_01JkPx97R4MbdoSQcC5HgGdb)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01JkPx97R4MbdoSQcC5HgGdb
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d79dc48 commit f573976
9 files changed
Lines changed: 288 additions & 11 deletions
File tree
- .changeset
- content/docs/references/ui
- packages
- platform-objects/src
- identity
- pages
- qa/dogfood/test
- spec/src/identity
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
58 | | - | |
| 58 | + | |
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
| |||
91 | 91 | | |
92 | 92 | | |
93 | 93 | | |
| 94 | + | |
| 95 | + | |
94 | 96 | | |
95 | 97 | | |
96 | 98 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | | - | |
| 15 | + | |
| 16 | + | |
16 | 17 | | |
17 | 18 | | |
18 | 19 | | |
| |||
25 | 26 | | |
26 | 27 | | |
27 | 28 | | |
| 29 | + | |
28 | 30 | | |
29 | 31 | | |
30 | 32 | | |
| |||
97 | 99 | | |
98 | 100 | | |
99 | 101 | | |
100 | | - | |
101 | | - | |
| 102 | + | |
| 103 | + | |
102 | 104 | | |
103 | 105 | | |
104 | 106 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
97 | 97 | | |
98 | 98 | | |
99 | 99 | | |
100 | | - | |
| 100 | + | |
| 101 | + | |
101 | 102 | | |
| 103 | + | |
102 | 104 | | |
103 | 105 | | |
104 | 106 | | |
| |||
118 | 120 | | |
119 | 121 | | |
120 | 122 | | |
| 123 | + | |
| 124 | + | |
121 | 125 | | |
122 | 126 | | |
123 | 127 | | |
| |||
200 | 204 | | |
201 | 205 | | |
202 | 206 | | |
203 | | - | |
| 207 | + | |
204 | 208 | | |
205 | 209 | | |
206 | 210 | | |
| |||
242 | 246 | | |
243 | 247 | | |
244 | 248 | | |
| 249 | + | |
| 250 | + | |
245 | 251 | | |
246 | 252 | | |
247 | 253 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | | - | |
16 | | - | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
585 | 585 | | |
586 | 586 | | |
587 | 587 | | |
588 | | - | |
589 | | - | |
590 | | - | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
591 | 593 | | |
592 | 594 | | |
593 | 595 | | |
| |||
0 commit comments