Skip to content

Commit 13e311f

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-23293-client-delete-package
2 parents 4ae4bd7 + 3490bf5 commit 13e311f

47 files changed

Lines changed: 1935 additions & 618 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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
The `mailAssociation` liveness row is `live`: the mailbox service's matcher reads the key, so authoring `mailAssociation: true` on an email field no longer draws the "no mailbox service reads this yet" author warning
6+
7+
Clause-②: no (dropping an author warning widens no accept set and no export: `FieldSchema` accepts `mailAssociation` exactly as before; only the warning the shipped liveness ledger carried is gone)
8+
9+
- `liveness/field.json`: the row moves from `planned` with `authorWarn` and `authorHint` to `live`, citing `MailRecordMatcher` in `@objectstack/service-mailbox` as its reader. The `field` state counts move from 94 live and 4 planned to 95 live and 3 planned.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/plugin-auth': minor
3+
---
4+
5+
The identity import's automatic invitation email links to the Console's set-password page, no longer to an `INVALID_TOKEN` error
6+
7+
Clause-②: yes (widening)
8+
9+
- **Before.** For every row an admin imported under the Automatic (or Invite) password policy with email delivery, `POST /api/v1/auth/admin/import-users` requested the password reset with no `redirectTo`. better-auth then emailed a link ending in an empty `callbackURL=`, and its `GET /reset-password/:token` refused that link with a redirect to `?error=INVALID_TOKEN`, which the Console shows on its sign-in page. The token in the link was still valid, but the invited user had no page to use it on.
10+
- **Now.** The reset request carries `redirectTo` set to the Console's set-password page (`${origin}${uiBasePath}/reset-password`, by default `/_console/reset-password`), the page the Console's own forgot-password flow lands on. The emailed link opens `/_console/reset-password?token=…`, and better-auth's origin check accepts it because it is the deployment's own origin.
11+
- **The two new members.** Both are additions; nothing existing changes shape.
12+
- `AuthManager.getConsoleUrl(path)` is a public reader of the absolute URL of a Console page. It returns what the class already composes for every email and SMS link it sends.
13+
- `IdentityImportDeps.consoleResetPasswordUrl?()` is optional. `AuthPlugin` wires it to `getConsoleUrl('/reset-password')`. A host that calls `runAdminImportUsers` itself should pass it to get a working invitation link. A caller that omits it sends the request exactly as before (`{ email }`), so the link keeps the empty `callbackURL`.
14+
- **Unchanged.** The SMS invitation, the temporary-password policy, and the email copy (the invitation still uses the password-reset template).
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
'@objectstack/metadata-core': minor
3+
'@objectstack/rest': minor
4+
'@objectstack/runtime': minor
5+
'@objectstack/cloud-connection': minor
6+
'@objectstack/spec': minor
7+
---
8+
9+
fix(security)!: under a walled posture every environment-metadata write is the platform operator's, whatever its type (ADR-0131 D12 items 7–8, D11)
10+
11+
Clause-②: no (narrowing)
12+
13+
<!-- adr-0087: registered walled-environment-metadata-writes-retired -->
14+
15+
**BREAKING** accept-set narrowing, shipped as `minor` under the repo's launch-window convention for breaking changes. ADR-0131 D12 item 7: a walled deployment is "one environment, one schema, one catalog … The metadata-authoring capability (`manage_metadata` / `studio.access`) is what headquarters IT holds and plant admins do not." Item 8: a plant "cannot create a position, a permission set, a capability, a template or any metadata … A plant that needs its own catalog is given its own environment." D11: the rules are identical in every posture; only whether the wall is enforced differs.
16+
17+
**Why.** The environment definition ledger carries no organization column, so a write lands for every organization. Measured on booted `isolated` and `group` stacks before this change: an organization owner granted `manage_metadata` inside their own organization created an object, a view and a flow at `/api/v1/meta`, edited, published, rolled back and reset the operator's view, ran `POST /api/v1/meta/_migrate-stored`, passed all thirteen `/api/v1/packages` write doors, installed, upgraded and uninstalled packages through `install-local`, and created a flow at `/api/v1/automation`, each with `2xx`; another organization's owner read every result. The security catalog was refused at these doors already; every other type was not.
18+
19+
**FROM → TO**, on a `group` or `isolated` deployment, for a caller below the platform operator (the `PLATFORM_ADMIN` rung: the deployment's declared `OS_PLATFORM_OWNER_EMAIL` owner):
20+
21+
- `PUT`, `DELETE`, `POST …/publish` and `POST …/rollback` of `/api/v1/meta/:type/:name` on `@objectstack/rest`, and `PUT /meta/:type/:name` on the runtime dispatcher, for a type outside the security catalog (`object`, `view`, `flow`, `app`, `dashboard`, `translation` and every other type): **FROM** admitted to any `manage_metadata` holder **TO** `403 FORBIDDEN` (the dispatcher answers `403 PERMISSION_DENIED`).
22+
- Every `/api/v1/packages` write on the runtime dispatcher — create, the manifest `PATCH`, enable, disable, publish, publish-drafts, discard-drafts, revert, rollback, `commits/:commitId/revert`, duplicate, adopt-orphans and delete — for a package carrying no position, permission set or capability: **FROM** admitted to any `manage_metadata` holder **TO** `403 PERMISSION_DENIED`, before anything is read or written. The refusal no longer reads the package's items.
23+
- `POST /api/v1/marketplace/install-local` (install and upgrade) and `DELETE /api/v1/marketplace/install-local/:id` (uninstall) on `@objectstack/cloud-connection`, for a package carrying no catalog item: **FROM** admitted to any `manage_metadata` holder **TO** `403 FORBIDDEN`. The refusal's `error.details` no longer carries `catalogItems`; it carries `manifestId`.
24+
- `POST /api/v1/automation`, `PUT /api/v1/automation/:name`, `DELETE /api/v1/automation/:name` and `POST /api/v1/automation/:name/clone`: **FROM** admitted to any `manage_metadata` holder **TO** `403 PERMISSION_DENIED`, before the automation service is touched.
25+
- `POST /api/v1/meta/_migrate-stored`, the install-wide rewrite of the stored metadata, preview and apply alike: **FROM** admitted to any `manage_metadata` holder **TO** `403 FORBIDDEN` on `@objectstack/rest` and `403 PERMISSION_DENIED` on the runtime dispatcher, before a row is read.
26+
- Every one of these refusals carries one sentence: it names the written type (`a package` at the package doors, `the environment's stored metadata` at the rewrite), the posture and "ADR-0131 D3, D12 items 7–8, D11", and points at an environment of the organization's own. The catalog refusals that shipped before keep their envelopes and now read the same sentence.
27+
- The activation refusals under a wall (`POST /api/v1/automation/:name/toggle`, `POST /api/v1/actions/_activation/…`, `POST /api/v1/security/_activation/permission/…`) no longer recommend cloning a flow or a permission set, or authoring a sibling action, since those writes are the operator's under a wall too; they name the platform operator.
28+
29+
**Unchanged.** Reads. Assignments: `sys_user_position` and `sys_user_permission_set` rows stay organization rows. The platform operator and the engine's own writes (seeders, installers, migrations) write as before. Under `single` (and with no `tenancy` service), a `manage_metadata` holder writes exactly as before.
30+
31+
**The remedy.** The platform operator authors the environment's metadata, in Studio or through a managed package; an organization that needs metadata of its own is given an environment of its own. `@objectstack/spec` registers the change as the ADR-0087 semantic entry `walled-environment-metadata-writes-retired`. `walledCatalogWriteVerdict` (`@objectstack/metadata-core`) now judges every type alike: a custom door that writes environment metadata asks it after its capability gate, with no `isSecurityCatalogType` pre-filter.

‎content/docs/api/metadata-api.mdx‎

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@ Get a specific metadata item with optional ETag caching.
5757

5858
Create or update a metadata item.
5959

60+
Saving requires the `manage_metadata` capability. Under a `group` or `isolated`
61+
tenancy posture it also requires the platform operator, for every type: the
62+
environment's metadata is shared by every organization (ADR-0131 D12 items 7–8,
63+
D11), so a `manage_metadata` holder inside one organization is refused
64+
`403 FORBIDDEN` naming the posture. The same holds for `DELETE` (reset),
65+
`POST /meta/:type/:name/publish` and `POST /meta/:type/:name/rollback`, and for
66+
`POST /meta/_migrate-stored`, the install-wide rewrite of the stored metadata,
67+
preview included.
68+
6069
Creates are gated per type: `MetadataTypeRegistryEntry.allowRuntimeCreate` decides
6170
whether a **brand-new** name may be minted under that type at all, separately from
6271
whether an item that already exists may be overlaid (`allowEnvironmentOverlay`). A type that
@@ -123,22 +132,17 @@ Get package details.
123132

124133
### `DELETE /packages/:id`
125134

126-
Uninstall a package. Under a `group` or `isolated` tenancy posture, a package
127-
holding positions or permission sets is the platform operator's to delete: any
128-
other caller is refused `403 PERMISSION_DENIED` (ADR-0131 D3).
129-
130-
The package's other write verbs follow the same rule under a wall, judged by
131-
the items each one acts on:
132-
- `POST /packages/:id/publish-drafts`, `POST /packages/:id/discard-drafts` and
133-
`POST /packages/:id/revert`, by the package's pending drafts;
134-
- `POST /packages/:id/rollback`, by the commits it would undo;
135-
- `POST /packages/:id/commits/:commitId/revert`, by that commit's own items,
136-
whatever package the URL names;
137-
- `POST /packages/:id/duplicate`, by the source's published items it copies;
138-
- `POST /packages/:id/adopt-orphans`, by the environment's orphaned rows.
139-
140-
When those items include a position, a permission set or a capability, only the platform
141-
operator may run the verb.
135+
Uninstall a package.
136+
137+
Under a `group` or `isolated` tenancy posture every `/packages` write is the
138+
platform operator's (ADR-0131 D12 items 7–8, D11): install (`POST /packages`),
139+
the manifest `PATCH`, enable, disable, publish, `publish-drafts`,
140+
`discard-drafts`, `revert`, `rollback`, `commits/:commitId/revert`, `duplicate`,
141+
`adopt-orphans` and this delete. Any other caller, a `manage_metadata` holder
142+
inside one organization included, is refused `403 PERMISSION_DENIED` naming the
143+
posture, before anything is read or written, whatever the package carries.
144+
Reads are unchanged. Under `single` every `manage_metadata` holder writes as
145+
before.
142146

143147
### `PATCH /packages/:id/enable`
144148

‎content/docs/deployment/tenancy-modes.mdx‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ fallback **input** to posture resolution. See
3737
| Enterprise `@objectstack/organizations` | not needed | **required** | **required** |
3838
| Default organization | one bootstrapped "Default Organization" | owned by the enterprise runtime | owned by the enterprise runtime |
3939
| Org-management UI (create / switch org) | hidden | shown | shown |
40+
| Who writes the environment's metadata | any `manage_metadata` holder | the platform operator only | the platform operator only |
4041

4142
Only the wall's **predicate** differs between `group` and `isolated`. Everything
4243
about how it composes is identical: Layer 0 is AND-composed ahead of, and
@@ -46,6 +47,33 @@ W1/W2, ADR-0105 D2/D4). A walled posture with no resolvable scope — no active
4647
organization under `isolated`, an empty membership set under `group` — fails
4748
closed to zero rows rather than falling through.
4849

50+
### Under a wall, the environment's metadata is the platform operator's
51+
52+
A walled deployment is one environment with one schema and one catalog
53+
(ADR-0131 D12 items 7–8, D11). Its metadata (objects, fields, views, flows,
54+
apps, dashboards, translations, packages, positions, permission sets,
55+
capabilities, every type) carries no organization, so a write lands for every
56+
organization at once. Under `group` and `isolated` only the platform operator
57+
(the `PLATFORM_ADMIN` rung: an address declared in `OS_PLATFORM_OWNER_EMAIL`)
58+
and the engine's own writes (seeders, installers, migrations) write it. Holding
59+
`manage_metadata` inside an organization does not admit a write: the `/meta`
60+
write doors (save, reset, publish, rollback), the install-wide stored rewrite
61+
(`POST /meta/_migrate-stored`), every `/packages` write,
62+
`install-local` install, upgrade and uninstall, and the `/automation`
63+
definition writes refuse such a caller `403`, naming the posture.
64+
65+
- **Headquarters IT** authors the environment's metadata as the platform
66+
operator: declare its administrators in `OS_PLATFORM_OWNER_EMAIL`. A
67+
`manage_metadata` grant scoped to an organization authors nothing under a wall.
68+
- **An organization's administrators assign**: they place their own members in
69+
the catalog's positions and permission sets, and manage their organization's
70+
data. They define nothing.
71+
- **An organization that needs metadata of its own** is given an environment of
72+
its own.
73+
74+
Under `single` the organization is the environment, and every `manage_metadata`
75+
holder writes as before.
76+
4977
`single` is not "multi-org with the walls left standing": Layer 0 contributes no
5078
filter at all, and the platform's own tenant-scoped RLS policies are stripped.
5179
Policies **you** authored that mention `current_user.organization_id` are

‎content/docs/permissions/capabilities.mdx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,10 @@ result is an unreferenced grant target sitting in the live authorization
245245
namespace. `job` and `agent` carry the same pair for the same reason.
246246

247247
Under a `group` or `isolated` tenancy posture, a caller below the platform
248-
operator reads a different refusal, with the same `403` status: the security
249-
catalog refusal (ADR-0131 D3), whose message names the posture, with the code
250-
`FORBIDDEN` on the REST door and `PERMISSION_DENIED` on the runtime dispatcher.
248+
operator reads a different refusal, with the same `403` status: the walled
249+
refusal every environment-metadata write takes there (ADR-0131 D3, D12 items
250+
7–8), whose message names the posture, with the code `FORBIDDEN` on the REST
251+
door and `PERMISSION_DENIED` on the runtime dispatcher.
251252
It runs before the code-only refusal, so the block above is the answer the
252253
platform operator reads, and every caller under `single`.
253254

‎content/docs/permissions/delegated-administration.mdx‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,12 @@ issuance:
181181

182182
Without the cap the role would be a ladder: invite an `admin`, and acceptance
183183
auto-elevates that membership to `organization_admin` → tenant admin — the
184-
grade the delegation exists to withhold. `sys_member` is not one of the gate's
185-
governed objects and the acceptance write runs outside the issuer's context, so
186-
the cap is the defense on that path.
184+
grade the delegation exists to withhold. `sys_member` *is* one of the gate's
185+
governed objects (the "Membership is not delegable" rule above), but the
186+
acceptance write never reaches the gate: better-auth writes the membership row
187+
as the system (`isSystem`), and the security middleware passes a system write
188+
through before the gate runs. So the cap — judged at issuance, while the
189+
inviter is still the actor — is the defense on that path.
187190

188191
Both halves are pinned end-to-end over the real HTTP route by
189192
`packages/qa/dogfood/test/delegated-admin-invite.dogfood.test.ts`: a

‎content/docs/permissions/permission-sets.mdx‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -356,13 +356,10 @@ same request (no ghost grants); the uninstall response reports the revocation
356356
under `data.persisted.cleanups` (the dispatcher's `/packages` domain answers
357357
the route, and it nests the protocol's uninstall report under `persisted`).
358358
Environment-authored sets and other packages' rows survive. Under a `group` or
359-
`isolated` tenancy posture, removing a package that carries positions or
360-
permission sets is the platform operator's (ADR-0131 D3): that delete, and an
361-
`install-local` install, upgrade or uninstall of such a package, refuse any
362-
other caller `403` before anything changes. So do the package verbs that
363-
publish, discard, roll back, revert, duplicate or adopt catalog items
364-
(`publish-drafts`, `discard-drafts`, `revert`, `rollback`,
365-
`commits/:commitId/revert`, `duplicate`, `adopt-orphans`).
359+
`isolated` tenancy posture every package write is the platform operator's
360+
(ADR-0131 D3, D12 items 7–8): the delete, an `install-local` install, upgrade
361+
or uninstall, and every other `/packages` write verb refuse any other caller
362+
`403` before anything changes, whatever the package carries.
366363

367364
## One authoritative store — the definition (ADR-0094 D1, ADR-0131 D3)
368365

0 commit comments

Comments
 (0)