Skip to content

Commit 0c232d8

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-23088-bootstack-metadata-plugin
2 parents 480dcb7 + f931e76 commit 0c232d8

87 files changed

Lines changed: 2382 additions & 2795 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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@objectstack/plugin-security': minor
3+
'@objectstack/metadata-protocol': minor
4+
'@objectstack/objectql': minor
5+
'@objectstack/cloud-connection': patch
6+
---
7+
8+
feat(plugin-security)!: no seeder or materializer writes a `sys_permission_set` row — the declared-permission seeder, the platform-default materialization and the `permission` publish materializer are deleted
9+
10+
Clause-②: yes (narrowing)
11+
12+
<!-- adr-0087: not-required (no-migration-prescription) A runtime change on a platform object's rows and on TypeScript surfaces, not on any metadata schema: no key of any spec schema is removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone. The rows the retired seeders wrote stay in the table until a later ADR-0131 stage; nothing reads them for authorization. The package publishes (not unpublished); no ADR-0087 id covers this rule and this diff adds none (not registered / already-registered). -->
13+
14+
**BREAKING**, shipped as `minor` under the repo's launch-window convention for breaking changes.
15+
16+
`SecurityPlugin` no longer writes a `sys_permission_set` row for any permission set: not for the platform defaults (`admin_full_access`, `member_default`, …), not for a set a package declares, not for an organization's copy, and not on a metadata reload or an organization's creation. Permission sets are served by the security catalog (ADR-0131 D3), and since the grant names its set (`sys_user_permission_set.permission_set`), no grant path needs a row. A fresh database still promotes its first platform administrator, grants organization administrators and self-registered members, and grants every principal what it was granted before. On a database that was already seeded, the existing rows stay as they are; they are no longer refreshed at boot. Until a later stage retires the ADR-0094 projector, publishing a permission set (a package-door publish, or a writable base's direct publish) still has it write a projection row, `managed_by: 'admin'` with no `package_id` (a package-door publish wrote `managed_by: 'package'` before this release); no authorization path reads it, and uninstalling the package removes it.
17+
18+
- **Accepted package defaults follow every publish.** The `everyone` anchor is derived again after every landed `permission` mutation (publish, direct save or delete, through any door), so an accepted set whose new body no longer qualifies stops granting at once. Before this release the re-derivation ran only inside the publish materializer, and only when the published body carried an `isDefault` key.
19+
- **`drift_status` and `drift_detail` are gone from `sys_permission_set`**, with the **Needs Attention** list view that listed them. Nothing computes them any more: the declared-versus-enforced diagnostic retired with the rows it compared. A Studio overlay that shadows a packaged set is still named at boot.
20+
- **Uninstalling a package still revokes every grant of its permission sets** (ADR-0090 D5, "no ghost grants"). With no row standing for a package's set, both uninstall doors (`deletePackage` and `DELETE /api/v1/marketplace/install-local/:id`) capture the set names the package held just before they withdraw it from the registry, and the `security.package-permissions` cleanup revokes every `sys_user_permission_set` grant naming them. The withdrawal still runs before the cleanups. A grant naming such a set would otherwise survive the uninstall and be live again on reinstall. Grants written before the name column, and position bindings, are still reached through a legacy `sys_permission_set` row where an already-seeded database holds one.
21+
- `bootstrapPlatformAdmin` no longer takes the `resync` option and no longer reports `seeded`, `resynced` or `resyncSkipped`. A refused read of the legacy `admin_full_access` row (the restrict-only reach to a grant written before the name column existed) withholds the promotion and says so once.
22+
23+
Added (additive, nothing an author writes changes):
24+
25+
- `@objectstack/metadata-protocol`: the optional `declared?: UninstallDeclared` argument of `UninstallCleanup` (and of `runUninstallCleanups`' request), the `UninstallDeclared` type, and `captureUninstallDeclared(registry, packageId, stored?)`, the one capture both doors call. A cleanup that ignores `declared` behaves as before.
26+
- `@objectstack/objectql`: `SchemaRegistry.securityCatalogNamesHeldBy(packageId, type)`.
27+
- `@objectstack/plugin-security`: `cleanupPackagePermissions` takes an optional fourth argument, the captured names.
28+
29+
Removed from `@objectstack/plugin-security`'s public exports, with no replacement:
30+
31+
- `bootstrapDeclaredPermissions`
32+
- `normalizeManagedByVocab`
33+
- `computePermissionSetDriftDiagnostics`, `persistPermissionSetDriftDiagnostics`, `runPermissionSetDriftDiagnostics`, and the types `PermissionSetDriftStatus`, `PermissionSetDriftDiagnostic`, `DriftDiagnosticsOptions`
34+
- `CAPABILITY_DECLARATION_UNOWNED`, `CAPABILITY_PLATFORM_NAME_REFUSED`, `PERMISSION_SET_DECLARATION_UNOWNED`, `capabilityDeclarationUnownedDiagnostic`, `capabilityPlatformNameRefusedDiagnostic`, `permissionSetDeclarationUnownedDiagnostic`, `reportCapabilityDeclarationUnowned`, `reportCapabilityPlatformNameRefused`, `reportPermissionSetDeclarationUnowned`, and the types `CapabilityDeclarationUnownedDiagnostic`, `CapabilityPlatformNameRefusedDiagnostic`, `PermissionSetDeclarationUnownedDiagnostic`
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: the page action rows' url `target`, a dashboard header action's `actionUrl` and an action's `onSuccess.navigate` declare the closed url scheme set — `http://`, `https://`, `mailto:` and in-app paths — and refuse every other target at parse
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: registered ui-page-action-row-url-target-scheme-set-closed, ui-dashboard-header-action-url-scheme-set-closed, ui-action-on-success-navigate-scheme-set-closed -->
10+
11+
**BREAKING** — an accept-set narrowing on three published authoring surfaces, shipped as `minor` under the launch-window convention for accept-set narrowings.
12+
13+
All three were free strings, so a target the console's action runner refuses — `javascript:`, `data:`, `tel:`, a protocol-relative `//host`, a bare relative path — parsed clean, published, and failed only at the click or after the action succeeded. A `type: 'url'` action's `target` already closed this set; the scheme set is stated once in the contract and the runtime opens exactly that set, so these doors now apply the same grammar, with no second one:
14+
15+
- **The page action rows' `target`** on the url executor: an `action:button` or `action:icon` with `actionType: 'url'`, and an `action:group` / `action:menu` member with `type: 'url'`. On every other executor `target` names a script, flow, page, endpoint or form and is untouched, and a row with no `target` is not judged. A page's `properties` bag is outside the stack parse; `os validate` judges it through its props gate, which parses the bag against these same rows and so reports the refusal at the row's `target` with the same text, as a `component-props-invalid` finding (that gate's one tier, a warning).
16+
- **`dashboards[].header.actions[].actionUrl`**, when `actionType` is `'url'` or omitted. An omitted `actionType` is judged as the url route, because the two readers that give it a meaning (the console's dashboard renderer and `os validate`'s dashboard action check) both read it as `'url'`. On `'flow'`, `'script'`, `'modal'`, `'api'` and `'form'`, `actionUrl` names the flow, script, page, endpoint or form and is untouched.
17+
- **`actions[].onSuccess.navigate`**, on every value.
18+
19+
The admitted set, at both doors:
20+
21+
- an `http://` or `https://` URL (the scheme compared case-insensitively);
22+
- a `mailto:` address;
23+
- an in-app path that starts with `/` or `./` (`/apps/crm/tasks/${result.id}`);
24+
- a template that opens with `${` (`${result.redirectUrl}`), judged by the runner after interpolation.
25+
26+
Every other value is refused at the key, with a message that names the reason and the admitted set: `javascript:`, `data:`, `tel:` and every other scheme; `http:` or `https:` without the `//`; a protocol-relative `//host`; `/` followed by a backslash; a tab, line feed or carriage return anywhere; a bare relative value such as `forecast`, `../forecast` or `tasks/${result.id}`; and the empty string.
27+
28+
### FROM → TO
29+
30+
| before | what to write instead |
31+
| --- | --- |
32+
| `target: 'javascript:…'` / `actionUrl: 'javascript:…'` / `navigate: 'javascript:…'`, or `data:…` | no url target runs or renders content. For a page action row or a header action, declare the executor that does the work (`'script'`, `'flow'`, `'api'`), or link to an `https://` page; for `onSuccess`, drop it or navigate to an `https://` page. |
33+
| `'tel:+1…'`, `'ftp://…'` or any other scheme | an `https://` page that offers it, or a `mailto:` address. `tel:` is not admitted. |
34+
| `'https:example.com'` (no `//`) | `'https://example.com'` |
35+
| `'//cdn.example.com/x'` (protocol-relative) | `'https://cdn.example.com/x'` for an external link, or `'/x'` for an in-app path |
36+
| `'/\example.com'` (`/` then a backslash) | `'https://example.com'`, or an in-app path written with forward slashes |
37+
| a value holding a tab, line feed or carriage return | the same value without them; percent-encode one that belongs in the address (`%09`, `%0A`, `%0D`) |
38+
| `actionUrl: 'forecast'` with `actionType` omitted, meant as a flow or script | `actionType: 'flow'` (or `'script'`, …) beside it — the declared type takes `actionUrl` off the url route |
39+
| `'forecast'`, `'../forecast'`, `'?tab=2'`, `'#top'`, `navigate: 'tasks/${result.id}'` | the rooted in-app path, `'/forecast'` / `'/tasks/${result.id}'`, or a `./` path |
40+
| `target: ''` / `actionUrl: ''` / `navigate: ''` | the address the button or the hop was meant to open; drop an `onSuccess` that navigates nowhere |
41+
42+
**The one-line fix: give every url page action row, every url header action (`actionType: 'url'` or omitted) and every `onSuccess.navigate` an `http(s)://` URL, a `mailto:` address, an in-app path that starts with `/` or `./`, or a template that opens with `${` — or, for a row or a header action meant to run something, the executor that names it.** `os migrate meta --from 17` lists the D3 entries `ui-page-action-row-url-target-scheme-set-closed`, `ui-dashboard-header-action-url-scheme-set-closed` and `ui-action-on-success-navigate-scheme-set-closed`; the rewrite is by hand, because which address a refused value meant is the author's call.
43+
44+
**What an author now sees.** `os validate`, `os build`, `defineStack` and `defineAction` refuse such a value at `header.actions[].actionUrl` or `onSuccess.navigate`; on a page action row, `os validate`'s props gate reports it at the row's `target` (`properties.target`, or `properties.actions.N.target` for a member). For example, `javascript:alert(1)` on a header action gets: ``A dashboard header action's `actionUrl`, a url target when `actionType` is `'url'` or omitted, opens with `javascript:`, a scheme a url target never opens.``, followed by the reason and the admitted set. Every admitted value parses unchanged.
45+
46+
**Measured producers: none outside tests.** Every url-executor page action row and every header action outside tests in this repository and in objectui (at its pin `7578323` and its main `7af0ab37`) names an admitted target or declares a non-url `actionType`, and the only `onSuccess.navigate` outside tests is the documented in-app route template `/apps/crm/tasks/${result.id}`.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@objectstack/metadata-protocol': minor
3+
'@objectstack/spec': patch
4+
---
5+
6+
fix(metadata-protocol)!: the metadata save door refuses a `protection` block with a located `400 VALIDATION_ERROR` instead of storing a lock that binds nothing (ADR-0010 §3.5, Phase 4.3)
7+
8+
Clause-②: no (narrowing)
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: `protection` stays declared on every type that declares it, a code package's source keeps it, and the package loader still translates it into the item's lock; no spec key, authorable spelling, export or stored shape is added, removed, renamed or re-shaped, and a row stored before this change keeps its bytes and loads unchanged, so there is nothing for `objectstack migrate meta` to rewrite. What narrows is one runtime write door: a body saved through the metadata API that carries the block is refused, and the remedy is in the request a caller sends to that door, never a rewrite of a package source or a stored row. The other categories are closed on facts: both packages publish (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and no exported declaration is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
11+
12+
**BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes.
13+
14+
`protection` is the package author's lock declaration. ADR-0010 states one door for it: a code package's loader translates it into the item's private lock envelope when the package loads, and strips it (Phase 4.3). An item saved through the metadata API is authored in the environment, and ADR-0010 §3.5 states that such an item takes no package lock.
15+
16+
**This entry supersedes one sentence of the entry `15213-unmanaged-lock-strip`, in this same release.** That entry says a malformed `protection` block is not stripped and "the save door refuses that item as before, `422 INVALID_METADATA`, and the install is rolled back". What holds now: on a type whose schema declares `protection`, an item the unmanaged install hands on with its block still in it (a malformed block, or a block beside a `_lock*` value its declaration refuses) meets this entry's door refusal, `400 VALIDATION_ERROR`, and the install is still rolled back. An item carrying a refused `_lock*` value and no block, and a block on a type that does not declare one (`action`, `hook`, `page`), still answer `422 INVALID_METADATA`. Where the two entries disagree, this one is current.
17+
18+
**FROM.** `PUT /api/v1/meta/{type}/{name}` with a `protection` block answered `200` and stored the block verbatim. The item was then served with `protection.lock: 'full'` while it read `lock: 'none'`, `editable: true` and `deletable: true`, and a second save and a delete both answered `200`. Measured on the showcase for an `app` and a `dashboard`. An author, or an AI, reading that item was told it was protected when anyone holding the authoring capability could change or delete it.
19+
20+
**TO.** The same save answers `400 VALIDATION_ERROR` and writes nothing. The message names the block and the door that does honour it, and the envelope's `issues[]` carries `protection` as the path, so the Studio designer can highlight it.
21+
22+
- **Every type that declares the block**, in draft and in publish mode, for a new item and for an overlay of a packaged one. A malformed block is refused the same way, because the block is not accepted at this door at all. On a type whose schema does not declare `protection`, the schema's own `422 INVALID_METADATA` answers, as before.
23+
- **Every authoring write**: the REST and dispatcher saves and the domain doors that save through them. The unmanaged install already drops a block it can read before it saves a copy, and a malformed one is refused here.
24+
- **Server-side rewrites strip it.** A row stored before this change may carry the block. When the server itself rewrites such a row (`os migrate meta --stored --apply`, the stored flow credential move, package duplication), it drops the block instead of failing the rewrite: the block never bound, so nothing changes but the stored bytes.
25+
- **Unchanged.** A body without `protection` saves as before. A packaged item's lock still binds: its loader translated the block at load time, and a save against it still answers `403 ITEM_LOCKED`, or `403 NOT_OVERRIDABLE` on a type with no environment overlay, before this refusal is asked. A row stored before this change keeps its bytes.
26+
- **Package export.** `GET /packages/:id/export` copies a writable base's rows into a manifest, so a block saved into a base row rode into that manifest, where the package loader translates it on install. A block can no longer be saved into a base row. Declare it in the manifest you install instead.
27+
- **Remedy.** Delete the `protection` block from the body you send to the metadata API. To lock an item, declare `protection` on it in a code package's source.
28+
29+
`@objectstack/spec`: the `protection` TSDoc on every type that mounts it (app, view, dashboard, report, dataset, object, flow, webhook, agent, tool, skill, position, permission set, email template) now says that only the package loader translates the block, and that the metadata API's save door refuses it.

0 commit comments

Comments
 (0)