Skip to content

Commit 4f7ac62

Browse files
committed
Merge origin/main into claude/issue-21839-share-link-password
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
2 parents eacae6b + 25eb7de commit 4f7ac62

124 files changed

Lines changed: 4081 additions & 517 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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
The organization's member, invitation and team actions are offered only to the membership grades the server admits. A plain member no longer sees "Invite User", "Change Role", "Remove Member", "Cancel Invitation", "Create Team" and the rest, each of which the server refused with 403.
6+
7+
- `invite_user` (on the Users, Members and Invitations lists) and `resend_invitation`: owner, admin and delegated_admin.
8+
- `update_member_role`, `remove_member`, `cancel_invitation`, `create_team`, `update_team`, `remove_team`, `add_team_member` and `remove_team_member`: owner and admin.
9+
- `transfer_ownership`: the owner alone, on a non-owner row.
10+
- `add_member` is unchanged. Its door is platform-admin standing, not a membership grade.
11+
12+
Each action declares `requiresMembershipReach` from `@objectstack/spec`, which is lowered into its `visible` predicate.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
An action can declare which organization membership grades it is offered to: `requiresMembershipReach` names a row of the new `MEMBERSHIP_REACH` table and is lowered at parse time into `visible`, the way `requiresFeature` is.
6+
7+
Clause-②: yes (widening)
8+
9+
- **`MEMBERSHIP_REACH`** (`@objectstack/spec/identity`) says which membership grades reach which better-auth organization endpoint. `invite_member` is reached by owner, admin and delegated_admin. `cancel_invitation`, `update_member_role`, `remove_member`, `create_team`, `update_team`, `remove_team`, `add_team_member` and `remove_team_member` are reached by owner and admin. `transfer_ownership` (setting the creator role on a member) is reached by the owner alone. The rows are read off better-auth's own access-control statements plus the `delegated_admin` registration, and plugin-auth pins them equal to the door. It is reach, not authority (ADR-0108 D1): a fourth fact beside the membership names, the administrative-grade rule and the identity projection, and merged into none of them. Also exported: `MEMBERSHIP_REACH_NAMES`, `membershipReachPredicate`, `lowerRequiresMembershipReach`, and the `MembershipReachEntry`, `MembershipReachName` and `MembershipReachStatement` types.
10+
- **`ActionSchema.requiresMembershipReach`** is optional and enum-checked against the table's row names. At parse time it becomes one `'<name>' in current_user.positions` term per grade, in the names `mapMembershipRole` projects them to (`org_owner`, `org_admin`, `delegated_admin`). The terms are AND-composed with an explicit `visible`, and the key is stripped from the parsed output. It composes ahead of `requiresFeature`, so a feature gate stays the last term. `visible: false`, a non-CEL or AST-only `visible`, and a blank `source` are refused at parse time, at the key.
11+
- It is UI courtesy, not authorization: the endpoint's own door stays the authority. The capability channel (`current_user.can`) is unchanged and carries no grade (ADR-0108).
12+
13+
Nothing that parsed before is refused now, and an action without the key lowers exactly as before.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
---
4+
5+
Global search (`GET /api/v1/search`) skips the objects a caller cannot read instead of failing the whole request with `403 PERMISSION_DENIED` (#21836).
6+
7+
Clause-②: no
8+
9+
- **Object level.** Before an object is queried, `searchAll` asks the `security` service's `canReadObject` with the caller's context, the same read gate the engine middleware enforces. An object it refuses is skipped. A member whose scope included any object they hold no read grant on used to get 403 for every query. The console palette then showed "No results found." with no error.
10+
- **Field level.** Each object is searched only on the fields the caller may query (`getQueryableFields`). The engine refuses a search that would match on a hidden field, and `sys_user`'s searchable fields include admin-only columns, so a member's unscoped search hit that refusal as well. An object left with no queryable search field is skipped.
11+
- **Nothing about a skipped object reaches the response.** It is not queried, named or counted in `totalObjects`, and the decision is made before any row is read. An explicit `objects=` naming an unreadable object gets the same answer as a name that matches no object.
12+
- **Other failures still fail the search.** A read error on a readable object, or an admission check that throws, propagates as before. Callers that can read every object, and calls without a context, get the same answer as before.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/service-datasource': patch
3+
---
4+
5+
fix(service-datasource): `POST /api/v1/datasources/:name/external/validate` sees a federated object saved at runtime, with no restart (#21842)
6+
7+
Clause-②: no
8+
9+
- **What was wrong.** The federation service read its objects from the `metadata` service. That service holds a copy of the engine's object registry taken once at boot. `PUT /api/v1/meta/object/:name`, and the external-table import that saves through it, write `sys_metadata` and the engine registry, but never that copy. So after a federated object was saved at runtime, validate answered the code-defined objects only, and listed the saved one after a restart. An object re-saved at runtime was judged on its definition as it stood at boot.
10+
- **What it reads now.** `ExternalDatasourceServicePlugin` reads objects (`listObjects` and `getObject`) from the engine's object registry on the `objectql` service, which is the registry the save writes through to. The registry is looked up when validation runs, not when the plugin starts. A saved or imported object is listed and judged on what was saved, the moment the save answers.
11+
- **What does not move.** The comparison is unchanged: the same federation predicate, the same column and type checks, and datasource definitions read from the same place. On `objectstack dev` the boot validation gate sweeps the same objects with the same verdicts as before. No route's request or response shape changes, and no export is added.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/plugin-approvals': patch
3+
---
4+
5+
Approval notifications reach their recipient with their text (#21847). The approvals service put each notification's text in `payload.message`. The messaging service builds the delivered notification from `payload.title` and `payload.body`, the fields its `EmitInput` documents, and no channel reads `message`. So `GET /api/v1/notifications` served every approval notification as a title over an empty `body`, and the inbox row's `body_md` was empty too. The lost texts were comments, request-info questions, send-back notes, reassignments, reminders, escalations, SLA breaches and out-of-office substitutions. Every one of them now travels in `payload.body`.
6+
7+
Clause-②: no
8+
9+
- The texts are unchanged. Only the field they travel in moved. Who is notified, and when, is unchanged.
10+
- Approval notifications no longer carry `payload.message`. Nothing in the platform or the console read it. A tenant-authored `sys_notification_template` for an `approval.*` topic that wrote `{{ message }}` should write `{{ body }}`.
11+
- The service's notify helper now declares its payload: `title`, `body`, `actionUrl`, and a reminder's `actions`. A call site that spells the text any other way no longer compiles.
12+
- `@objectstack/service-messaging` is unchanged. There is one field, as documented, and no alias for the old one.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/platform-objects': minor
3+
---
4+
5+
fix(platform-objects)!: retire the `sys_account` `link_social` action, which was dead on every boot; `unlink_account` stays
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) The withdrawn action is platform-shipped metadata on sys_account, not something an author writes: no spec key, spelling, export name or config field is retired or renamed, and nothing an author wrote needs rewriting. No stored shape carries it either: sys_account is lock full, so no sys_metadata overlay of the object can be saved, and an action is not a stored row. No reachable member of an exported type carries it, measured on the built declarations: SysAccount's declared type keeps only fields from its literal, so its action names already resolved to string, and the bundles are typed as TranslationData objects with no key types. -->
10+
11+
**BREAKING**: `sys_account` no longer declares the `link_social` action, so the "Link Social Account" toolbar button is gone from the Account app's Linked Accounts list and from Setup's Identity Links. It never completed a link on any boot: it navigated to a `GET` of the social sign-in route, which is served as `POST` only, and it offered a fixed list of seven providers whatever the boot had configured. It is retired under ADR-0049 (enforce or remove) and ships as `minor` under the launch-window convention for narrowings.
12+
13+
**What stays.** `unlink_account` is unchanged: the same type, target, placement and row-id parameter. Its confirm question no longer says the user can re-link "from their account settings", because no console surface offers that now. The `sys_account._actions.link_social` leaves are gone from the `en`, `zh-CN`, `ja-JP` and `es-ES` bundles.
14+
15+
**What to do after upgrading.** Linking a social or OIDC identity stays available through the signed-in `POST /api/v1/auth/link-social`, which is `auth.accounts.linkSocial({ provider, callbackURL })` in `@objectstack/client`: call it and navigate to the `url` it answers.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
A data-door edit of a permission set saved into a writable runtime package updates that set's stored definition instead of forking it
6+
7+
Clause-②: no
8+
9+
Setup saves a permission set through the data door (`PATCH /api/v1/data/sys_permission_set/:id`), which redirects the edit into the metadata store. A stored definition row is keyed by its package as well as its name, and the redirected save named no package. For a set saved into a writable runtime package (`PUT /api/v1/meta/permission/:name?package=<id>`), whose only stored row is bound to that package, the save therefore created a second, package-less row carrying the edit and left the package's row untouched: two active definitions for one name, with the package's copy no longer receiving the organization's edits. The save now goes into the package the edited row is bound to, read from that row through the metadata door's own item read, so the edit lands on the set's one row and the row stays in its package.
10+
11+
Unchanged: a set with no package binding still saves with none, and a set a code package ships is still refused with `403 NOT_OVERRIDABLE` before anything is read or written. The edit answers `200` as before; no error code, route or field moves.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
'@objectstack/service-datasource': minor
3+
---
4+
5+
fix(service-datasource)!: on `objectstack start`, external validation and the boot gate compare every federated object
6+
7+
**BREAKING (narrowing)** — on `objectstack start` a deployment with real schema drift on a
8+
federated object can now refuse to boot, as its `external.validation.onMismatch` setting
9+
declares.
10+
11+
The federation service (`ExternalDatasourceServicePlugin`) read the `metadata` service once,
12+
in its `init()`, and kept the answer. `objectstack start` composes no metadata plugin: its
13+
`metadata` service is the kernel's in-memory fallback, which the kernel registers after every
14+
plugin's `init()`, just before the start phase. So on `start` the federation service kept "no
15+
metadata service" for the life of the process, and every read behind it answered as if the
16+
deployment declared nothing. It now asks for the `metadata` service each time it reads it, so
17+
`start` sees what `objectstack dev` always saw.
18+
19+
| on `objectstack start` | before | now |
20+
| --- | --- | --- |
21+
| the boot gate (ADR-0015 §5.2) | logged "all federated objects match their remote schema" with `objects: 0`, having compared nothing, so no `onMismatch` policy ever applied | compares every federated object and applies each datasource's `onMismatch` to every measured mismatch |
22+
| `POST /api/v1/datasources/:name/external/validate` | `ok: true` with no rows | one row per federated object bound to the datasource, with its diffs |
23+
| `POST /api/v1/datasources/:name/external/refresh-catalog` | answered the snapshot, never stored it | also stores it as the datasource's `external_catalog` record |
24+
| `GET /api/v1/datasources/:name/external/tables` | ignored the datasource's `external.allowedSchemas` | leaves out a table whose schema is outside them |
25+
| draft and import names | never resolved a package namespace: drafts were unprefixed, and an import's explicit `name` was never held to the ADR-0028 prefix rule | resolve the namespace of the datasource's package when the datasource carries package provenance; an import whose explicit `name` lacks that prefix is refused `400 EXTERNAL_IMPORT_ERROR` |
26+
27+
**What to do if a `start` deployment now refuses to boot.** Under `onMismatch: 'fail'` (the
28+
default) the boot stops with `ExternalSchemaMismatchError`, naming the object, the datasource
29+
and each drifted column. Either fix the drift (align the object's fields, its
30+
`external.columnMap` or `external.ignoreColumns`, or the remote table) or set
31+
`external.validation.onMismatch: 'warn'` on that datasource, which boots and logs the drift
32+
instead. To see the drift before deploying, call
33+
`POST /api/v1/datasources/:name/external/validate` on `objectstack dev`, which already
34+
compared. A remote that cannot be reached still never stops the boot. An import refused for
35+
its name takes a `name` carrying the datasource package's namespace prefix.
36+
37+
**Unchanged.** `objectstack dev`, and every composition that registers a metadata plugin before
38+
the federation service, answers exactly as before: measured on the showcase, every federation
39+
door and the boot gate gave the same answers before and after. What validation judges, what
40+
each `onMismatch` value does, and the boot gate's skip for a datasource that sets
41+
`external.validation.checkOnBoot: false` are unchanged.
42+
43+
Clause-②: no (narrowing)
44+
45+
<!-- adr-0087: not-required (no-migration-prescription) No authorable key, spelling, export, config field or stored shape is removed or renamed, and no stored row is read or rewritten: on a composition with no metadata plugin the federation service now reads the metadata service it always declared it reads, so the boot gate and the federation doors judge the objects and datasources the deployment already declares. The handling above is an operator choice between two settings that already exist, not a FROM to TO mapping that objectstack migrate meta could apply. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers when a plugin resolves a service (not registered / already-registered); and the change is runtime behaviour, not a declaration (not runtime-interface-only / type-surface-only). -->

‎content/docs/protocol/objectui/actions.mdx‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,15 @@ label: View Dashboard
9494
type: url
9595
target: /_console/dashboards/sales_overview
9696
97-
name: link_social
98-
label: Connect Account
97+
name: open_in_maps
98+
label: Open in Maps
9999
type: url
100-
target: '/api/v1/auth/sign-in/social?provider=${param.provider}&callbackURL=${ctx.origin}/_console/apps/account/sys_account'
100+
target: 'https://www.google.com/maps/search/?api=1&query=${param.address}'
101+
params:
102+
- name: address
103+
label: Address
104+
type: text
105+
required: true
101106
```
102107

103108
For a **static** url, `openIn: 'new-tab'` opens `target` in a new tab and `openIn: 'self'` navigates in place; omitted, absolute URLs open in a new tab and relative ones navigate in place. For an **async handler that redirects**, set `opensInNewTab: true` instead (the renderer pre-opens the tab synchronously so popup blockers don't fire); `newTabUrl` provides a zero-roundtrip new-tab target template supporting the `{recordId}` placeholder.

‎content/docs/references/data/object.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,7 @@ const result = ApiMethod.parse(data);
468468
| **resultDialog** | `{ title?: string \| Record<string, string>; description?: string \| Record<string, string>; acknowledge?: string \| Record<string, string>; format?: Enum<'qrcode' \| 'code-list' \| 'secret' \| 'text' \| 'json'>; … }` | optional | Render API response in a one-shot reveal dialog (suppresses successMessage when set). |
469469
| **visible** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate — `true`/`false` literal, CEL string, or `{dialect, source}` envelope. The action is offered when it evaluates TRUE. Omit = always visible. |
470470
| **requiresFeature** | `Enum<'twoFactor' \| 'organization' \| 'multiOrgEnabled' \| 'degradedTenancy' \| …>` | optional | Public auth feature flag gating this action; lowered into `visible` at parse time. |
471+
| **requiresMembershipReach** | `Enum<'invite_member' \| 'cancel_invitation' \| 'update_member_role' \| …>` | optional | Organization endpoint (a MEMBERSHIP_REACH row) whose membership-grade gate this action follows; lowered into `visible` over current_user.positions at parse time. |
471472
| **disabled** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Disabled predicate — `true`/`false` literal, CEL string, or `{dialect, source}` envelope. The action is shown but refused when it evaluates TRUE. Omit = never disabled. |
472473
| **requiredPermissions** | `string[]` | optional | [ADR-0066 D4] Capabilities required to invoke this action. Enforced with 403 on the platform action route (script/flow/modal + MCP) and mirrored as a UI hide; a `type: api` action pointed at a custom endpoint must re-check it there. |
473474
| **shortcut** | `never` | optional | [REMOVED] `action.shortcut` was removed in @objectstack/spec 17.0.0 (audit close-out) — it never triggered anything: no keydown listener feeds ActionEngine.getShortcuts(), and objectui's keyboard stack (useKeyboardShortcuts) is hand-registered and never consults action metadata. Delete the key. For a real shortcut, register the key in the Console keyboard stack and have its handler invoke the action by name. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |

0 commit comments

Comments
 (0)