Skip to content

Commit 347061e

Browse files
committed
Merge origin/main into claude/issue-4640-revoke-delete-signature
2 parents 7acbcb0 + 65ca83a commit 347061e

45 files changed

Lines changed: 1312 additions & 212 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
feat(spec)!: 双源 C5 收敛 — `ActivationEventSchema``./kernel` 结构化形状,`./studio` re-export (#4653)
6+
7+
`ActivationEventSchema` 这个名字过去在两个入口解析到**两份不同的声明**,插件作者拿到哪套校验取决于他从哪个子路径 import(#4411 陷阱):
8+
9+
| 入口 | 声明 | 作者写的样子 |
10+
|:--|:--|:--|
11+
| `@objectstack/spec/kernel` | `z.object({ type: z.enum([...]), pattern: z.string() })` | `{ type: 'onCommand', pattern: 'my.cmd' }` |
12+
| `@objectstack/spec/studio` | `z.string()` | `'onCommand:my.cmd'` |
13+
14+
两侧都在作者面上(kernel 侧嵌在 `DynamicLoadRequest.activationEvents`,studio 侧嵌在 `StudioPluginManifest.activationEvents`,后者正是 `defineStudioPlugin` 的入参),所以没有"死侧"可删。v17 统一到**结构化形状**:`./studio` 现在 re-export `./kernel` 的那一份声明,平台只剩一套激活词表。
15+
16+
**为什么是结构化的那一侧赢。** 字符串那一侧更眼熟(照搬 VS Code),但它什么都不校验:`z.string()` 接受 `''``'banana'`,以及真正要命的 `'onMetadatType:flow'` —— 这个文件文档里列的词表(`*``onMetadataType:``onCommand:``onView:`)只活在散文里,拼错永远静默通过。结构化形状用 enum 在**创作时**就把触发器类型钉死,这才是声明它的意义。
17+
18+
## FROM → TO
19+
20+
`activationEvents` 的每一项从字符串变成对象。冒号前的段成为 `type`,冒号后的段成为 `pattern`:
21+
22+
```ts
23+
// FROM (v16 及以前,@objectstack/spec/studio)
24+
defineStudioPlugin({
25+
id: 'objectstack.flow-designer',
26+
name: 'Flow Designer',
27+
activationEvents: ['onMetadataType:flow'],
28+
});
29+
30+
// TO (v17+)
31+
defineStudioPlugin({
32+
id: 'objectstack.flow-designer',
33+
name: 'Flow Designer',
34+
activationEvents: [{ type: 'onMetadataType', pattern: 'flow' }],
35+
});
36+
```
37+
38+
逐条对照:
39+
40+
| FROM | TO |
41+
|:--|:--|
42+
| `'*'` | `{ type: 'onStartup', pattern: '*' }` |
43+
| `'onMetadataType:flow'` | `{ type: 'onMetadataType', pattern: 'flow' }` |
44+
| `'onCommand:myPlugin.doSomething'` | `{ type: 'onCommand', pattern: 'myPlugin.doSomething' }` |
45+
| `'onView:myPlugin.myPanel'` | `{ type: 'onView', pattern: 'myPlugin.myPanel' }` |
46+
47+
`StudioPluginManifest.activationEvents` 的默认值随之从 `['*']` 变为 `[{ type: 'onStartup', pattern: '*' }]``'*'` 没有拿到独立的 `type`:它一直就是"立即激活",而 kernel 侧的 `onStartup` 本来就是这个意思,再加一个枚举值只会造出两个同义词。
48+
49+
## 词表 = 两侧并集,没有能力被静默拿掉
50+
51+
enum 取**两侧 v17 前词表的并集**,共 9 个值:
52+
53+
|| 来源 |
54+
|:--|:--|
55+
| `onCommand` | kernel enum + studio 文档 `onCommand:myPlugin.doSomething` |
56+
| `onRoute` | kernel enum |
57+
| `onObject` | kernel enum |
58+
| `onEvent` | kernel enum |
59+
| `onService` | kernel enum |
60+
| `onSchedule` | kernel enum |
61+
| `onStartup` | kernel enum;同时是 studio `'*'` 的落点 |
62+
| `onMetadataType` | studio 文档/测试 `onMetadataType:object` —— kernel 原本没有 |
63+
| `onView` | studio 文档/测试 `onView:myPlugin.myPanel` —— kernel 原本没有 |
64+
65+
**未采纳**:cloud-v1 未发布的 marketplace runtime 里的 `priority``onInstall``onWebhook`。四仓无人读它们,而新增一个 declared-but-unenforced 的键正是 ADR-0049 在清的债 —— 等真有执行点再单独提。
66+
67+
## 迁移是手工的,但失败是响亮的
68+
69+
**没有随附 ADR-0087 conversion,因为写不出能跑到的那一个。** conversion 层(`applyConversions`)接在 `normalizeStackInput` 上,只走 stack 树;而 `StudioPluginManifestSchema``DynamicLoadRequestSchema` 都是**根 schema**,没有任何父 schema 嵌入它们(前者由 `defineStudioPlugin` 直接 parse,后者是运行时请求载荷),都不在 stack 里。伪造一个永远不会命中的 conversion 只会制造"已自动迁移"的假象。
70+
71+
手工迁移步骤:按上表把每个字符串改写成 `{ type, pattern }`**漏改会在 parse 处响亮失败** —— `StudioPluginManifestSchema``strictObject`,字符串遇到对象 schema 直接抛错,不存在静默吞掉或强制转换。
72+
73+
## 其它影响
74+
75+
- `@objectstack/spec/studio` 现在**额外导出** `ActivationEvent` 类型(此前只有 schema),与 `./kernel` 指向同一份声明。
76+
- `ActivationEventSchema``dual-source-exports.baseline.json` 移除,基线 22 → 21。
77+
- 零可作者化 key 消失、零 tombstone:kernel`ActivationEvent:type` / `:pattern` 原样存活,`studio/ActivationEvent` 侧新增 2 个 key(字符串没有 key,对象有),属 `gen:schema` 允许的**新增**
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/platform-objects": minor
4+
"@objectstack/plugin-security": minor
5+
"@objectstack/plugin-approvals": minor
6+
"@objectstack/plugin-hono-server": minor
7+
"@objectstack/service-messaging": minor
8+
---
9+
10+
feat(spec)!: retire the overloaded `managedBy: 'system'` bucket — the residue becomes `system-data` (#3355)
11+
12+
**FROM → TO: `managedBy: 'system'``managedBy: 'system-data'`.** One-line fix:
13+
rename the value. Nothing else about the object changes. `os migrate meta --from 16`
14+
rewrites it for you; stored metadata is CONVERTED by the ADR-0087 entry
15+
`object-managed-by-system-to-system-data`, never silently reinterpreted.
16+
17+
ADR-0103 split the overloaded `system` bucket in v16, and it split it
18+
**additively**: the 20 engine-owned objects moved to the new explicit
19+
`engine-owned`, while the 8 admin/user-writable ones — the RBAC link tables
20+
(`sys_user_position`, `sys_user_permission_set`, `sys_position_permission_set`),
21+
`sys_user_preference`, `sys_approval_delegation`, and the three messaging config
22+
grids — stayed behind on `system`. That was the right move for a v16 that could
23+
not break authors, but it left the enum in a state where the surviving value
24+
names the half that had already moved out: `system` sitting on precisely the
25+
objects a user writes.
26+
27+
That is not a cosmetic complaint. An author choosing between `system` and
28+
`engine-owned` had nothing in the vocabulary to choose *on*, so the bucket was
29+
re-overloadable by anyone reading the name in good faith — a model author most
30+
of all, since "system table" reads as "the engine owns this" in every other
31+
codebase. `system-data` states both boundaries explicitly: the **schema** is the
32+
platform's (versus `platform`, which is tenant-modelled), the **data** is the
33+
admin's or the user's (versus `engine-owned`, where the engine owns both).
34+
35+
Because v16 already drained the engine side, the conversion is a **one-to-one
36+
mechanical value rename** with no judgement call — by construction every
37+
remaining `system` declaration is writable platform data.
38+
39+
**One deliberate consequence — the affordance default flips.** `system` defaulted
40+
LOCKED and each of the 8 objects re-opened its writes with a
41+
`userActions: { create: true, edit: true, delete: true }` block. `system-data`
42+
defaults **WRITABLE** (full CRUD), because a bucket that exists to say "the data
43+
is yours" should not make every member ask for it back. Those blocks are now
44+
redundant and have been deleted from the 8 platform objects; keep `userActions`
45+
only to **NARROW**. If you converted an object that carried no `userActions`, it
46+
gains the generic affordances — the honest reading of the bucket it moved into.
47+
48+
**No enforcement moves.** The engine write guard, the `DelegatedAdminGate`, RLS
49+
and permission sets all adjudicate off resolved affordances and the principal,
50+
never off the bucket name. `system-data` simply joins `platform` / `config` as a
51+
bucket the fail-closed guard does not cover, because a writable default has
52+
nothing to close on. The 8 objects passed that guard before (via `userActions`)
53+
and pass it now (via the bucket default), for the same resolved-affordance
54+
reason.
55+
56+
`'system'` is **retired from the load path**: the enum rejects it with a
57+
prescription naming `system-data` and the one-line fix. Absorbing it silently at
58+
load would leave every author still writing the name this rename exists to
59+
unteach.

content/docs/data-modeling/objects.mdx

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,8 @@ indexes: [
215215
| Property | Type | Description |
216216
| :--- | :--- | :--- |
217217
| `isSystem` | `boolean` | System object, protected from deletion (default: `false`) |
218-
| `managedBy` | `enum` | Lifecycle bucket that sets the default CRUD affordances and write policy — `'platform'` (default), `'config'`, `'system'`, `'engine-owned'`, `'append-only'`, `'better-auth'`. See [Lifecycle bucket](#lifecycle-bucket-managedby) below. |
219-
| `userActions` | `object` | Per-object override of the CRUD affordances the `managedBy` default implies — `{ create?, edit?, delete?, import?, exportCsv? }`. This is what makes a `system`/`append-only` object admin/user-writable. See [Lifecycle bucket](#lifecycle-bucket-managedby). |
218+
| `managedBy` | `enum` | Lifecycle bucket that sets the default CRUD affordances and write policy — `'platform'` (default), `'config'`, `'system-data'`, `'engine-owned'`, `'append-only'`, `'better-auth'`. See [Lifecycle bucket](#lifecycle-bucket-managedby) below. |
219+
| `userActions` | `object` | Per-object override of the CRUD affordances the `managedBy` default implies — `{ create?, edit?, delete?, import?, exportCsv? }`. This is what NARROWS a `system-data` object, or opens a verb on an `engine-owned`/`append-only` one. See [Lifecycle bucket](#lifecycle-bucket-managedby). |
220220
| `sharingModel` | `enum` | Org-Wide Default record visibility (ADR-0055/0056/0090). Canonical four only: `'private'`, `'public_read'`, `'public_read_write'`, `'controlled_by_parent'` (detail visibility derived from its master). The legacy aliases (`'read'`, `'read_write'`, `'full'`) were removed from the enum (ADR-0090 D4) — authoring rejects them. Unset on a custom object resolves to `'private'` (ADR-0090 D1) |
221221
| `ownership` | `enum` | Record-ownership model: `'user'` (default — injects the reassignable `owner_id` lookup, engaging owner-scoped RLS, "My" views and owner reports), `'org'`, or `'none'` (no per-record owner — Dataverse-style catalog / junction tables, skips `owner_id`). Distinct from the package `own`/`extend` contribution kind. |
222222
| `validations` | `ValidationRule[]` | Object-level validation rules (see [Validation](/docs/data-modeling/validation)) |
@@ -233,44 +233,58 @@ bare bucket string.
233233
| :--- | :--- |
234234
| `platform` | **Default.** User-owned business data — full New / Import / Edit / Delete. |
235235
| `config` | Admin-authored configuration — New / Edit / Delete, no CSV import. |
236-
| `system` | Platform-defined schema holding **admin/user-writable data** (RBAC link tables, preferences, messaging config). Locked by default; each object opens its writes via `userActions`. |
236+
| `system-data` | Platform-defined schema holding **admin/user-writable data** (RBAC link tables, preferences, messaging config). Full CRUD by default; narrow it with `userActions`. |
237237
| `engine-owned` | Runtime rows a platform service owns end to end — generic CRUD hidden, exposed `['get', 'list']` only, **no user writes ever**. |
238238
| `append-only` | Immutable audit trail — View + Export only. |
239239
| `better-auth` | Identity tables owned by the better-auth driver — generic user-context CRUD is suppressed; mutations flow through the auth API (sign-in, invite, reset). |
240240

241-
**`engine-owned` vs. writable `system` objects (ADR-0103).** Two buckets share
242-
the same locked default matrix but say different things:
241+
**`engine-owned` vs. `system-data` (ADR-0103, #3355).** Both hold a
242+
platform-defined schema no tenant may model; they differ on who owns the *rows*:
243243

244244
- **`engine-owned`** — jobs, notifications, approval runtime rows,
245245
`sys_record_share`, `sys_automation_run`, the metadata store, `sys_secret`,
246246
audit trails — written only by their owning service under a system context,
247-
never through the generic `/data` API. A fail-closed guard
248-
(`assertEngineOwnedWriteAllowed`) rejects user-context generic writes to them.
249-
- **`system`** — platform-defined schema holding admin/user-writable *data*: the
250-
RBAC link tables, `sys_user_preference`, `sys_approval_delegation`, the
251-
messaging config grids. These declare `userActions` to open the writes they
252-
legitimately take:
247+
never through the generic `/data` API. Locked by default, and a fail-closed
248+
guard (`assertEngineOwnedWriteAllowed`) rejects user-context generic writes.
249+
- **`system-data`** — the schema is the platform's, the *data* is the admin's or
250+
the user's: the RBAC link tables, `sys_user_preference`,
251+
`sys_approval_delegation`, the messaging config grids. Full CRUD by default,
252+
and no write guard covers the bucket — a writable default has nothing to fail
253+
closed on:
253254

254255
```typescript
255256
export const SysUserPreference = ObjectSchema.create({
256257
name: 'sys_user_preference',
257-
managedBy: 'system',
258-
// Affordance only — RLS / delegated administration is the actual authz.
259-
userActions: { create: true, edit: true, delete: true },
258+
// Full CRUD by default — no `userActions` needed. RLS / delegated
259+
// administration is the actual authz.
260+
managedBy: 'system-data',
260261
//
261262
});
262263
```
263264

264-
The same override works on `append-only`. `userActions` is an *affordance*
265-
declaration; the real authorization for these rows is still enforced by RLS,
266-
delegated administration, and permission sets.
265+
Pick between them on the *data*, not the table name: if no user ever writes a
266+
row through the generic API, it is `engine-owned`. Declaring `system-data` on an
267+
object whose resolved affordances grant no create, edit or delete is a
268+
contradiction, and `ObjectSchema.create()` refuses it.
269+
270+
`userActions` NARROWS `system-data` (an editable-only grid: `{ create: false,
271+
delete: false }`) and OPENS a verb on `append-only`. Either way it is an
272+
*affordance* declaration; the real authorization for these rows is still
273+
enforced by RLS, delegated administration, and permission sets.
274+
275+
<Callout type="info">
276+
**Upgrading from v16.** `managedBy: 'system'` was retired in protocol 17 —
277+
rename it to `'system-data'`, or run `os migrate meta --from 16`. Because the
278+
new bucket defaults to full CRUD, a `userActions` block that existed only to
279+
re-open create/edit/delete is now redundant and can be deleted.
280+
</Callout>
267281

268282
<Callout type="warn">
269283
A managed object may not advertise `enable.apiMethods` verbs its resolved
270284
affordances forbid — the registry strips the contradiction at registration
271-
(`reconcileManagedApiMethods`, ADR-0049). To expose a generic write verb on a
272-
`system`/`append-only` object, declare the matching `userActions` rather than
273-
listing the verb in `apiMethods`.
285+
(`reconcileManagedApiMethods`, ADR-0049). To expose a generic write verb on an
286+
`engine-owned`/`append-only` object, declare the matching `userActions` rather
287+
than listing the verb in `apiMethods`.
274288
</Callout>
275289

276290
## Naming Conventions

content/docs/plugins/development.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ export const manifest = defineStudioPlugin({
384384
name: 'Flow Designer',
385385
version: '2.0.0',
386386
description: 'Visual flow builder for automation workflows',
387-
activationEvents: ['onMetadataType:flow'],
387+
activationEvents: [{ type: 'onMetadataType', pattern: 'flow' }],
388388

389389
contributes: {
390390
metadataViewers: [{

content/docs/references/data/object.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ const result = ApiMethod.parse(data);
115115
| **description** | `string` | optional | Developer documentation / description |
116116
| **icon** | `string` | optional | Icon name (Lucide/Material) for UI representation |
117117
| **isSystem** | `boolean` | optional | Is system object (protected from deletion; defaults its org-wide sharing to public when no sharingModel is set — plugin-sharing) |
118-
| **managedBy** | `Enum<'platform' \| 'config' \| 'system' \| 'engine-owned' \| 'append-only' \| 'better-auth'>` | optional | Lifecycle bucket — platform (user CRUD) \| config (admin authored) \| system (engine-managed schema, writable via userActions) \| engine-owned (engine owns the lifecycle, no user writes) \| append-only (audit) \| better-auth (identity). UI clients honour the resolved affordance matrix. |
118+
| **managedBy** | `Enum<'platform' \| 'config' \| 'system-data' \| 'engine-owned' \| 'append-only' \| 'better-auth'>` | optional | Lifecycle bucket — platform (user CRUD) \| config (admin authored) \| system-data (platform-defined schema, admin/user-writable data) \| engine-owned (engine owns the lifecycle, no user writes) \| append-only (audit) \| better-auth (identity). UI clients honour the resolved affordance matrix. |
119119
| **ownership** | `Enum<'user' \| 'org' \| 'none'>` | optional | Record-ownership model: user (default — injects reassignable owner_id) \| org \| none (no per-record owner, skips owner_id). Distinct from the package own/extend contribution kind. |
120120
| **userActions** | `{ create?: boolean; import?: boolean; edit?: boolean \| { enabled?: boolean; visibleWhen?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; disabledWhen?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } }; delete?: boolean \| { enabled?: boolean; visibleWhen?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; disabledWhen?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } }; … }` | optional | Per-object override of the resolved CRUD affordance matrix. |
121121
| **systemFields** | `'false' \| { tenant?: boolean; audit?: boolean }` | optional | Opt out of, or selectively disable, registry-level system-field auto-injection. |

content/docs/references/kernel/plugin-runtime.mdx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,27 @@ This protocol enables:
3838
## TypeScript Usage
3939

4040
```typescript
41-
import { DynamicLoadRequestSchema, DynamicPluginOperationSchema, DynamicPluginResultSchema, DynamicUnloadRequestSchema, PluginSourceSchema } from '@objectstack/spec/kernel';
42-
import type { DynamicLoadRequest, DynamicPluginOperation, DynamicPluginResult, DynamicUnloadRequest, PluginSource } from '@objectstack/spec/kernel';
41+
import { ActivationEventSchema, DynamicLoadRequestSchema, DynamicPluginOperationSchema, DynamicPluginResultSchema, DynamicUnloadRequestSchema, PluginSourceSchema } from '@objectstack/spec/kernel';
42+
import type { ActivationEvent, DynamicLoadRequest, DynamicPluginOperation, DynamicPluginResult, DynamicUnloadRequest, PluginSource } from '@objectstack/spec/kernel';
4343

4444
// Validate data
45-
const result = DynamicLoadRequestSchema.parse(data);
45+
const result = ActivationEventSchema.parse(data);
4646
```
4747

48+
---
49+
50+
## ActivationEvent
51+
52+
Lazy activation trigger for a dynamic plugin
53+
54+
### Properties
55+
56+
| Property | Type | Required | Description |
57+
| :--- | :--- | :--- | :--- |
58+
| **type** | `Enum<'onCommand' \| 'onRoute' \| 'onObject' \| 'onEvent' \| 'onService' \| 'onSchedule' \| 'onStartup' \| 'onMetadataType' \| 'onView'>` || Trigger type for lazy activation |
59+
| **pattern** | `string` || Match pattern for the activation trigger |
60+
61+
4862
---
4963

5064
## DynamicLoadRequest
@@ -57,7 +71,7 @@ Request to dynamically load a plugin at runtime
5771
| :--- | :--- | :--- | :--- |
5872
| **pluginId** | `string` || Unique plugin identifier |
5973
| **source** | `{ type: Enum<'npm' \| 'local' \| 'url' \| 'registry' \| 'git'>; location: string; version?: string; integrity?: string }` || Plugin source location for dynamic resolution |
60-
| **activationEvents** | `{ type: Enum<'onCommand' \| 'onRoute' \| 'onObject' \| 'onEvent' \| 'onService' \| 'onSchedule' \| 'onStartup'>; pattern: string }[]` | optional | Lazy activation triggers; if omitted plugin starts immediately |
74+
| **activationEvents** | `{ type: Enum<'onCommand' \| 'onRoute' \| 'onObject' \| 'onEvent' \| 'onService' \| 'onSchedule' \| 'onStartup' \| 'onMetadataType' \| 'onView'>; pattern: string }[]` | optional | Lazy activation triggers; if omitted plugin starts immediately |
6175
| **config** | `Record<string, any>` | optional | Runtime configuration overrides |
6276
| **priority** | `integer` || Loading priority (lower is higher) |
6377
| **sandbox** | `boolean` || Run in an isolated sandbox |

0 commit comments

Comments
 (0)