diff --git a/.changeset/spec-13-permission-model-v2.md b/.changeset/spec-13-permission-model-v2.md new file mode 100644 index 0000000000..a9f21ffaf4 --- /dev/null +++ b/.changeset/spec-13-permission-model-v2.md @@ -0,0 +1,21 @@ +--- +'@object-ui/app-shell': major +'@object-ui/components': patch +'@object-ui/console': patch +'@object-ui/types': patch +--- + +Adopt `@objectstack/spec` 13 (ADR-0090 Permission Model v2) across the workspace. + +Every workspace package now depends on `@objectstack/spec` ^13.0.0 — the v2 major that renames role → position (D3), removes the profile concept (D2), makes OWD default to `private` when unset (D1), and drops the legacy `read`/`read_write`/`full` sharing aliases (D4). UI fallout fixed in the same sweep: + +- **clientValidation**: the `role` draft-schema loader is now `position` → `PositionSchema` (fixes the `RoleSchema does not exist` build break, #2365); the dead `profile` loader is removed (D2). +- **Studio previews**: `RolePreview` → `PositionPreview` (flat — positions carry no hierarchy; the old parent-chain breadcrumb and "assign to a Profile" copy are gone). Legacy `role`/`profile` preview keys stay registered for pre-v2 backends. +- **OWD control** (`ObjectSettingsPanel`): removed the now-dead alias normalization (spec 13 rejects the aliases at authoring time) and the amber "fully public" warning — an unset sharing model now defaults to Private (D1), and the copy says so in both locales. +- **Fallback schemas / anchors / samples**: `position` replaces the hierarchical `role` fallback schema; `isProfile` dropped from the permission create-anchor and previews samples; permission-set viewer no longer renders a profile badge; console System hub counts `sys_position` instead of the removed `sys_role`. +- **Studio i18n**: type labels `Role/角色` → `Position/岗位`, `profile` label removed, Access-pillar heading and sharing copy rewritten to the v2 vocabulary. +- `@object-ui/types` now exports `SubmitBehavior` (was defined but missing from the public surface, breaking `@object-ui/plugin-form`'s re-export under a clean build). +- **External OWD dial (D11)**: the object Settings sharing card gains an `externalSharingModel` select (portal/partner baseline) with an inline wider-than-internal warning mirroring the publish-time lint. +- **Permission matrix OWD badges**: every object row now shows its record-level baseline (`OWD Public read`, `Ext Private`, or `OWD Private (default)` for the D1 fail-closed unset case) so grant edits carry their record-reach context. + +The flow designer's approval assignee `role` kind is intentionally unchanged — spec 13 keeps it as the sole D3 exception (better-auth `sys_member.role` org-membership tier). diff --git a/apps/console/package.json b/apps/console/package.json index 0a6b27bc98..ad076cd16b 100644 --- a/apps/console/package.json +++ b/apps/console/package.json @@ -88,7 +88,7 @@ "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", "@objectstack/client": "^12.3.0", - "@objectstack/spec": "^12.3.0", + "@objectstack/spec": "^13.0.0", "@tailwindcss/postcss": "^4.3.2", "@tailwindcss/typography": "^0.5.20", "@testing-library/jest-dom": "^6.9.1", diff --git a/apps/console/src/pages/system/SystemHubPage.tsx b/apps/console/src/pages/system/SystemHubPage.tsx index a1df112778..b0a16a7c89 100644 --- a/apps/console/src/pages/system/SystemHubPage.tsx +++ b/apps/console/src/pages/system/SystemHubPage.tsx @@ -12,7 +12,7 @@ * * Unified entry point for all system administration functions. * Displays card-based overview linking to Apps, Users, Organizations, - * Roles, Permissions, Audit Log, Profile management pages, and + * Positions, Permissions, Audit Log, Profile management pages, and * dynamically generated metadata type cards from the registry. */ @@ -65,7 +65,7 @@ export function SystemHubPage() { const [counts, setCounts] = useState>({ users: null, orgs: null, - roles: null, + positions: null, permissions: null, auditLogs: null, }); @@ -76,17 +76,17 @@ export function SystemHubPage() { setLoading(true); try { // TODO: Replace with count-specific API endpoint when available - const [usersRes, orgsRes, rolesRes, permsRes, logsRes] = await Promise.all([ + const [usersRes, orgsRes, positionsRes, permsRes, logsRes] = await Promise.all([ dataSource.find('sys_user').catch(() => ({ data: [] })), dataSource.find('sys_org').catch(() => ({ data: [] })), - dataSource.find('sys_role').catch(() => ({ data: [] })), + dataSource.find('sys_position').catch(() => ({ data: [] })), dataSource.find('sys_permission').catch(() => ({ data: [] })), dataSource.find('sys_audit_log').catch(() => ({ data: [] })), ]); setCounts({ users: usersRes.data?.length ?? 0, orgs: orgsRes.data?.length ?? 0, - roles: rolesRes.data?.length ?? 0, + positions: positionsRes.data?.length ?? 0, permissions: permsRes.data?.length ?? 0, auditLogs: logsRes.data?.length ?? 0, }); @@ -148,12 +148,12 @@ export function SystemHubPage() { count: counts.orgs, }, { - title: 'Roles', - description: 'Configure roles and access levels', + title: 'Positions', + description: 'Configure positions and access levels', icon: Shield, - href: `${basePath}/system/roles`, - countLabel: 'roles', - count: counts.roles, + href: `${basePath}/system/positions`, + countLabel: 'positions', + count: counts.positions, }, { title: 'Permissions', @@ -223,7 +223,7 @@ export function SystemHubPage() {

System Settings

- Manage applications, users, roles, permissions, and system configuration + Manage applications, users, positions, permissions, and system configuration

diff --git a/apps/console/src/pages/system/systemObjects.ts b/apps/console/src/pages/system/systemObjects.ts index c5fd6cf799..4a1c22f3f9 100644 --- a/apps/console/src/pages/system/systemObjects.ts +++ b/apps/console/src/pages/system/systemObjects.ts @@ -58,15 +58,15 @@ export const systemObjects = [ ], }, { - name: 'sys_role', - label: 'Roles', + name: 'sys_position', + label: 'Positions', icon: 'Shield', fields: [ { name: 'id', type: 'text', label: 'ID', readonly: true }, - { name: 'name', type: 'text', label: 'Role Name', required: true }, + { name: 'name', type: 'text', label: 'Position Name', required: true }, { name: 'description', type: 'textarea', label: 'Description' }, { name: 'permissions', type: 'text', label: 'Permissions' }, - { name: 'isSystem', type: 'boolean', label: 'System Role', readonly: true }, + { name: 'isSystem', type: 'boolean', label: 'System Position', readonly: true }, { name: 'userCount', type: 'number', label: 'Users', readonly: true }, { name: 'createdAt', type: 'datetime', label: 'Created At', readonly: true }, ], @@ -74,7 +74,7 @@ export const systemObjects = [ views: [ { name: 'all', - label: 'All Roles', + label: 'All Positions', type: 'grid', columns: ['name', 'description', 'isSystem', 'userCount'], }, diff --git a/apps/console/src/preview-samples.ts b/apps/console/src/preview-samples.ts index ea29490a20..309bf9c973 100644 --- a/apps/console/src/preview-samples.ts +++ b/apps/console/src/preview-samples.ts @@ -238,7 +238,6 @@ export const SAMPLES: Record> = { permission: { name: 'sales_rep', label: 'Sales Rep', - isProfile: false, objects: { sales_order: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: false }, account: { allowRead: true, allowCreate: false, allowEdit: true, allowDelete: false }, @@ -250,23 +249,10 @@ export const SAMPLES: Record> = { tabPermissions: { crm: 'visible', admin: 'hidden' }, }, - profile: { - name: 'standard_user', - label: 'Standard User', - isProfile: true, - objects: { - account: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: false }, - sales_order: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: false }, - }, - systemPermissions: ['runReports'], - tabPermissions: { crm: 'visible' }, - }, - - role: { + position: { name: 'sales_manager', label: 'Sales Manager', description: 'Manages the regional sales team.', - parent: 'vp_sales', }, datasource: { diff --git a/apps/site/package.json b/apps/site/package.json index b5c3c001d7..33221eb1eb 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -31,7 +31,7 @@ "@object-ui/plugin-view": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^12.3.0", + "@objectstack/spec": "^13.0.0", "fumadocs-core": "16.10.7", "fumadocs-mdx": "15.0.13", "fumadocs-ui": "16.10.7", diff --git a/packages/app-shell/package.json b/packages/app-shell/package.json index 9c89960834..42117d5242 100644 --- a/packages/app-shell/package.json +++ b/packages/app-shell/package.json @@ -49,7 +49,7 @@ "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", "@objectstack/lint": "^12.6.0", - "@objectstack/spec": "^12.3.0", + "@objectstack/spec": "^13.0.0", "@sentry/react": "^10.63.0", "jsonc-parser": "^3.3.1", "lucide-react": "^1.22.0", diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.scope.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.scope.test.tsx index 4cb4a02eb1..d86134f3a1 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.scope.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.scope.test.tsx @@ -230,11 +230,34 @@ describe('PermissionMatrixEditor — private posture badge (ADR-0066 ④)', () = await screen.findByText('Account'); // The private object's row carries the badge (with the wildcard hint)… - const badges = screen.getAllByText('Private'); + // (every row also shows an ADR-0090 OWD badge, so match the exact posture + // badge text, not the substring) + const badges = screen.getAllByText('Private', { exact: true }); expect(badges).toHaveLength(1); expect(badges[0].closest('tr')!.textContent).toContain('a_account'); expect(badges[0]).toHaveAttribute('title', expect.stringContaining('wildcard')); - // …and the public row does not. - expect(screen.getByText('a_contact').closest('tr')!.textContent).not.toContain('Private'); + // …and the public row does not carry the posture badge. + const contactRow = screen.getByText('a_contact').closest('tr')!; + expect(within(contactRow).queryByText('Private', { exact: true })).toBeNull(); + }); + + it('shows an OWD badge on every row (authored value or the D1 private default)', async () => { + const server = freshServer(); + server.packageObjects = [ + { name: 'a_account', label: 'Account', sharingModel: 'public_read', externalSharingModel: 'private' } as any, + { name: 'a_contact' }, + ]; + clientImpl = makeClient(server); + renderMatrix(); + + await screen.findByText('Account'); + // Authored OWD pair renders both dials… + const accountRow = screen.getByText('Account').closest('tr')!; + expect(accountRow.textContent).toContain('OWD Public read'); + expect(accountRow.textContent).toContain('Ext Private'); + // …an unset OWD renders the fail-closed D1 default and no Ext badge. + const contactRow = screen.getByText('a_contact').closest('tr')!; + expect(contactRow.textContent).toContain('OWD Private (default)'); + expect(contactRow.textContent).not.toContain('Ext '); }); }); diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx index 79ab7a1d14..9372d6b87f 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx @@ -89,6 +89,25 @@ interface ObjectSummary { * admins editing the matrix know a wildcard-only set does not reach it. */ accessDefault?: 'public' | 'private'; + /** + * [ADR-0090 D1/D11] The object's authored OWD pair. Record-level baseline + * context for the grants edited here: object CRUD in this matrix gates the + * operation, the OWD decides WHICH records it reaches (own vs org-wide). + * `owd` unset renders as the D1 fail-closed default (private). + */ + owd?: string; + owdExternal?: string; +} + +/** Localized short label for an OWD value; falls back to the raw value. */ +function owdLabel(t: (k: string) => string, value: string): string { + const key: Record = { + private: 'perm.owd.private', + public_read: 'perm.owd.public_read', + public_read_write: 'perm.owd.public_read_write', + controlled_by_parent: 'perm.owd.controlled_by_parent', + }; + return key[value] ? t(key[value]) : value; } interface FieldSummary { @@ -199,6 +218,9 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved, name: String(item?.name ?? ''), label: item?.label, accessDefault: item?.access?.default as ObjectSummary['accessDefault'], + owd: typeof item?.sharingModel === 'string' ? item.sharingModel : undefined, + owdExternal: + typeof item?.externalSharingModel === 'string' ? item.externalSharingModel : undefined, }; }) .filter((o) => !!o.name) @@ -676,6 +698,22 @@ function PermissionTable({ {t('perm.posture.private')} )} + + {`OWD ${o.owd ? owdLabel(t, o.owd) : t('perm.owd.defaultPrivate')}`} + + {o.owdExternal && ( + + {`Ext ${owdLabel(t, o.owdExternal)}`} + + )} {objectActions.map((a) => ( diff --git a/packages/app-shell/src/views/metadata-admin/anchors.ts b/packages/app-shell/src/views/metadata-admin/anchors.ts index bb48fd8a5d..3474a0740d 100644 --- a/packages/app-shell/src/views/metadata-admin/anchors.ts +++ b/packages/app-shell/src/views/metadata-admin/anchors.ts @@ -338,7 +338,7 @@ export function registerBuiltinAnchors(): void { groupLabel: 'Permissions', order: 60, }], - createFields: ['label', 'name', 'isProfile'], + createFields: ['label', 'name'], createDerive: [ { from: 'label', to: 'name', transform: 'slugify', untilUserEdits: true }, ], diff --git a/packages/app-shell/src/views/metadata-admin/clientValidation.ts b/packages/app-shell/src/views/metadata-admin/clientValidation.ts index a012df8b14..60091a370d 100644 --- a/packages/app-shell/src/views/metadata-admin/clientValidation.ts +++ b/packages/app-shell/src/views/metadata-admin/clientValidation.ts @@ -90,15 +90,16 @@ const LOADERS: Record = { // security // NOTE: use PermissionSetSchema from /security, NOT PluginPermissionSchema from /kernel — // the kernel one is the plugin-sandbox permission ({id,resource,actions}), not the - // metadata permission set ({name,isProfile,objects,fields}). See + // metadata permission set ({name,objects,fields}). See // packages/spec/src/kernel/metadata-type-schemas.ts for the canonical mapping. permission: async () => (await import('@objectstack/spec/security')).PermissionSetSchema as unknown as ZodLikeSchema, - profile: async () => (await import('@objectstack/spec/security')).PermissionSetSchema as unknown as ZodLikeSchema, // `policy` intentionally omitted — spec 11.2.0 dropped `PolicySchema` and the metadata-type // registry has no `policy` schema; drafts fall through to server-side validation (see top). + // `profile` intentionally omitted — ADR-0090 D2 removed the profile concept (spec 13); + // `role` is likewise gone, renamed to `position` (ADR-0090 D3). // identity - role: async () => (await import('@objectstack/spec/identity')).RoleSchema as unknown as ZodLikeSchema, + position: async () => (await import('@objectstack/spec/identity')).PositionSchema as unknown as ZodLikeSchema, // api api: async () => (await import('@objectstack/spec/api')).ApiEndpointSchema as unknown as ZodLikeSchema, diff --git a/packages/app-shell/src/views/metadata-admin/default-schemas.ts b/packages/app-shell/src/views/metadata-admin/default-schemas.ts index 40c5f262e1..e76352c779 100644 --- a/packages/app-shell/src/views/metadata-admin/default-schemas.ts +++ b/packages/app-shell/src/views/metadata-admin/default-schemas.ts @@ -42,38 +42,16 @@ const headerProps = { /** Per-type fallback schemas. */ const SCHEMAS: Record> = { - role: { + // Position is deliberately flat (ADR-0090 D3) — no level, no parent; the + // hierarchy lives on the business-unit tree. `role`/`profile` schemas are + // gone with the spec-13 vocabulary (positions replace roles; D2 removed + // the profile concept). + position: { type: 'object', - title: 'Role', + title: 'Position', required: ['name'], properties: { ...headerProps, - level: { - type: 'string', - title: 'Level', - description: 'Coarse seniority tier (used by UI / sort order).', - enum: ['member', 'lead', 'manager', 'director', 'executive', 'admin'], - }, - parentRole: { - type: 'string', - title: 'Parent Role', - description: 'Optional parent role name for hierarchy.', - }, - }, - }, - - profile: { - type: 'object', - title: 'Profile', - required: ['name'], - properties: { - ...headerProps, - isProfile: { type: 'boolean', title: 'Is profile', description: 'Profiles are mutually exclusive (one per user).' }, - objects: { - type: 'object', - title: 'Object Permissions', - description: 'Edit this via the Permission Matrix tab for a row-by-row UI.', - }, }, }, diff --git a/packages/app-shell/src/views/metadata-admin/i18n.ts b/packages/app-shell/src/views/metadata-admin/i18n.ts index ee95be7e99..e7a2d39d9f 100644 --- a/packages/app-shell/src/views/metadata-admin/i18n.ts +++ b/packages/app-shell/src/views/metadata-admin/i18n.ts @@ -52,10 +52,9 @@ const TYPE_LABELS_EN: Record = { service: 'Service', email_template: 'Email Template', book: 'Documentation Book', - // Security + // Security — ADR-0090: profile removed (D2), role renamed to position (D3). permission: 'Permission Set', - profile: 'Profile', - role: 'Role', + position: 'Position', // AI agent: 'AI Agent', tool: 'AI Tool', @@ -99,8 +98,7 @@ const TYPE_LABELS_ZH: Record = { email_template: '邮件模板', book: '文档手册', permission: '权限集', - profile: '配置文件', - role: '角色', + position: '岗位', agent: 'AI 智能体', tool: 'AI 工具', skill: 'AI 技能', @@ -706,6 +704,13 @@ const ENGINE_STRINGS_EN: Record = { 'perm.col.bulk': 'Bulk', 'perm.posture.private': 'Private', 'perm.posture.private.tip': 'Secure-by-default object (access.default = private): a permission set’s “*” wildcard grant does NOT cover it. Access needs an explicit grant on this object (or View/Modify All Records). Grants you edit here apply normally.', + 'perm.owd.tip': 'Org-wide default (sharingModel): the record-level visibility baseline for internal users, applied before positions and sharing rules. Object CRUD here gates the operation; the OWD decides which records it reaches (own vs org-wide).', + 'perm.owd.ext.tip': 'External OWD (externalSharingModel): the baseline for portal / partner principals — never wider than the internal model (ADR-0090 D11).', + 'perm.owd.defaultPrivate': 'Private (default)', + 'perm.owd.private': 'Private', + 'perm.owd.public_read': 'Public read', + 'perm.owd.public_read_write': 'Public read/write', + 'perm.owd.controlled_by_parent': 'By parent', 'perm.bulk.read': 'R', 'perm.bulk.crud': 'CRUD', 'perm.bulk.all': 'All', @@ -1049,23 +1054,30 @@ const ENGINE_STRINGS_EN: Record = { 'engine.studio.settings.addFieldOption': '+ Add field…', 'engine.studio.settings.undeclared': '(not declared — surfaces pick heuristically)', 'engine.studio.settings.sharing': 'Record sharing (OWD)', - 'engine.studio.settings.sharingHint': 'Baseline record visibility applied before roles and sharing rules (ADR-0056)', + 'engine.studio.settings.sharingHint': 'Baseline record visibility applied before positions and sharing rules (ADR-0056/0090)', 'engine.studio.settings.sharingModel': 'Sharing model (sharingModel) — who can see and edit records another user owns', - 'engine.studio.settings.sharingUnset': '(not set)', + 'engine.studio.settings.sharingUnset': '(not set — defaults to Private)', 'engine.studio.settings.sharingPrivate': 'Private — owner only', 'engine.studio.settings.sharingPublicRead': 'Public read — everyone reads, only the owner writes', 'engine.studio.settings.sharingPublicReadWrite': 'Public read/write — everyone reads and writes', 'engine.studio.settings.sharingControlledByParent': 'Controlled by parent — inherited from the master record', 'engine.studio.settings.sharingDescUnset': - 'Not set — records are fully public: every user in the tenant can read AND edit all records. Choose Private to isolate records by owner.', + 'Not set — the platform defaults to Private (ADR-0090): only the owner can access records. Pick an explicit model to widen visibility.', 'engine.studio.settings.sharingDescPrivate': - 'Only the owner (plus users granted via roles or sharing rules) can access a record. Read / Edit permissions then apply to owned records only.', + 'Only the owner (plus users granted via positions or sharing rules) can access a record. Read / Edit permissions then apply to owned records only.', 'engine.studio.settings.sharingDescPublicRead': 'Every user in the tenant can read all records; only the owner can edit.', 'engine.studio.settings.sharingDescPublicReadWrite': 'Every user in the tenant can read and edit all records.', 'engine.studio.settings.sharingDescControlledByParent': 'Record visibility is inherited from the parent (master) record through its master-detail relationship.', + 'engine.studio.settings.sharingExternal': + 'External sharing (externalSharingModel) — baseline for portal / partner principals', + 'engine.studio.settings.sharingExternalUnset': '(not set — defaults to Private)', + 'engine.studio.settings.sharingExternalDesc': + 'Applies to external principals only (ADR-0090 D11). Never wider than the internal model; unset falls back to Private.', + 'engine.studio.settings.sharingExternalWider': + 'External baseline is WIDER than the internal sharing model — publishing will be rejected (ADR-0090 D11). Narrow it to the internal model or below (private < public read < public read/write).', 'engine.studio.pkg.writable': 'Writable', 'engine.studio.pkg.heading': 'Packages (apps)', 'engine.studio.pkg.none': 'No app packages yet', @@ -1213,7 +1225,7 @@ const ENGINE_STRINGS_EN: Record = { 'engine.studio.access.bannerTitle': 'This matrix lists only the objects this package declares, and “Save” merges just that slice — grants contributed by other packages are preserved. Edits are saved as package drafts and go live when you Publish the package (top bar), exactly like Data and Interfaces.', 'engine.studio.access.banner': 'This package’s objects · saved as draft', - 'engine.studio.access.heading': 'Permission sets / Profiles', + 'engine.studio.access.heading': 'Permission sets', 'engine.studio.access.search': 'Search permissions…', 'engine.studio.access.none': 'No permission sets yet — create one below', 'engine.studio.access.labelPlaceholder': 'Display name (e.g. Sales permissions)', @@ -1791,6 +1803,13 @@ const ENGINE_STRINGS_ZH: Record = { 'perm.col.bulk': '批量', 'perm.posture.private': '私有', 'perm.posture.private.tip': '默认安全对象(access.default = private):权限集的 “*” 通配授权不覆盖它,访问需要对此对象的显式授权(或查看/修改所有记录)。在此编辑的显式授权正常生效。', + 'perm.owd.tip': '组织级默认(sharingModel):内部用户的记录级可见性基线,先于岗位与共享规则生效。此处的对象 CRUD 决定能否执行操作,OWD 决定操作触达哪些记录(自己的还是全组织的)。', + 'perm.owd.ext.tip': '外部 OWD(externalSharingModel):门户 / 合作伙伴等外部主体的基线 —— 不得宽于内部模型(ADR-0090 D11)。', + 'perm.owd.defaultPrivate': 'Private(默认)', + 'perm.owd.private': 'Private 私有', + 'perm.owd.public_read': 'Public read 公共只读', + 'perm.owd.public_read_write': 'Public read/write 公共读写', + 'perm.owd.controlled_by_parent': '受父级控制', 'perm.bulk.read': '读', 'perm.bulk.crud': '增改删', 'perm.bulk.all': '全选', @@ -2132,23 +2151,30 @@ const ENGINE_STRINGS_ZH: Record = { 'engine.studio.settings.addFieldOption': '+ 添加字段…', 'engine.studio.settings.undeclared': '(未声明 —— 各处按启发式自动挑选)', 'engine.studio.settings.sharing': '记录共享模型(OWD)', - 'engine.studio.settings.sharingHint': '在角色与共享规则之前生效的记录可见性基线(ADR-0056)', + 'engine.studio.settings.sharingHint': '在岗位与共享规则之前生效的记录可见性基线(ADR-0056/0090)', 'engine.studio.settings.sharingModel': '共享模型(sharingModel)—— 谁能查看和编辑他人拥有的记录', - 'engine.studio.settings.sharingUnset': '(未设置)', + 'engine.studio.settings.sharingUnset': '(未设置 —— 默认 Private)', 'engine.studio.settings.sharingPrivate': 'Private 私有 —— 仅记录所有者', 'engine.studio.settings.sharingPublicRead': 'Public read 公共只读 —— 所有人可读,仅所有者可写', 'engine.studio.settings.sharingPublicReadWrite': 'Public read/write 公共读写 —— 所有人可读可写', 'engine.studio.settings.sharingControlledByParent': 'Controlled by parent 受父级控制 —— 继承自主记录', 'engine.studio.settings.sharingDescUnset': - '未设置 —— 记录完全公开:租户内每个用户都能读取并编辑所有记录。选择 Private 可按所有者隔离记录。', + '未设置 —— 平台默认 Private(ADR-0090):仅所有者能访问记录。如需放宽可见性请显式选择模型。', 'engine.studio.settings.sharingDescPrivate': - '只有所有者(以及经角色或共享规则授予的用户)能访问记录。此时读取 / 编辑权限仅作用于自己拥有的记录。', + '只有所有者(以及经岗位或共享规则授予的用户)能访问记录。此时读取 / 编辑权限仅作用于自己拥有的记录。', 'engine.studio.settings.sharingDescPublicRead': '租户内每个用户都能读取所有记录;仅所有者可编辑。', 'engine.studio.settings.sharingDescPublicReadWrite': '租户内每个用户都能读取并编辑所有记录。', 'engine.studio.settings.sharingDescControlledByParent': '记录可见性通过主从(master-detail)关系继承自父级(主)记录。', + 'engine.studio.settings.sharingExternal': + '外部共享模型(externalSharingModel)—— 门户 / 合作伙伴等外部主体的基线', + 'engine.studio.settings.sharingExternalUnset': '(未设置 —— 默认 Private)', + 'engine.studio.settings.sharingExternalDesc': + '仅作用于外部主体(ADR-0090 D11)。不得宽于内部模型;未设置时回落为 Private。', + 'engine.studio.settings.sharingExternalWider': + '外部基线比内部共享模型更宽 —— 发布会被拒绝(ADR-0090 D11)。请收窄到不超过内部模型(private < public read < public read/write)。', 'engine.studio.pkg.writable': '可写', 'engine.studio.pkg.heading': '软件包(应用)', 'engine.studio.pkg.none': '暂无应用软件包', @@ -2296,7 +2322,7 @@ const ENGINE_STRINGS_ZH: Record = { 'engine.studio.access.bannerTitle': '此矩阵仅列出本包声明的对象,「Save」只合并本包切片 —— 其他包贡献的授权原样保留。编辑保存为软件包草稿,点击顶栏「发布」后随整个包一起生效(与数据、界面一致)。', 'engine.studio.access.banner': '仅本包对象 · 保存为草稿', - 'engine.studio.access.heading': '权限集 / Profile', + 'engine.studio.access.heading': '权限集', 'engine.studio.access.search': '搜索权限…', 'engine.studio.access.none': '还没有权限集 — 在下方新建一个', 'engine.studio.access.labelPlaceholder': '显示名(如:销售权限)', diff --git a/packages/app-shell/src/views/metadata-admin/inspectors/ObjectDefaultInspector.test.tsx b/packages/app-shell/src/views/metadata-admin/inspectors/ObjectDefaultInspector.test.tsx index 4ac1f3f637..c2b39cbb00 100644 --- a/packages/app-shell/src/views/metadata-admin/inspectors/ObjectDefaultInspector.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/inspectors/ObjectDefaultInspector.test.tsx @@ -238,7 +238,7 @@ describe('ObjectDefaultInspector — access section (ADR-0066 D2/D3/④/⑤)', ( }); }); - it('hides the per-operation toggle when the bundled spec lacks the ⑤ union', async () => { + it('shows the per-operation toggle when the bundled spec exports the ⑤ union', async () => { render( , ); - // Installed @objectstack/spec (< 12.7) does not export - // ObjectRequiredPermissionsSchema, so the feature-detect keeps the mode - // toggle hidden (progressive enhancement — no stale UI offering a shape - // client-side validation would reject). Let the async detect settle first. + // Installed @objectstack/spec (≥ 12.7) exports + // ObjectRequiredPermissionsSchema, so the feature-detect reveals the mode + // toggle (progressive enhancement — hidden only when the bundled spec + // would reject the map shape). Let the async detect settle first. await new Promise((r) => setTimeout(r, 20)); - expect(screen.queryByTestId('object-reqperms-perop')).toBeNull(); + expect(screen.queryByTestId('object-reqperms-perop')).not.toBeNull(); }); }); diff --git a/packages/app-shell/src/views/metadata-admin/previews/PositionPreview.tsx b/packages/app-shell/src/views/metadata-admin/previews/PositionPreview.tsx new file mode 100644 index 0000000000..54fcb5c102 --- /dev/null +++ b/packages/app-shell/src/views/metadata-admin/previews/PositionPreview.tsx @@ -0,0 +1,77 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * PositionPreview — read-only summary of a Position draft (ADR-0090 D3). + * + * The Position schema is intentionally minimal — name / label / + * description — and deliberately FLAT: no parent, no hierarchy. The + * visibility hierarchy lives on the business-unit tree, not here, so the + * preview's job is to put those fields into the mental model an operator + * carries: + * + * • Header card with label and machine name. + * • Description block. + * • A "permissions are managed separately" note pointing the user to + * Permission Sets that grant CRUD-VAMA — this is the most common + * confusion when first authoring a position. + */ + +import * as React from 'react'; +import { ShieldCheck, Users } from 'lucide-react'; +import type { MetadataPreviewProps } from '../preview-registry'; +import { PreviewShell, PreviewMessage, PreviewErrorBoundary } from './PreviewShell'; + +export function PositionPreview({ name, draft }: MetadataPreviewProps) { + const d = draft as Record; + const positionName = String(d.name ?? name ?? ''); + const label = String(d.label ?? positionName); + const description = (d.description as string | undefined) ?? ''; + + if (!positionName) { + return ( + + Give the position a name to see the preview. + + ); + } + + return ( + + +
+ {/* Header */} +
+
+ +
+
+ {label} + {positionName} +
+ {description && ( +
{description}
+ )} +
+
+
+ + {/* Permissions note */} +
+
+ +
+
Positions don't grant data access on their own.
+
+ Bind {positionName} to one or more{' '} + Permission Sets to control CRUD-VAMA, field access, and tab + visibility. Record visibility comes from the business-unit tree and sharing + rules, not from the position. +
+
+
+
+
+
+
+ ); +} diff --git a/packages/app-shell/src/views/metadata-admin/previews/RolePreview.tsx b/packages/app-shell/src/views/metadata-admin/previews/RolePreview.tsx deleted file mode 100644 index 9efe9a151a..0000000000 --- a/packages/app-shell/src/views/metadata-admin/previews/RolePreview.tsx +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -/** - * RolePreview — read-only summary of a Role draft. - * - * The Role schema is intentionally minimal — name / label / parent / - * description. The preview's job is therefore to put those four - * fields into the mental model an operator carries: - * - * • Header card with label and machine name. - * • Hierarchy breadcrumb — the role itself plus a chip for the - * declared parent. We cannot resolve the full chain from a single - * draft, but we surface the immediate parent prominently. - * • Description block. - * • A "permissions are managed separately" note pointing the user - * to PermissionSets / Profiles that grant CRUD-VAMA — this is - * the most common confusion when first authoring a role. - */ - -import * as React from 'react'; -import { CornerDownRight, IdCard, ShieldCheck, Users } from 'lucide-react'; -import type { MetadataPreviewProps } from '../preview-registry'; -import { PreviewShell, PreviewMessage, PreviewErrorBoundary } from './PreviewShell'; - -export function RolePreview({ name, draft }: MetadataPreviewProps) { - const d = draft as Record; - const roleName = String(d.name ?? name ?? ''); - const label = String(d.label ?? roleName); - const description = (d.description as string | undefined) ?? ''; - const parent = d.parent as string | undefined; - - if (!roleName) { - return ( - - Give the role a name to see the preview. - - ); - } - - return ( - - -
- {/* Header */} -
-
- -
-
- {label} - {roleName} -
- {description && ( -
{description}
- )} -
-
-
- - {/* Hierarchy */} -
-
- Hierarchy -
-
- {parent ? ( -
-
- - Parent: - {parent} -
-
- - - {roleName} - - this role -
-
- Open the parent role to walk further up the chain. -
-
- ) : ( -
- - {roleName} - - root role — no parent -
- )} -
-
- - {/* Permissions note */} -
-
- -
-
Roles don't grant data access on their own.
-
- Bind {roleName} to one or more Permission Sets{' '} - or assign it to a Profile to control CRUD-VAMA, field access, and tab visibility. -
-
-
-
-
-
-
- ); -} diff --git a/packages/app-shell/src/views/metadata-admin/previews/index.ts b/packages/app-shell/src/views/metadata-admin/previews/index.ts index 68aa30814e..a723a363e6 100644 --- a/packages/app-shell/src/views/metadata-admin/previews/index.ts +++ b/packages/app-shell/src/views/metadata-admin/previews/index.ts @@ -24,7 +24,7 @@ import { PermissionPreview } from './PermissionPreview'; import { ActionPreview } from './ActionPreview'; import { JobPreview } from './JobPreview'; import { TranslationPreview } from './TranslationPreview'; -import { RolePreview } from './RolePreview'; +import { PositionPreview } from './PositionPreview'; import { SkillPreview } from './SkillPreview'; import { DatasourcePreview } from './DatasourcePreview'; import { ValidationPreview } from './ValidationPreview'; @@ -65,9 +65,11 @@ export function registerBuiltinPreviews(): void { registerMetadataPreview('tool', ToolPreview); registerMetadataPreview('skill', SkillPreview); // Security & Identity - // ADR-0090 D2 removed the Profile concept; the `profile` registration stays - // only so metadata from pre-v2 backends still renders (inert otherwise). + // ADR-0090 D2 removed the Profile concept and D3 renamed role → position; + // the `profile`/`role` registrations stay only so metadata from pre-v2 + // backends still renders (inert otherwise). registerMetadataPreview('permission', PermissionPreview); registerMetadataPreview('profile', PermissionPreview); - registerMetadataPreview('role', RolePreview); + registerMetadataPreview('position', PositionPreview); + registerMetadataPreview('role', PositionPreview); } diff --git a/packages/app-shell/src/views/studio-design/ObjectSettingsPanel.test.tsx b/packages/app-shell/src/views/studio-design/ObjectSettingsPanel.test.tsx index 3c85cdf3ed..6ef7b2dfca 100644 --- a/packages/app-shell/src/views/studio-design/ObjectSettingsPanel.test.tsx +++ b/packages/app-shell/src/views/studio-design/ObjectSettingsPanel.test.tsx @@ -6,7 +6,7 @@ * LICENSE file in the root directory of this source tree. */ import { describe, it, expect, vi } from 'vitest'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen, fireEvent, within } from '@testing-library/react'; import React from 'react'; import { ObjectSettingsPanel } from './ObjectSettingsPanel'; @@ -29,29 +29,31 @@ describe('ObjectSettingsPanel — record sharing (OWD)', () => { renderPanel(baseDraft); // The section header is present. expect(screen.getByText('Record sharing (OWD)')).toBeTruthy(); - // Canonical OWD options are all offered. - expect(screen.getByRole('option', { name: 'Private — owner only' })).toBeTruthy(); + // Canonical OWD options are all offered (scoped to the internal dial — + // the external D11 dial offers the same set). + const internal = within(screen.getByTestId('owd-internal-select')); + expect(internal.getByRole('option', { name: 'Private — owner only' })).toBeTruthy(); expect( - screen.getByRole('option', { name: 'Public read — everyone reads, only the owner writes' }), + internal.getByRole('option', { name: 'Public read — everyone reads, only the owner writes' }), ).toBeTruthy(); expect( - screen.getByRole('option', { name: 'Public read/write — everyone reads and writes' }), + internal.getByRole('option', { name: 'Public read/write — everyone reads and writes' }), ).toBeTruthy(); expect( - screen.getByRole('option', { name: 'Controlled by parent — inherited from the master record' }), + internal.getByRole('option', { name: 'Controlled by parent — inherited from the master record' }), ).toBeTruthy(); }); - it('warns that an unset sharing model means fully public records', () => { + it('explains that an unset sharing model defaults to private (ADR-0090 D1)', () => { renderPanel(baseDraft); expect( - screen.getByText(/records are fully public/i), + screen.getByText(/defaults to Private \(ADR-0090\)/i), ).toBeTruthy(); }); it('patches sharingModel when a model is picked', () => { const onPatch = renderPanel(baseDraft); - const select = screen.getByDisplayValue('(not set)') as HTMLSelectElement; + const select = screen.getByTestId('owd-internal-select') as HTMLSelectElement; fireEvent.change(select, { target: { value: 'private' } }); expect(onPatch).toHaveBeenCalledWith({ sharingModel: 'private' }); }); @@ -62,12 +64,32 @@ describe('ObjectSettingsPanel — record sharing (OWD)', () => { fireEvent.change(select, { target: { value: '' } }); expect(onPatch).toHaveBeenCalledWith({ sharingModel: undefined }); }); +}); + +describe('ObjectSettingsPanel — external OWD dial (ADR-0090 D11)', () => { + it('renders the external dial defaulting to unset and patches externalSharingModel', () => { + const onPatch = renderPanel(baseDraft); + const select = screen.getByTestId('owd-external-select') as HTMLSelectElement; + expect(select.value).toBe(''); + fireEvent.change(select, { target: { value: 'public_read' } }); + expect(onPatch).toHaveBeenCalledWith({ externalSharingModel: 'public_read' }); + }); + + it('clears externalSharingModel back to unset', () => { + const onPatch = renderPanel({ ...baseDraft, externalSharingModel: 'private' }); + const select = screen.getByTestId('owd-external-select') as HTMLSelectElement; + expect(select.value).toBe('private'); + fireEvent.change(select, { target: { value: '' } }); + expect(onPatch).toHaveBeenCalledWith({ externalSharingModel: undefined }); + }); + + it('warns when the external baseline is wider than the internal one', () => { + renderPanel({ ...baseDraft, sharingModel: 'public_read', externalSharingModel: 'public_read_write' }); + expect(screen.getByTestId('owd-external-desc').textContent).toMatch(/WIDER/); + }); - it('normalises legacy aliases (read → public_read) for display', () => { - renderPanel({ ...baseDraft, sharingModel: 'read' }); - const select = screen.getByDisplayValue( - 'Public read — everyone reads, only the owner writes', - ) as HTMLSelectElement; - expect(select.value).toBe('public_read'); + it('stays calm when external ≤ internal', () => { + renderPanel({ ...baseDraft, sharingModel: 'public_read', externalSharingModel: 'private' }); + expect(screen.getByTestId('owd-external-desc').textContent).not.toMatch(/WIDER/); }); }); diff --git a/packages/app-shell/src/views/studio-design/ObjectSettingsPanel.tsx b/packages/app-shell/src/views/studio-design/ObjectSettingsPanel.tsx index 31e2093d38..18874ffa55 100644 --- a/packages/app-shell/src/views/studio-design/ObjectSettingsPanel.tsx +++ b/packages/app-shell/src/views/studio-design/ObjectSettingsPanel.tsx @@ -17,10 +17,9 @@ * 2. Record sharing (ADR-0056) — the object-level Org-Wide Default (OWD) * `sharingModel` (private | public_read | public_read_write | * controlled_by_parent). This is the baseline record-level visibility the - * runtime applies BEFORE roles and sharing rules: without it the platform - * treats records as fully public (every tenant user reads AND edits every - * record), so record isolation is invisible/unconfigurable at design time - * unless the builder exposes it. + * runtime applies BEFORE positions and sharing rules. Since ADR-0090 D1 + * an unset value defaults to `private` (secure default) — the old + * fully-public cliff is gone, so leaving it unset is safe. * 3. Semantic roles (ADR-0085) — the cross-surface presentation roles: * `nameField`, `stageField` (string | false | unset), `highlightFields`. * These are the ONLY presentation knobs the protocol carries, so the @@ -62,16 +61,10 @@ export function ObjectSettingsPanel({ (e) => e.def.hidden !== true && !highlightFields.includes(e.name), ); - // Record sharing (OWD). The spec keeps legacy aliases (`read` → public_read, - // `read_write`/`full` → public_read_write) for back-compat; normalise them to - // the canonical value so the onPatch(e.target.value ? { sharingModel: e.target.value } : { sharingModel: undefined }) } @@ -139,14 +142,49 @@ export function ObjectSettingsPanel({ +

+ {t(sharingDescKey, locale)} +

+

- {t(sharingDescKey, locale)} + {t( + externalWider + ? 'engine.studio.settings.sharingExternalWider' + : 'engine.studio.settings.sharingExternalDesc', + locale, + )}

diff --git a/packages/components/src/renderers/basic/metadata-viewer.tsx b/packages/components/src/renderers/basic/metadata-viewer.tsx index eecc6267ca..130efdb102 100644 --- a/packages/components/src/renderers/basic/metadata-viewer.tsx +++ b/packages/components/src/renderers/basic/metadata-viewer.tsx @@ -317,7 +317,7 @@ function PermissionView({ name }: ViewerProps) { hint="permission" icon={ShieldCheck} title={perm.label ?? perm.name ?? name} - subtitle={<>Object access · {entries.length} object{entries.length === 1 ? '' : 's'}{perm.isProfile ? ' · profile' : ''}} + subtitle={<>Object access · {entries.length} object{entries.length === 1 ? '' : 's'}} > {entries.length === 0 ? ( No object permissions declared. diff --git a/packages/core/package.json b/packages/core/package.json index ae6cdf7b94..7a1d93537c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -33,7 +33,7 @@ "dependencies": { "@object-ui/types": "workspace:*", "@objectstack/formula": "^12.3.0", - "@objectstack/spec": "^12.3.0", + "@objectstack/spec": "^13.0.0", "lodash": "^4.18.1", "zod": "^4.4.3" }, diff --git a/packages/plugin-detail/package.json b/packages/plugin-detail/package.json index e705f3c4b9..aa12c88361 100644 --- a/packages/plugin-detail/package.json +++ b/packages/plugin-detail/package.json @@ -32,7 +32,7 @@ }, "dependencies": { "@object-ui/i18n": "workspace:*", - "@objectstack/spec": "^12.3.0", + "@objectstack/spec": "^13.0.0", "lucide-react": "^1.22.0" }, "peerDependencies": { diff --git a/packages/plugin-form/package.json b/packages/plugin-form/package.json index 458eaf722c..b600929a03 100644 --- a/packages/plugin-form/package.json +++ b/packages/plugin-form/package.json @@ -27,7 +27,7 @@ "@object-ui/permissions": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^12.3.0", + "@objectstack/spec": "^13.0.0", "lucide-react": "^1.22.0" }, "peerDependencies": { diff --git a/packages/plugin-gantt/package.json b/packages/plugin-gantt/package.json index 74f4d7d3ec..ca92ad1d4b 100644 --- a/packages/plugin-gantt/package.json +++ b/packages/plugin-gantt/package.json @@ -38,7 +38,7 @@ "@object-ui/plugin-detail": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^12.3.0", + "@objectstack/spec": "^13.0.0", "lucide-react": "^1.22.0" }, "peerDependencies": { diff --git a/packages/plugin-map/package.json b/packages/plugin-map/package.json index 5e02969bab..165177824b 100644 --- a/packages/plugin-map/package.json +++ b/packages/plugin-map/package.json @@ -35,7 +35,7 @@ "@object-ui/core": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^12.3.0", + "@objectstack/spec": "^13.0.0", "lucide-react": "^1.22.0", "maplibre-gl": "^5.24.0", "react-map-gl": "^8.1.1", diff --git a/packages/plugin-timeline/package.json b/packages/plugin-timeline/package.json index 29eb3c0353..f581d1b326 100644 --- a/packages/plugin-timeline/package.json +++ b/packages/plugin-timeline/package.json @@ -36,7 +36,7 @@ "@object-ui/mobile": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^12.3.0", + "@objectstack/spec": "^13.0.0", "class-variance-authority": "^0.7.1", "zod": "^4.4.3" }, diff --git a/packages/plugin-tree/package.json b/packages/plugin-tree/package.json index 502c4fd3d7..0d98369cd8 100644 --- a/packages/plugin-tree/package.json +++ b/packages/plugin-tree/package.json @@ -35,7 +35,7 @@ "@object-ui/core": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^12.3.0", + "@objectstack/spec": "^13.0.0", "lucide-react": "^1.22.0" }, "peerDependencies": { diff --git a/packages/plugin-view/package.json b/packages/plugin-view/package.json index d7bf9a9930..fb307a4ee0 100644 --- a/packages/plugin-view/package.json +++ b/packages/plugin-view/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@dnd-kit/core": "^6.3.1", - "@objectstack/spec": "^12.3.0", + "@objectstack/spec": "^13.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@object-ui/components": "workspace:*", diff --git a/packages/react/package.json b/packages/react/package.json index fecddd3dd5..f2670c699c 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -34,7 +34,7 @@ "@object-ui/data-objectstack": "workspace:*", "@object-ui/i18n": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^12.3.0", + "@objectstack/spec": "^13.0.0", "react-hook-form": "^7.81.0" }, "peerDependencies": { diff --git a/packages/types/package.json b/packages/types/package.json index 123ff85f0b..7c8de1fdbf 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -90,7 +90,7 @@ "directory": "packages/types" }, "dependencies": { - "@objectstack/spec": "^12.3.0", + "@objectstack/spec": "^13.0.0", "zod": "^4.4.3" }, "devDependencies": { diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 90b0cf5585..5c1af4592d 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -341,6 +341,7 @@ export type { ObjectGridSchema, ObjectFormSchema, ObjectFormSection, + SubmitBehavior, ObjectViewSchema, NamedListView, ViewNavigationConfig, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7327fd5f15..9a55be3d23 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -265,8 +265,8 @@ importers: specifier: ^12.3.0 version: 12.3.0(ai@7.0.15(zod@4.4.3)) '@objectstack/spec': - specifier: ^12.3.0 - version: 12.3.0(ai@7.0.15(zod@4.4.3)) + specifier: ^13.0.0 + version: 13.0.0(ai@7.0.15(zod@4.4.3)) '@tailwindcss/postcss': specifier: ^4.3.2 version: 4.3.2 @@ -397,8 +397,8 @@ importers: specifier: workspace:* version: link:../../packages/types '@objectstack/spec': - specifier: ^12.3.0 - version: 12.3.0(ai@7.0.15(zod@4.4.3)) + specifier: ^13.0.0 + version: 13.0.0(ai@7.0.15(zod@4.4.3)) fumadocs-core: specifier: 16.10.7 version: 16.10.7(@mdx-js/mdx@3.1.1)(@tanstack/react-router@1.170.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.22.0(react@19.2.7))(next@16.2.10(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-router@7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(zod@4.4.3) @@ -689,8 +689,8 @@ importers: specifier: ^12.6.0 version: 12.6.0(ai@7.0.15(zod@4.4.3)) '@objectstack/spec': - specifier: ^12.3.0 - version: 12.3.0(ai@7.0.15(zod@4.4.3)) + specifier: ^13.0.0 + version: 13.0.0(ai@7.0.15(zod@4.4.3)) '@sentry/react': specifier: ^10.63.0 version: 10.63.0(react@19.2.7) @@ -1072,8 +1072,8 @@ importers: specifier: ^12.3.0 version: 12.3.0(ai@7.0.15(zod@4.4.3)) '@objectstack/spec': - specifier: ^12.3.0 - version: 12.3.0(ai@7.0.15(zod@4.4.3)) + specifier: ^13.0.0 + version: 13.0.0(ai@7.0.15(zod@4.4.3)) lodash: specifier: ^4.18.1 version: 4.18.1 @@ -1719,8 +1719,8 @@ importers: specifier: workspace:^ version: link:../permissions '@objectstack/spec': - specifier: ^12.3.0 - version: 12.3.0(ai@7.0.15(zod@4.4.3)) + specifier: ^13.0.0 + version: 13.0.0(ai@7.0.15(zod@4.4.3)) lucide-react: specifier: ^1.22.0 version: 1.22.0(react@19.2.7) @@ -1835,8 +1835,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^12.3.0 - version: 12.3.0(ai@7.0.15(zod@4.4.3)) + specifier: ^13.0.0 + version: 13.0.0(ai@7.0.15(zod@4.4.3)) lucide-react: specifier: ^1.22.0 version: 1.22.0(react@19.2.7) @@ -1890,8 +1890,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^12.3.0 - version: 12.3.0(ai@7.0.15(zod@4.4.3)) + specifier: ^13.0.0 + version: 13.0.0(ai@7.0.15(zod@4.4.3)) lucide-react: specifier: ^1.22.0 version: 1.22.0(react@19.2.7) @@ -2119,8 +2119,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^12.3.0 - version: 12.3.0(ai@7.0.15(zod@4.4.3)) + specifier: ^13.0.0 + version: 13.0.0(ai@7.0.15(zod@4.4.3)) lucide-react: specifier: ^1.22.0 version: 1.22.0(react@19.2.7) @@ -2308,8 +2308,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^12.3.0 - version: 12.3.0(ai@7.0.15(zod@4.4.3)) + specifier: ^13.0.0 + version: 13.0.0(ai@7.0.15(zod@4.4.3)) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -2360,8 +2360,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^12.3.0 - version: 12.3.0(ai@7.0.15(zod@4.4.3)) + specifier: ^13.0.0 + version: 13.0.0(ai@7.0.15(zod@4.4.3)) lucide-react: specifier: ^1.22.0 version: 1.22.0(react@19.2.7) @@ -2424,8 +2424,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^12.3.0 - version: 12.3.0(ai@7.0.15(zod@4.4.3)) + specifier: ^13.0.0 + version: 13.0.0(ai@7.0.15(zod@4.4.3)) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -2489,8 +2489,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^12.3.0 - version: 12.3.0(ai@7.0.15(zod@4.4.3)) + specifier: ^13.0.0 + version: 13.0.0(ai@7.0.15(zod@4.4.3)) react: specifier: 19.2.7 version: 19.2.7 @@ -2614,8 +2614,8 @@ importers: packages/types: dependencies: '@objectstack/spec': - specifier: ^12.3.0 - version: 12.3.0(ai@7.0.15(zod@4.4.3)) + specifier: ^13.0.0 + version: 13.0.0(ai@7.0.15(zod@4.4.3)) zod: specifier: ^4.4.3 version: 4.4.3 @@ -4002,6 +4002,15 @@ packages: ai: optional: true + '@objectstack/spec@13.0.0': + resolution: {integrity: sha512-qBqhMLbZSUBveG3njAazujGAUN1b2JSXhGpgiV8fQVTx9jLs5Ywpu2MVZRPBCg79TXfKGJ+eOAFSUnQyTFt6yA==} + engines: {node: '>=18.0.0'} + peerDependencies: + ai: ^7.0.0 + peerDependenciesMeta: + ai: + optional: true + '@open-draft/deferred-promise@2.2.0': resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} @@ -12155,6 +12164,12 @@ snapshots: optionalDependencies: ai: 7.0.15(zod@4.4.3) + '@objectstack/spec@13.0.0(ai@7.0.15(zod@4.4.3))': + dependencies: + zod: 4.4.3 + optionalDependencies: + ai: 7.0.15(zod@4.4.3) + '@open-draft/deferred-promise@2.2.0': {} '@open-draft/deferred-promise@3.0.0': {}