Skip to content

Commit 4289f44

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-24457-objectui-pin-past-12957
2 parents b089906 + ec951e2 commit 4289f44

47 files changed

Lines changed: 2541 additions & 311 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/plugin-auth": patch
3+
---
4+
5+
fix(plugin-auth): the self-registration grant settles on the registrant's first membership, owned by that organization (ADR-0131 D3)
6+
7+
Clause-②: no
8+
9+
The declared self-registration permission set (`auth.audience.selfRegistrationPermissionSet`) is an assignment, and an assignment is an organization row (ADR-0131 D3). It is now written with the registrant's first membership and owned by that membership's organization, under every posture. It is never written without an organization.
10+
11+
- **`single`: unchanged.** The membership reconciler binds the Default Organization in the same `user.create.after` hook, so a sign-up still ends with one grant, owned by the Default Organization.
12+
- **A wall (`group`, `isolated`).** A self-registrant belongs to no organization at sign-up, so the sign-up writes no `sys_user_permission_set` row. It used to write one with no organization. The grant is held as a pending claim in `sys_verification`, which is deployment-level and survives a restart. The first `sys_member` row written for the user lands exactly one grant, owned by that organization. That row can come from creating an organization (the creator becomes its owner), accepting an invitation, being added by an administrator, or any other membership writer. A later membership writes no second grant.
13+
- **The claim waits 365 days.** A registrant who first joins an organization after that holds only the deployment's baseline. An engine with no middleware seam cannot settle a claim, so there a registrant with no organization is reported ungranted at `error` at sign-up instead.
14+
- Grants already written without an organization by earlier walled sign-ups are not touched here.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: retire a page action row's own `patch` — a row's `operation: 'update'` runs the declared action its `name` names, and the platform action route writes that action's `patch` (#24394)
6+
7+
**BREAKING** — `patch` on a page action row (`action:button`, `action:icon`, and each `action:group` / `action:menu` member of `properties.actions`) is now refused. A row's `operation: 'update'` posts to the platform action route under the row's `name`. The route resolves that declared action by name and writes the action's own `patch`, merged under the request's params. The row's `patch` was never sent: the reference console read it only for its Undo snapshot. Beside `operation: 'update'` a row now needs a `name`, since without one it has no declared action to run and the console refused it before any request. A row's static `params` are written only where that action declares them, and the route refuses any other with a 400 (ADR-0104 D2). `ActionSchema.patch`, the action's own declaration, is unchanged.
8+
9+
Clause-②: yes (narrowing)
10+
11+
Both directions, measured on the built rows. Narrowing: every row `patch` is refused, and a row with `operation: 'update'` and no `name` (an input list or static `params` included) is refused at `name`. Widening: a row with `operation: 'update'` and a `name` but no `patch` and no `params` now parses; before, the rows refused it at `patch` as having nothing to write, though its write is the named action's `patch`.
12+
13+
Measured before removal, at `71fa166e96`: no producer. No page action row in this repository's examples or apps authors `patch` (or any `operation`), and neither does any objectui page row at the pinned `0707b4ae37` or at objectui `main` `4b7ce47`.
14+
15+
## FROM → TO
16+
17+
| you wrote (17.7 and earlier) | write instead |
18+
| --- | --- |
19+
| `operation: 'update'` with a `patch` on a page action row | the values in the `patch` of the declared action the row's `name` names (`defineAction({ name, operation: 'update', patch })`), the row keeping only `operation: 'update'` and that `name` |
20+
| `patch` on a page action row with no `operation` | nothing — delete the key; it was already refused |
21+
| `operation: 'update'` on a page action row with no `name` | a `name` naming the declared `operation: 'update'` action |
22+
23+
**The one-line fix:** move each row's `patch` values onto the declared action its `name` names, then delete the row's `patch`.
24+
25+
**What an author who still writes it sees.** `tsc` fails at a typed authoring site (the rows type the key as retired), and `os validate`, `os build` and `os lint` refuse it as an `error` (`component-props-invalid`) at its own path (`properties.patch`, `properties.actions.N.patch`) with the prescription:
26+
27+
> An `action:button`'s `patch` was removed in @objectstack/spec 18 (ADR-0110 D2) — a page action row's `operation: 'update'` runs the declared action its `name` names, and the platform action route resolves that action by name and writes ITS `patch`, so the row's own `patch` was never sent and never written. Move the values into the `patch` of the action the row's `name` names, then delete the key. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.
28+
29+
A page's own parse keeps its component `properties` as an open bag, so an existing page carrying the key is never refused at load.
30+
31+
## The retirement kit
32+
33+
- **A `retiredKey()` tombstone** at `patch` on `ActionButtonProps`, `ActionIconProps` and the `action:group` / `action:menu` member. `RETIRED_KEYS_BY_MAJOR[18]`: `ui/ActionButtonProps:patch`, `ui/ActionIconProps:patch` (the member's keys have no surface row of their own). The authorable-surface rows read `[RETIRED]`.
34+
- **The rows' half of the declarative-update judge** reads the row's own surface: a row has no "`patch` without `operation`" rule of its own (its tombstone refuses every `patch`), and its "nothing to write" rule asks for the `name`. `ActionSchema`'s half of every rule, and every text it prints, is unchanged.
35+
- **The D2 conversion `page-action-row-patch-removed`** (protocol 18, retired from the load path) deletes the key from every node and member — region-level, nested in a container's `children`, and in a slotted page's named slot — one notice per row. It is scoped by component `type`: the same key name on another component is left alone.
36+
- **The D3 entry `ui-page-action-row-patch-retired`** records the judgement the strip cannot make: when the named action's own `patch` lacks a value the row carried, that value was never written, and adding it to the action changes what the action writes.
37+
- **No deprecation window**, per the project's startup-stage posture.
38+
39+
**Not part of it:** objectui's renderers still forward the row's `patch`, its runner still reads it for the Undo snapshot, and its component registry still lists a `patch` input on `action:button` / `action:icon` (so the tracked `sdui.manifest.json` advertises it to the html-tier page gate until the pin moves). Those reads leave on the objectui side.
40+
41+
⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is published, so this is breaking for consumers no telemetry was consulted for.
42+
43+
<!-- adr-0087: registered page-action-row-patch-removed, ui-page-action-row-patch-retired -->
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/plugin-sharing': minor
3+
'@objectstack/runtime': minor
4+
---
5+
6+
fix(plugin-sharing,runtime)!: a declared sharing rule is read-only and clonable — no rule, new or renamed, can take its name, and it is switched off through the activation ledger, which revokes its grants at once
7+
8+
Clause-②: no (narrowing)
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) no spec key, export, option or stored metadata shape is added, removed or renamed, so `objectstack migrate meta` has nothing to rewrite; what changes is the accept set of two write doors, whose remedy is another name for the caller's rule plus one activation call -->
11+
12+
**BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes.
13+
14+
ADR-0131 D6 charts declared sharing rules under regime C: a rule a package declares is read-only, it is customised by a clone under another name, and it is switched off through the deployment-level activation ledger (`sys_metadata_activation`), like flows, actions, positions and permission sets. Same-name shadowing is retired.
15+
16+
What is refused now:
17+
18+
- `POST /api/v1/sharing/rules` (and `SharingRuleService.defineRule`) creating a rule under a declared rule's name — FROM `201`, the new row replacing the declared rule in its scope, TO `409 RESOURCE_CONFLICT` naming the declared rule. Fix: create your rule under another name, and switch the declared one off with `POST /api/v1/security/_activation/sharing_rule/<name>` and `{ "enabled": false }`.
19+
- A `/data` insert of a `sys_sharing_rule` row under a declared rule's name, or an update that renames a row onto one — FROM `201` / `200`, TO `409 RESOURCE_CONFLICT`. Fix: the same.
20+
21+
What is added:
22+
23+
- **The switch.** `POST /api/v1/security/_activation/sharing_rule/:name` writes the declared rule's activation row, behind the same authority as the other activation doors: `manage_metadata`, and the platform operator under a wall, because the row covers every organization. A declared rule switched off is served nowhere. The write revokes every grant that carries the rule's name in the same request, and switching it back on re-derives them. The name must be a declared rule (`404` otherwise; a rule your organization authored keeps its own `active` field), and with no sharing-rule service bound the door answers `503`.
24+
- **A declaration over a stored row is reported, never made an overlay.** When a package declares a rule whose name a `sys_sharing_rule` row already holds in a scope (a package upgrade, or a copy customised before this release), the row keeps answering as its own rule. The declaration is not served beside it, and that is logged at `error` once per name and scope per process, with the remedy.
25+
26+
Unchanged: rules whose name no declaration holds are created, renamed and switched as before. A stored row that already holds a declared name stays editable as itself. `DELETE /api/v1/sharing/rules/:idOrName` on a declared rule is still `403 PERMISSION_DENIED`, and is now answered the same way when the rule is switched off.

‎content/docs/permissions/system-context.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ One of the largest consumers of the flag;
159159
| 37 | `sys_record_share` reads are **not** self-scoped | Get: tenant-wide share listing without `manage_sharing` | `packages/plugins/plugin-sharing/src/sharing-plugin.ts#buildSharingMiddleware` |
160160
| 38 | Share-link policy `enabled` check bypassed; system callers re-enter under a system context | Get: link **creation** while the policy is off — resolution is **not** bypassed since #14033 (`publicSharing.enabled` is a standing policy held at every redemption): a link minted this way does not resolve until the block is enabled | `packages/plugins/plugin-sharing/src/share-link-service.ts#createLink`, `#revokeLink`, `#listLinks` |
161161
| 39 | Sharing-rule provenance stamp skipped | Lose: the row is not marked as an admin customization — seeder / `defineRule` / boot reconcilers are "the package door" | `packages/plugins/plugin-sharing/src/sharing-rule-provenance.ts#bindRuleProvenanceStamp` |
162-
| 40 | Sharing-rule service write + delete paths return early | Lose: the manage-rules gate on the service surface, and the platform-global-rule delete guard | `packages/plugins/plugin-sharing/src/sharing-rule-service.ts#assertCanManageRules`, `#assertCanDeletePlatformGlobalRule` |
162+
| 40 | Sharing-rule service write + delete paths return early | Lose: the manage-rules gate on the service surface, the platform-global-rule delete guard, and the declared-name guard on `sys_sharing_rule` writes (a new row or a rename taking a declared rule's name, 409) — `defineRule` makes that check itself before its own system write, and the other system writers are the platform's machinery (the ownership ceremony, migrations) | `packages/plugins/plugin-sharing/src/sharing-rule-service.ts#assertCanManageRules`, `#assertCanDeletePlatformGlobalRule`, `#bindDeclaredRuleNameGuard` |
163163

164164
### 4. Approvals, attachments, comments, knowledge
165165

‎content/docs/permissions/tenant-audit-census.mdx‎

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ what moved this page's population from 225 to 227; nothing about the two sites
8383
changed, only whether this instrument could see them.
8484

8585
**The expensive failure direction is a keyword.** Sites whose receiver the author
86-
typed `any` have no type to read, and there are 38 of them — just under a fifth
86+
typed `any` have no type to read, and there are 40 of them — just under a fifth
8787
of the population, concentrated in exactly the seed and bootstrap paths this
8888
control exists for. Scoring an unreadable receiver as "not an engine" would have
8989
dropped every one of them silently, with a clean exit and a smaller number that
@@ -122,7 +122,7 @@ are reported as `undecidable` rather than assumed either way.
122122

123123
The same holds twice over for the context. An options argument spelled as a
124124
literal can be read; one spelled `options`, `{ ...opts }`, or handed through a
125-
forwarding shim cannot, and **53 of the 229 sites are spelled that way**. A
125+
forwarding shim cannot, and **52 of the 231 sites are spelled that way**. A
126126
context resolved from an inline literal or a local `const` can be tested for
127127
`isSystem`; one arriving from a helper call cannot.
128128

@@ -150,8 +150,8 @@ now **0**: nothing on this surface threads a context that provably lacks the fla
150150

151151
**"No tenant context" counted sites it had not read.** An options argument the
152152
walker could not parse was folded into the same bucket as one it had read and
153-
found empty. That published **61 sites "carrying no tenant context at all"**
154-
when 8 said so and 53 were simply unread — an over-claim in the *alarming*
153+
found empty. That published **60 sites "carrying no tenant context at all"**
154+
when 8 said so and 52 were simply unread — an over-claim in the *alarming*
155155
direction, on the very figure this page tells other cards to cite. `carries` is
156156
now three-valued, and an unreadable argument can never contribute to the
157157
provable count.
@@ -187,10 +187,10 @@ reproduce them. Where it disagrees, it disagrees on the page:
187187

188188
| carried figure | where it survives | this census |
189189
| :--- | :--- | ---: |
190-
| 175 write call sites | quoted in the merged changeset | **229** |
191-
| 24 carrying no tenant context | quoted in the merged changeset | **2** provable and tenancy-enabled; **32** more whose options argument is unreadable |
192-
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **158 of 229** decidable, **71** undecidable |
193-
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 125 decidably elevated, 0 decidably not, 96 undecidable |
190+
| 175 write call sites | quoted in the merged changeset | **231** |
191+
| 24 carrying no tenant context | quoted in the merged changeset | **2** provable and tenancy-enabled; **31** more whose options argument is unreadable |
192+
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **160 of 231** decidable, **71** undecidable |
193+
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 128 decidably elevated, 0 decidably not, 95 undecidable |
194194
| 141 and 132, two independent re-derivations | the card that filed this work | — |
195195

196196
**The differences are not reconciled, and deliberately so.** The old census's
@@ -200,21 +200,21 @@ be stated is what this instrument counts, which is written above and re-runnable
200200
at any commit.
201201

202202
Two structural facts do plausibly widen this reading against any hand or regex
203-
one, and both are counted in the generated tables below: the 38 sites reached
203+
one, and both are counted in the generated tables below: the 40 sites reached
204204
through an erased (`any`) receiver, and the 52 that name their object through a
205205
`const` rather than inline. An instrument that read either the way a person does
206206
would report a smaller number and would not say so.
207207

208208
The fourth row is the one worth flagging to anyone citing it. **The 135 / 77%
209209
figure has no surviving corroboration anywhere in the tree.** This census reads
210-
125 of 229 (55%) as decidably elevated, with 96 more whose elevation is a
210+
128 of 231 (55%) as decidably elevated, with 95 more whose elevation is a
211211
run-time fact — so the claim is neither confirmed nor refuted, and the honest
212212
answer is that a static reading cannot settle it.
213213

214-
⇒ **Cite `2 / 229`, and say what it is**: the sites whose options argument was
214+
⇒ **Cite `2 / 231`, and say what it is**: the sites whose options argument was
215215
READ and holds no tenant context, against a decidably tenancy-enabled object.
216216
That is the control's provable yield surface. ⛔ Do not cite it as "the sites
217-
without tenant context" — **32 further sites** have an options argument this
217+
without tenant context" — **31 further sites** have an options argument this
218218
cannot read, and they are neither in nor out.
219219

220220
{/* BEGIN GENERATED: tenant-audit-census (scripts/tenant-audit-census.mjs) — DO NOT EDIT */}
@@ -223,28 +223,28 @@ cannot read, and they are neither in nor out.
223223

224224
| what | count |
225225
| :--- | ---: |
226-
| write call sites on the application surface | **229** |
227-
| …whose object name is statically decidable | 158 |
226+
| write call sites on the application surface | **231** |
227+
| …whose object name is statically decidable | 160 |
228228
| …whose object name is chosen at run time | 71 |
229-
| …against an object with tenancy ENABLED | 157 |
229+
| …against an object with tenancy ENABLED | 159 |
230230
| …against an object that declares tenancy off | 1 |
231-
| threading a tenant context | 168 |
231+
| threading a tenant context | 171 |
232232
| PROVABLY carrying none (options read, no context key) | **8** |
233233
| …of those, against a decidably tenancy-enabled object | **2** |
234-
| options argument UNREADABLE — may or may not carry one | 53 |
235-
| …of those, against a decidably tenancy-enabled object | 32 |
236-
| threading a decidably ELEVATED (`isSystem`) context | 125 |
234+
| options argument UNREADABLE — may or may not carry one | 52 |
235+
| …of those, against a decidably tenancy-enabled object | 31 |
236+
| threading a decidably ELEVATED (`isSystem`) context | 128 |
237237
| threading a context that is decidably NOT elevated | 0 |
238-
| threading a context whose elevation is a run-time fact | 96 |
238+
| threading a context whose elevation is a run-time fact | 95 |
239239

240240
| how the instrument reached the site | count |
241241
| :--- | ---: |
242242
| receiver carried a readable engine type | 191 |
243-
| receiver erased, placed by the object NAME | 28 |
243+
| receiver erased, placed by the object NAME | 30 |
244244
| receiver erased, placed by an `object: string` PARAMETER | 5 |
245245
| receiver erased, placed by an `UNTYPED_RECEIVERS` row | 5 |
246246

247-
| object name spelled inline | 106 |
247+
| object name spelled inline | 108 |
248248
| object name spelled through a `const` | 52 |
249249
| object name is an `object: string` parameter | 7 |
250250
| object name is some other run-time expression | 64 |
@@ -297,13 +297,13 @@ holds still. They are required to be HERE and to say WHEN they were true;
297297
their values are not compared. The reasoning, and the measurement behind it,
298298
are in `scripts/check-tenant-audit-census.mjs`.
299299

300-
Measured on 2026-10-16 at `22b217924`.
300+
Measured on 2026-10-19 at `2623d6d6c`.
301301

302302
| corpus scale (not enforced) | count |
303303
| :--- | ---: |
304-
| tracked non-test sources scanned | 657 |
304+
| tracked non-test sources scanned | 670 |
305305
| engine-shaped types recognised | 79 |
306306
| declared objects in the registry | 116 |
307-
| same-named calls subtracted as non-engine | 165 |
307+
| same-named calls subtracted as non-engine | 166 |
308308

309309
{/* END GENERATED: tenant-audit-census */}

0 commit comments

Comments
 (0)