Skip to content

Commit f64673b

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-22792-explain-asker-gate
2 parents 2de8d8a + 69d4218 commit f64673b

135 files changed

Lines changed: 5652 additions & 2747 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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/client": patch
4+
"@objectstack/rest": patch
5+
"@objectstack/cli": patch
6+
---
7+
8+
docs: the suggested-binding acceptance is deployment-level and takes effect
9+
10+
Clause-②: no
11+
12+
Text only. These are the published sentences that describe what confirming a package's `isDefault` suggestion does. They now match the behaviour `@objectstack/plugin-security` ships in this release.
13+
14+
- `@objectstack/spec`: `ISecurityService.listAudienceBindingSuggestions`, `confirmAudienceBindingSuggestion` and `dismissAudienceBindingSuggestion` keep "writes the binding" for the confirm. The text now also says what that means: a `manage_metadata` holder accepts once for the deployment, `everyone`'s definition is derived again, and a dismiss on an accepted suggestion revokes it. The package-level `isDefault` note keeps "an admin confirms in Setup" and adds who that admin is.
15+
- `@objectstack/client`: the `security.suggestedBindings.confirm` and `.dismiss` TSDoc.
16+
- `@objectstack/rest`: the route summaries of the confirm and dismiss routes. Paths and envelopes are unchanged.
17+
- `@objectstack/cli`: the ownership-inventory citation for `sys_audience_binding_suggestion` now says the record is deployment-level and owned by the Default Organization.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
"@objectstack/plugin-security": minor
3+
---
4+
5+
feat(plugin-security)!: accepting a package's `isDefault` suggestion takes effect for the whole deployment
6+
7+
Clause-②: yes (narrowing)
8+
9+
**BREAKING.** A package's `isDefault: true` permission set now reaches every signed-in member through ONE deployment-level acceptance, and the acceptance takes effect. Before this release a confirm wrote a `sys_position_permission_set` row that granted nothing.
10+
11+
- **What an accept does.** `POST /api/v1/security/suggested-bindings/:id/confirm` records the decision once for the deployment, and the security plugin derives the `everyone` anchor's definition again as the baseline plus every accepted set. The derivation also runs at boot, on uninstall and on a package-door publish. A member's reads of the package's objects change on the next request. `bindingCreated` is `true` when the definition did not name the set before.
12+
- **Who may decide (narrowing).** All three routes now need the `manage_metadata` capability, and under a `group` or `isolated` tenancy posture the platform operator. Before this release a tenant administrator (the `*` wildcard holder) listed, confirmed and dismissed suggestions for their organization. That caller is now refused `403 PERMISSION_DENIED`. Remedy: a `manage_metadata` holder decides, once for the deployment.
13+
- **The record is the deployment's.** `sys_audience_binding_suggestion` rows are owned by the Default Organization. A deployment with no Default Organization (several organizations, none with slug `default`) answers `409 SUGGESTION_STATE` with the remedy.
14+
- **Rows written before this release are never promoted.** A Default Organization row a tenant administrator confirmed or dismissed is set back to `pending`, unless the baseline already names the set. A row another organization owns, or none, is not read. Boot logs a warning with the count. Such rows granted nothing before this release and grant nothing after it.
15+
- **The high-risk gate is on the definition (ADR-0090 D7).** Accepting a set that carries a bit the `everyone` anchor forbids answers `403 PERMISSION_DENIED`. If a publish later adds such a bit to an accepted set, the derivation withholds it and logs an `error` that names the set.
16+
- **Revoke.** `POST …/:id/dismiss` on an accepted suggestion revokes it, and `everyone` is derived again without the set. A set that only the baseline names (`confirmed`, no resolver) cannot be dismissed (`409`).
17+
- **Only `everyone`.** A `guest`-anchor suggestion is refused `409`, because accepting it would record a grant that never takes effect.
18+
- **The anchors stay sealed.** No caller writes `everyone`'s definition. An acceptance changes the derivation's input, and the plugin re-declares the definition. Reconciliation reads that definition and no binding row.
19+
- **Declarations come from the security catalog.** A suggestion is offered for an `isDefault` set that the catalog serves with an owning package, which is the source the authorization resolver reads. The module no longer reads `sys_position`, `sys_permission_set` or `sys_position_permission_set`. It also no longer materializes a set.
20+
- **New field:** `sys_audience_binding_suggestion.deployment_decision`, a boolean that is `true` when a `manage_metadata` holder resolved the row.
21+
22+
Exported TypeScript surface (the compiler names each caller):
23+
24+
- Removed: `reapOrganizationLessSuggestions`, `listSuggestionOrganizationIds` and the `SuggestionReconcileScope` type. Nothing replaces them, because the record is no longer per organization.
25+
- `reconcileAudienceBindingSuggestions` and `syncAudienceBindingSuggestions` now take `(ql, logger)`. The metadata, scope and organization parameters are gone.
26+
- `SuggestionReconcileOutcome` reports `unread` in place of `organizations` and `reaped`. `SuggestionSyncOutcome` gains `reopened`.
27+
- `SuggestionDeps` drops `resolveSets` and takes `posture` and `rederiveEveryone`.
28+
- New: `readAcceptedEveryoneSets(ql, metadata)` and the `WithheldAcceptance` type.
29+
30+
<!-- adr-0087: not-required (runtime-interface-only packages/plugins/plugin-security/src/suggested-audience-bindings.ts#SuggestionDeps, packages/plugins/plugin-security/src/suggested-audience-bindings.ts#SuggestionReconcileOutcome) TypeScript service plumbing with no metadata surface; the compiler names every caller, and no stored row or authored key changes shape (the new object field defaults to false) -->
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`positionForm`: the `permissionSets` row now declares `widget: 'ref-multi:permission'` in place of `type: 'tags'`. ADR-0131 D4 resolves a reference to a declared item by machine name, registry-first, so the row names the permission-set registry (metadata type `permission`) for the designer to offer the declared sets and commit their names, instead of a free-text tag box where a name that does not exist can be typed.
6+
7+
Clause-②: no
8+
9+
- **The key.** `ref-multi:` followed by a metadata type is the list form of the forms' `ref:` pickers (`ref:object`, `ref:dataset`, `ref:component`, all single-valued). A distinct key, as `field-ref` / `field-multi` already are, because a single `ref:` picker writes one string and the widget registry declares one labelling per key.
10+
- **Which renderer honours it.** A console whose metadata-admin widget registry has no `ref-multi:permission` entry renders this row as its announced fallback: a JSON editor with the note that no custom renderer is registered. The stored value is the same list of names either way.
11+
- ⛔ Only this one form row changes. No schema, parse, export or accept-set change: `PositionSchema.permissionSets` stays a list of snake_case names, and `FormField.widget` stays a free string.

‎.changeset/22725-approvals-unstaffed-arm.md‎

Lines changed: 0 additions & 16 deletions
This file was deleted.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): the bulk action param's rejection text names the promoted field-backed route instead of saying the bulk surface has none (#22780)
6+
7+
Clause-②: no
8+
9+
`BulkActionParamSchema` still refuses `field`, and the widget-config keys, on an authored `bulkActionDefs[].params` entry. An authored param reaches the bulk dialog as written, and nothing on that path resolves it against the object's field definitions. What changed is the reason and the remedy in two rejection messages, which said the bulk surface has no field-backed param route at all. The console now has one. An action the object declares, named in a list view's `bulkActions`, is promoted with its params, and a field-backed param on it (`{ field }`, plus `objectOverride`) is resolved through the single-record dialog's resolver when the bulk dialog opens.
10+
11+
- **`field`.** The rejection now offers two fixes. Declare the param inline (`name` + `type`, plus `object` and optionally `labelField` for a picker). Or declare the action on the object with the field-backed param and name it in `bulkActions`. The promoted form runs the action once per selected record.
12+
- **Widget-config keys** (`min`, `max`, `step`, `accept`, `lookupFilters` and the rest of that family). The rejection states what the promoted route carries from the field: `type`, `label`, `options`, `required`, `description` (as the help text), `placeholder`, `defaultValue`, `multiple`, `accept` and `maxSize`, plus a reference field's `reference`, `displayField` and `dependsOn`. Of the refused keys, only `accept` and `maxSize` reach the bulk dialog that way. `lookupFilters`, `lookupColumns`, `lookupPageSize` and `descriptionField` are dropped on the bulk path, and the rest are never read off the field.
13+
14+
Nothing to migrate: the accepted and refused keys are unchanged. Only the text of the two rejection messages, and two source comments, moved.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/objectql': minor
3+
---
4+
5+
An `after*` hook whose `onError` is `abort` (the default) now rolls its write back on the default write door, as `HookSchema.onError` says ("abort: Rollback transaction (if blocking)")
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) A change to what the write door stores when a hook aborts: no key of the hook schema or of any other metadata schema is removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone. A hook that loaded before keeps its authored shape; what changes is that the refused write no longer leaves its row behind. No ADR-0087 id covers this rule and this diff adds none. -->
10+
11+
**BREAKING** stored-outcome narrowing, shipped as `minor` under the repo's launch-window convention for breaking changes (ADR-0087, the ratified pre-launch launch-window exemption, as amended 2026-09-13, #18003).
12+
13+
- **FROM:** `insert` / `update` / `delete` (REST `createData` / `updateData` / `deleteData`, `ctx.api`, the engine door) on an object with a blocking (not `async`) `after*` hook bound from metadata whose `onError` is `abort` or omitted. When that hook threw, the caller was refused, but the row stayed stored, changed or deleted, and so did every write the hook chain had already made through `ctx.api`. A client that retried the refused create made a duplicate.
14+
- **TO:** that write runs inside a unit of work, so the abort rolls back the row and the hook chain's engine writes together. The caller is refused exactly as before. Hook count and order are unchanged.
15+
- **Unchanged:** a write already inside a unit (a caller's `transaction()`, an atomic batch or cascade) joins it, and that unit's owner still decides. No unit is opened for an `onError: 'log'` hook, an `async: true` hook, a hook registered in code with `registerHook` (no declared `onError`), a write with `skipAutomations`, an object whose datasource is not the default one, or a default driver with no transactions (ADR-0119 D1, such as a libSQL remote transport). On those the row stays stored after an abort, as before.
16+
- **Inside the new unit, the rules of any unit apply.** An `after*` hook on such an object that writes a business row to an object on another datasource is now refused with `CrossDatasourceTransactionWriteError`, which the abort turns into a refused write. A MongoDB default store needs a replica set for this unit, as it does for `transaction()` already. If either applies to you, mark the hook `onError: 'log'` or `async: true`, or keep the objects on one datasource.
17+
- **A sandboxed hook body's own `ctx.api.transaction`** on such an object, when no transaction is open around the write, used to open and own its transaction, so a throw from it rolled back only the body's own writes and the triggering row stayed stored. It now joins the door's unit, as it joins any open unit, and the abort rolls back the triggering row too.
18+
- **Work a unit starts and does not await** (an `async: true` hook) no longer inherits the unit's finished transaction handle. It used to fail with "Transaction query already complete" and lose its write. It now runs on its own connection, as it would outside any unit.
19+
- **Cost:** one transaction per write on the objects above only. Measured on the in-process SQLite driver: about 1.36x the per-insert time of the same object with an `onError: 'log'` hook. Objects without such a hook pay one map lookup.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
`HookSchema.onError` and `HookEvent` now say where an `after*` hook's `abort` rolls the write back, and what it means elsewhere
6+
7+
Clause-②: no
8+
9+
Text only: no key, shape, default or enum changes, and every hook that parsed before parses the same.
10+
11+
- **`onError`** (its TSDoc and `describe()`): `abort` refuses the operation. A blocking hook's abort rolls the write back whenever the write runs inside a unit of work. Those units are a caller's `transaction()`, an atomic batch or cascade, and the unit the write door now opens for a blocking `abort` `after*` hook on an object on the default datasource whose driver declares transactions (`@objectstack/objectql`, same release). On an object on another datasource, or a driver without transactions, an `after*` hook's abort refuses the caller while the row stays stored.
12+
- **`HookEvent`'s JSDoc** listed three ways a write ends up inside a unit of work. It now lists four: the write door's own unit is the new one.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/objectql": minor
4+
"@objectstack/metadata-protocol": patch
5+
---
6+
7+
A paged list under a search now reports the real number of matching records as `total`, and `hasMore` agrees with it on every page.
8+
9+
Clause-②: yes (widening) — `EngineCountOptionsSchema` gains two OPTIONAL keys, `search` and `searchFields`, so the accept set of the count options grows. Nothing previously admitted is refused, no key is renamed or retired, and no producer is required to write them.
10+
11+
Until now `GET /api/v1/data/:object?$top=25&$search=Line` over 75 matching records answered `total: 26` (offset + page + 1, a page-local estimate), and the last page claimed `hasMore: true`; `POST /api/v1/data/:object/query` with a `search` did the same. Nothing on the response marked the number as an estimate, so the Console rendered it as the record count and offered the wrong number of pages.
12+
13+
- **`@objectstack/spec`** — `EngineCountOptionsSchema` declares `search` (the bare string, or the structured `FullTextSearchSchema` form) and `searchFields`, identically to `EngineQueryOptionsSchema`. FROM `count(object, { where })` only → TO `count(object, { where, search, searchFields })`. The structured `search` form carries flag defaults, so the parsed state is named `EngineCountOptionsParsed` (ADR-0122). `HookCountQuery` (`ctx.api.object(name).count()`) derives from it and gains the same two keys.
14+
- **`@objectstack/objectql`** — `engine.count()` (and `ctx.api.object(name).count()`) accepts the two keys it used to refuse as unknown options, and expands them through the same ADR-0061 expansion `find()` and `aggregate()` use: the same server-resolved searchable fields, the same `internal: true` exclusion, the same `searchFields` narrowing, AND-ed with `where` before the security middlewares run. There is one expander, not two, so a count under a search is the number of rows `find()` returns for the same bag.
15+
- **`@objectstack/metadata-protocol`** — `findData`'s paged branch passes `search` / `searchFields` to `engine.count()`, and the page-local estimate branch is gone. `$count=false` still skips the count and omits `total`.
16+
17+
Nothing to migrate. A client that worked around the estimate, for example by labelling the count as page-local under a search, can read `total` as the match count again.

0 commit comments

Comments
 (0)