- Updated dependencies [212b66a]
- Updated dependencies [d10c4dc]
- Updated dependencies [9e45b63]
- Updated dependencies [b20201f]
- @objectstack/platform-objects@16.1.0
- @objectstack/spec@16.1.0
- @objectstack/core@16.1.0
- @objectstack/formula@16.1.0
-
22013aa: Split the overloaded
managedBy: 'system'bucket into engine-owned vs. admin-writable, and enforce engine-owned writes (ADR-0103, #3220). Thesystembucket conflated two incompatible write policies: rows a platform service owns end to end (never user-written), and platform-defined schema whose rows are legitimately admin/user-writable. It carried the same all-false affordance row asbetter-auth/append-onlybut, unlikebetter-auth, had no engine enforcement — a wildcard admin could raw-write these rows through the generic data API (ADR-0049 gap).Rather than add a new
managedByenum value (which would fall through to fully-editableplatformdefaults on already-deployed Console clients), the write policy is now the resolved affordance (resolveCrudAffordances= bucket default +userActions), and engine-owned is defined as asystem/append-onlyobject that grants no write:- Writable set declares
userActions— the RBAC link tables (sys_user_position,sys_user_permission_set,sys_position_permission_set),sys_user_preference,sys_approval_delegation, and the messaging config grids (sys_notification_preference/…_subscription/…_template) now declareuserActions: { create, edit, delete: true }. The affordance is a declaration only — theDelegatedAdminGate/ RLS / permission sets remain the authz. - Engine-owned objects locked to reads —
apiMethods: ['get','list']added where absent (jobs, notifications, approval request/approver/token/action,sys_record_share,sys_automation_run, mail/settings/secret audit, the messaging delivery pipeline).sys_secretis explicitly read-locked (an emptyapiMethodsarray fails open). sys_import_jobstays engine-owned: the REST import route now writes its job rowsisSystem-elevated (attribution preserved via the explicitcreated_bystamp) and the object is locked to['get','list'].- New engine write guard (
assertEngineOwnedWriteAllowed, plugin-security) fail-closed rejects user-context generic writes to engine-ownedsystem/append-onlyobjects, keyed off the resolved affordance;isSystemand context-less engine/service writes bypass by construction. Wired into the security middleware alongside the other data-layer gates. reconcileManagedApiMethods(objectql registry) now runs for every managed bucket, not justbetter-auth: any advertised write verb an object's resolved affordances forbid is stripped at registration with a warning (the drift backstop, ADR-0049)./me/permissionsclamp (plugin-hono-server) now clampssystem/append-onlyas well asbetter-auth, so the client hint reflectspermission ∩ guard.
Potentially breaking: a downstream/third-party
systemobject that advertised generic write verbs relying on today's fail-open behaviour will have those verbs stripped (with a warning) and user-context generic writes to it rejected. DeclareuserActionsopening the verbs the object legitimately takes from a user context.better-authkeeps plugin-auth's identity write guard unchanged; the row-levelmanaged_byprovenance vocabulary (ADR-0066) is a different axis and is untouched. - Writable set declares
-
62a2117: Split the overloaded
managedBy: 'system'bucket with an explicitengine-ownedvalue (ADR-0103 addendum, #3343). ADR-0103 deferred the enum split ("revisitable later as a rename") because a newmanagedByvalue would fall through to the fully-editableplatformdefault on deployed Console clients. Both reasons against it are now retired — the server-side write guard /apiMethodsreconciliation //me/permissionsclamp make that fallthrough cosmetic (the write is rejected regardless of what the client renders), and objectui#2712 closed the UI union — so v16 lands it, additively.- New enum value
engine-ownedwith the same all-locked default affordance row assystem(create/import/edit/delete: false,exportCsv: true). It joinsENGINE_OWNED_BUCKETS(the engine write guard) andGUARDED_WRITE_BUCKETS(the/me/permissionsclamp); the guard,reconcileManagedApiMethods, and the clamp mechanisms are unchanged —engine-ownedis an explicit member of the set they already covered by resolved affordance. - 20 objects relabelled
system → engine-owned— the ones the engine owns end to end and that declared no write-openinguserActions(the metadata store, jobs, approval runtime rows, sharing rows,sys_automation_run, the messaging delivery/receipt pipeline,sys_secret, settings). One-line, behaviour-identical per object. - 8 admin/user-writable objects keep
managedBy: 'system'(the RBAC link tables,sys_user_preference,sys_approval_delegation, the messaging config grids) —systemnow reads as "engine-managed schema, writable viauserActions".
Behaviour-, enforcement- and wire-identical: resolved affordances, the guard verdict, the 405
apiMethodsreconciliation, and the permissions clamp are the same before and after — this is a self-documenting relabel, not a policy change. No data migration (managedByis schema metadata) and no code branches on the'system'literal. Retiring the overloadedsystementirely (moving the 8 writable objects to a dedicated bucket) is a breaking rename deferred to v17. - New enum value
-
2f3c641: ADR-0099 P0: land the probe-vs-carried-rung equivalence gate in the authz matrix (
authz-matrix-gate.test.ts) — seeded-shape equivalence cells, two adversarialKNOWN DIVERGENCEpins (scopedadmin_full_accessgrant; piecemeal platform-exclusive capability), the I2 nesting and I3 narrowing invariant cells, posture-blindness staging pins for the P1 flip, and the EXTERNAL dead-branch cell. Extracts the platform-admin capability probe as the exported purehasPlatformAdminCapability(mechanical, behavior unchanged). Test-only gate; the ADR-0099 P1 flip lands behind it (#3211). -
e38da5b: ADR-0099 P1 (#3211 M2): the Layer 0 cross-tenant exemption gate now reads the carried
ctx.posturerung (#2956) as authoritative, with the platform-admin capability probe demoted to a fallback for resolver-less contexts (delegated-admin bridge, sharing service,getReadFilter). The read and write (insert/update post-image) tenant checks share one decision (computeLayeredRlsFilter), so they cannot drift. A probe↔rung disagreement logs a defect breadcrumb and enforces the narrower rung verdict.Behavior change (security narrowing, multi-org /
@objectstack/organizationsonly): a principal whose carried rung is notPLATFORM_ADMINno longer crosses the tenant wall on private / platform-global / better-auth-managed objects, even when its resolved permission sets carry a platform-exclusive capability. Two shapes are affected: (a) a scopedadmin_full_accessgrant (sys_user_permission_set.organization_idnon-null), and (b) a custom set granting a platform capability (e.g.studio.access) piecemeal alongside a superuser bit. Both are now walled to their own org — the fail-safe direction (the carried rung is a strict subset of the probe). Single-org / env-per-database deployments are unaffected (Layer 0 is inert).Upgrade check: before upgrading, scan
sys_user_permission_setforadmin_full_accessrows with a non-nullorganization_id, and custom permission sets whosesystemPermissionsintersect{manage_metadata, manage_platform_settings, studio.access, manage_users}. To restore cross-tenant operator access for such a principal, grant the unscopedadmin_full_accessinstead. The[authz/ADR-0099]warn log names any principal hitting the divergence at runtime. -
f9b118d: ADR-0099 P2′ (#3211 M3′): pin the two-axis Amendment in the authz matrix. The original P2 (collapse the Layer 1 tier onto posture) was rejected — Layer 1's tier input is the per-object super-bit, a per-principal × per-object delegation primitive posture cannot represent. New cells pin: seeded-face agreement (seeded super-bit holders are already ≥ TENANT_ADMIN), the load-bearing delegation cell (a MEMBER with a delegated per-object
viewAllRecords/modifyAllRecordsshort-circuits Layer 1 yet stays walled by Layer 0 — the auditor pattern), invariant I7 (the scope axis never crosses a boundary posture has not opened), and the contrast that the bit is a real grantable capability, not conditionally inert. Test-only; zero behavior change. -
9d897b3: Derive the better-auth managed-object write denies from the live registry (#3325, follow-through of ADR-0092 / ADR-0103). The default permission sets deny generic writes on better-auth identity tables via a hand-maintained
BETTER_AUTH_MANAGED_OBJECTSlist — exactly the drift ADR-0092 forbids, and it had already drifted (the list carried 17 names while 22 schemas declaremanagedBy: 'better-auth', leavingsys_scim_provider,sys_sso_provider, and threesys_oauth_*tables wildcard-granted for writes at the permission-evaluator layer; the identity write guard still 403'd the actual write, so this was a defense-in-depth gap, not a live hole).- New
applyManagedWriteDenies(managed-object-write-denies.ts) injects a read-only-write deny for every registeredmanagedBy: 'better-auth'object into the four write-granting default sets (organization_admin,member_default,viewer_readonly, MCP write) atkernel:ready, mutating the shared in-memorybootstrapPermissionSetsin place (the array the evaluator resolves and the seeder serializes — a DB-row-only fix would be dead code). Never touchesadmin_full_access, never overrides an existing explicit entry, ignoresuserActions(the better-auth bucket is hard-denied —sys_user'suserActions.editopens only a field-level whitelist the identity guard enforces). - The static
BETTER_AUTH_MANAGED_OBJECTSlist is completed to 22 and kept as a compile-time baseline (covers the pre-kernel:readywindow), now pinned bidirectionally against the@objectstack/platform-objectsschemas by a test so it cannot silently rot again. - Engine-owned
system/append-onlyobjects are deliberately NOT given deny entries — a per-object entry overrides the wildcard and would dropviewAllRecords; their writes are already rejected by the ADR-0103 engine guard.
No public API change; the helper is internal. Behavior is byte-preserving for the 17 already-listed tables and closes the gap on the 5 that had drifted.
- New
-
Updated dependencies [f972574]
-
Updated dependencies [6289ec3]
-
Updated dependencies [22013aa]
-
Updated dependencies [3ad3dd5]
-
Updated dependencies [8efa395]
-
Updated dependencies [3a18b60]
-
Updated dependencies [a8aa34c]
-
Updated dependencies [e057f42]
-
Updated dependencies [a3823b2]
-
Updated dependencies [bc65105]
-
Updated dependencies [43a3efb]
-
Updated dependencies [524696a]
-
Updated dependencies [6b51346]
-
Updated dependencies [80273c8]
-
Updated dependencies [bfa3c3f]
-
Updated dependencies [5e3301d]
-
Updated dependencies [dd9f223]
-
Updated dependencies [46e876c]
-
Updated dependencies [7125007]
-
Updated dependencies [5f05de2]
-
Updated dependencies [021ba4c]
-
Updated dependencies [158aa14]
-
Updated dependencies [62a2117]
-
Updated dependencies [d2723e2]
-
Updated dependencies [fefcd54]
-
Updated dependencies [beaf2de]
-
Updated dependencies [369eb6e]
-
Updated dependencies [06ff734]
-
Updated dependencies [b659111]
-
Updated dependencies [5754a23]
-
Updated dependencies [6c270a6]
-
Updated dependencies [290e2f0]
-
Updated dependencies [668dd17]
-
Updated dependencies [8abf133]
-
Updated dependencies [e0859b1]
-
Updated dependencies [04ecd4e]
-
Updated dependencies [4d5a892]
-
Updated dependencies [16cebeb]
-
Updated dependencies [86d30af]
-
Updated dependencies [8923843]
-
Updated dependencies [ea32ec7]
-
Updated dependencies [a2795f6]
-
Updated dependencies [f16b492]
-
Updated dependencies [4b6fde8]
-
Updated dependencies [2018df9]
-
Updated dependencies [fc5a3a2]
-
Updated dependencies [8ff9210]
- @objectstack/spec@16.0.0
- @objectstack/platform-objects@16.0.0
- @objectstack/core@16.0.0
- @objectstack/formula@16.0.0
-
62a2117: Split the overloaded
managedBy: 'system'bucket with an explicitengine-ownedvalue (ADR-0103 addendum, #3343). ADR-0103 deferred the enum split ("revisitable later as a rename") because a newmanagedByvalue would fall through to the fully-editableplatformdefault on deployed Console clients. Both reasons against it are now retired — the server-side write guard /apiMethodsreconciliation //me/permissionsclamp make that fallthrough cosmetic (the write is rejected regardless of what the client renders), and objectui#2712 closed the UI union — so v16 lands it, additively.- New enum value
engine-ownedwith the same all-locked default affordance row assystem(create/import/edit/delete: false,exportCsv: true). It joinsENGINE_OWNED_BUCKETS(the engine write guard) andGUARDED_WRITE_BUCKETS(the/me/permissionsclamp); the guard,reconcileManagedApiMethods, and the clamp mechanisms are unchanged —engine-ownedis an explicit member of the set they already covered by resolved affordance. - 20 objects relabelled
system → engine-owned— the ones the engine owns end to end and that declared no write-openinguserActions(the metadata store, jobs, approval runtime rows, sharing rows,sys_automation_run, the messaging delivery/receipt pipeline,sys_secret, settings). One-line, behaviour-identical per object. - 8 admin/user-writable objects keep
managedBy: 'system'(the RBAC link tables,sys_user_preference,sys_approval_delegation, the messaging config grids) —systemnow reads as "engine-managed schema, writable viauserActions".
Behaviour-, enforcement- and wire-identical: resolved affordances, the guard verdict, the 405
apiMethodsreconciliation, and the permissions clamp are the same before and after — this is a self-documenting relabel, not a policy change. No data migration (managedByis schema metadata) and no code branches on the'system'literal. Retiring the overloadedsystementirely (moving the 8 writable objects to a dedicated bucket) is a breaking rename deferred to v17. - New enum value
-
9d897b3: Derive the better-auth managed-object write denies from the live registry (#3325, follow-through of ADR-0092 / ADR-0103). The default permission sets deny generic writes on better-auth identity tables via a hand-maintained
BETTER_AUTH_MANAGED_OBJECTSlist — exactly the drift ADR-0092 forbids, and it had already drifted (the list carried 17 names while 22 schemas declaremanagedBy: 'better-auth', leavingsys_scim_provider,sys_sso_provider, and threesys_oauth_*tables wildcard-granted for writes at the permission-evaluator layer; the identity write guard still 403'd the actual write, so this was a defense-in-depth gap, not a live hole).- New
applyManagedWriteDenies(managed-object-write-denies.ts) injects a read-only-write deny for every registeredmanagedBy: 'better-auth'object into the four write-granting default sets (organization_admin,member_default,viewer_readonly, MCP write) atkernel:ready, mutating the shared in-memorybootstrapPermissionSetsin place (the array the evaluator resolves and the seeder serializes — a DB-row-only fix would be dead code). Never touchesadmin_full_access, never overrides an existing explicit entry, ignoresuserActions(the better-auth bucket is hard-denied —sys_user'suserActions.editopens only a field-level whitelist the identity guard enforces). - The static
BETTER_AUTH_MANAGED_OBJECTSlist is completed to 22 and kept as a compile-time baseline (covers the pre-kernel:readywindow), now pinned bidirectionally against the@objectstack/platform-objectsschemas by a test so it cannot silently rot again. - Engine-owned
system/append-onlyobjects are deliberately NOT given deny entries — a per-object entry overrides the wildcard and would dropviewAllRecords; their writes are already rejected by the ADR-0103 engine guard.
No public API change; the helper is internal. Behavior is byte-preserving for the 17 already-listed tables and closes the gap on the 5 that had drifted.
- New
-
Updated dependencies [6289ec3]
-
Updated dependencies [8efa395]
-
Updated dependencies [bfa3c3f]
-
Updated dependencies [7125007]
-
Updated dependencies [62a2117]
-
Updated dependencies [06ff734]
- @objectstack/spec@16.0.0-rc.1
- @objectstack/platform-objects@16.0.0-rc.1
- @objectstack/formula@16.0.0-rc.1
- @objectstack/core@16.0.0-rc.1
-
22013aa: Split the overloaded
managedBy: 'system'bucket into engine-owned vs. admin-writable, and enforce engine-owned writes (ADR-0103, #3220). Thesystembucket conflated two incompatible write policies: rows a platform service owns end to end (never user-written), and platform-defined schema whose rows are legitimately admin/user-writable. It carried the same all-false affordance row asbetter-auth/append-onlybut, unlikebetter-auth, had no engine enforcement — a wildcard admin could raw-write these rows through the generic data API (ADR-0049 gap).Rather than add a new
managedByenum value (which would fall through to fully-editableplatformdefaults on already-deployed Console clients), the write policy is now the resolved affordance (resolveCrudAffordances= bucket default +userActions), and engine-owned is defined as asystem/append-onlyobject that grants no write:- Writable set declares
userActions— the RBAC link tables (sys_user_position,sys_user_permission_set,sys_position_permission_set),sys_user_preference,sys_approval_delegation, and the messaging config grids (sys_notification_preference/…_subscription/…_template) now declareuserActions: { create, edit, delete: true }. The affordance is a declaration only — theDelegatedAdminGate/ RLS / permission sets remain the authz. - Engine-owned objects locked to reads —
apiMethods: ['get','list']added where absent (jobs, notifications, approval request/approver/token/action,sys_record_share,sys_automation_run, mail/settings/secret audit, the messaging delivery pipeline).sys_secretis explicitly read-locked (an emptyapiMethodsarray fails open). sys_import_jobstays engine-owned: the REST import route now writes its job rowsisSystem-elevated (attribution preserved via the explicitcreated_bystamp) and the object is locked to['get','list'].- New engine write guard (
assertEngineOwnedWriteAllowed, plugin-security) fail-closed rejects user-context generic writes to engine-ownedsystem/append-onlyobjects, keyed off the resolved affordance;isSystemand context-less engine/service writes bypass by construction. Wired into the security middleware alongside the other data-layer gates. reconcileManagedApiMethods(objectql registry) now runs for every managed bucket, not justbetter-auth: any advertised write verb an object's resolved affordances forbid is stripped at registration with a warning (the drift backstop, ADR-0049)./me/permissionsclamp (plugin-hono-server) now clampssystem/append-onlyas well asbetter-auth, so the client hint reflectspermission ∩ guard.
Potentially breaking: a downstream/third-party
systemobject that advertised generic write verbs relying on today's fail-open behaviour will have those verbs stripped (with a warning) and user-context generic writes to it rejected. DeclareuserActionsopening the verbs the object legitimately takes from a user context.better-authkeeps plugin-auth's identity write guard unchanged; the row-levelmanaged_byprovenance vocabulary (ADR-0066) is a different axis and is untouched. - Writable set declares
-
2f3c641: ADR-0099 P0: land the probe-vs-carried-rung equivalence gate in the authz matrix (
authz-matrix-gate.test.ts) — seeded-shape equivalence cells, two adversarialKNOWN DIVERGENCEpins (scopedadmin_full_accessgrant; piecemeal platform-exclusive capability), the I2 nesting and I3 narrowing invariant cells, posture-blindness staging pins for the P1 flip, and the EXTERNAL dead-branch cell. Extracts the platform-admin capability probe as the exported purehasPlatformAdminCapability(mechanical, behavior unchanged). Test-only gate; the ADR-0099 P1 flip lands behind it (#3211). -
e38da5b: ADR-0099 P1 (#3211 M2): the Layer 0 cross-tenant exemption gate now reads the carried
ctx.posturerung (#2956) as authoritative, with the platform-admin capability probe demoted to a fallback for resolver-less contexts (delegated-admin bridge, sharing service,getReadFilter). The read and write (insert/update post-image) tenant checks share one decision (computeLayeredRlsFilter), so they cannot drift. A probe↔rung disagreement logs a defect breadcrumb and enforces the narrower rung verdict.Behavior change (security narrowing, multi-org /
@objectstack/organizationsonly): a principal whose carried rung is notPLATFORM_ADMINno longer crosses the tenant wall on private / platform-global / better-auth-managed objects, even when its resolved permission sets carry a platform-exclusive capability. Two shapes are affected: (a) a scopedadmin_full_accessgrant (sys_user_permission_set.organization_idnon-null), and (b) a custom set granting a platform capability (e.g.studio.access) piecemeal alongside a superuser bit. Both are now walled to their own org — the fail-safe direction (the carried rung is a strict subset of the probe). Single-org / env-per-database deployments are unaffected (Layer 0 is inert).Upgrade check: before upgrading, scan
sys_user_permission_setforadmin_full_accessrows with a non-nullorganization_id, and custom permission sets whosesystemPermissionsintersect{manage_metadata, manage_platform_settings, studio.access, manage_users}. To restore cross-tenant operator access for such a principal, grant the unscopedadmin_full_accessinstead. The[authz/ADR-0099]warn log names any principal hitting the divergence at runtime. -
f9b118d: ADR-0099 P2′ (#3211 M3′): pin the two-axis Amendment in the authz matrix. The original P2 (collapse the Layer 1 tier onto posture) was rejected — Layer 1's tier input is the per-object super-bit, a per-principal × per-object delegation primitive posture cannot represent. New cells pin: seeded-face agreement (seeded super-bit holders are already ≥ TENANT_ADMIN), the load-bearing delegation cell (a MEMBER with a delegated per-object
viewAllRecords/modifyAllRecordsshort-circuits Layer 1 yet stays walled by Layer 0 — the auditor pattern), invariant I7 (the scope axis never crosses a boundary posture has not opened), and the contrast that the bit is a real grantable capability, not conditionally inert. Test-only; zero behavior change. -
Updated dependencies [f972574]
-
Updated dependencies [22013aa]
-
Updated dependencies [3ad3dd5]
-
Updated dependencies [3a18b60]
-
Updated dependencies [a8aa34c]
-
Updated dependencies [e057f42]
-
Updated dependencies [a3823b2]
-
Updated dependencies [bc65105]
-
Updated dependencies [43a3efb]
-
Updated dependencies [524696a]
-
Updated dependencies [6b51346]
-
Updated dependencies [80273c8]
-
Updated dependencies [5e3301d]
-
Updated dependencies [dd9f223]
-
Updated dependencies [46e876c]
-
Updated dependencies [5f05de2]
-
Updated dependencies [021ba4c]
-
Updated dependencies [158aa14]
-
Updated dependencies [d2723e2]
-
Updated dependencies [fefcd54]
-
Updated dependencies [beaf2de]
-
Updated dependencies [369eb6e]
-
Updated dependencies [b659111]
-
Updated dependencies [5754a23]
-
Updated dependencies [6c270a6]
-
Updated dependencies [290e2f0]
-
Updated dependencies [668dd17]
-
Updated dependencies [8abf133]
-
Updated dependencies [e0859b1]
-
Updated dependencies [04ecd4e]
-
Updated dependencies [4d5a892]
-
Updated dependencies [16cebeb]
-
Updated dependencies [86d30af]
-
Updated dependencies [8923843]
-
Updated dependencies [ea32ec7]
-
Updated dependencies [a2795f6]
-
Updated dependencies [f16b492]
-
Updated dependencies [4b6fde8]
-
Updated dependencies [2018df9]
-
Updated dependencies [fc5a3a2]
- @objectstack/spec@16.0.0-rc.0
- @objectstack/platform-objects@16.0.0-rc.0
- @objectstack/core@16.0.0-rc.0
- @objectstack/formula@16.0.0-rc.0
- @objectstack/spec@15.1.1
- @objectstack/core@15.1.1
- @objectstack/formula@15.1.1
- @objectstack/platform-objects@15.1.1
- f531a26: OWD posture is now enforced on the runtime write path (#3050).
metadata-protocolgains the ADR-0094-addendumregisterAuthoringGate(type, gate)seam — an awaited, throwing pre-persistence hook insidesaveMetaItem(draft and publish-mode saves; environment writes only).plugin-securityregisters theobjectposture gate on it: an environment overlay of a packaged object may only TIGHTENsharingModel/externalSharingModel(ADR-0086 D1 — closes theOS_METADATA_WRITABLE=objectunvalidated-widening hole), andexternalSharingModel ≤ sharingModel(ADR-0090 D11) is now rejected at save time instead of only by CLI lint. Write-path only — stored metadata keeps loading unchanged.
-
f531a26: fix(security): fail-closed sentinel for on-behalf-of reads on getReadFilter (#2852)
getReadFilter(the read-scope provider the analytics/raw-SQL path binds to) resolves only the caller's own ceiling — the ADR-0090 D10 delegator RLS intersection that the engine middleware applies to find/count/aggregate is not implemented on this path. Computing a filter here for a delegated (on-behalf-of) context would therefore silently widen the read past the delegator's scope.Until the intersection is threaded through
computeRlsFilter(tracked with #2920 B1 / ADR-0095 D1),getReadFilternow denies fail-closed (deny sentinel + error log) whencontext.onBehalfOf.userIdis set. System on-behalf-of bypasses ahead of the guard, and no agent surface reaches analytics today, so this is a latent-invariant guard rather than a live-traffic behavior change. -
f531a26: feat(mcp):
aggregate_recordstool — GROUP BY aggregation over the engine read pathNew MCP tool
aggregate_records(count/sum/avg/min/max/count_distinct, optional groupBy incl. date bucketing, where filter, IANA timezone) in thedata:readfamily. Execution routes through the ObjectQL ENGINE (callData('aggregate')deliberately never uses the raw per-env driver), so RLS/tenant scoping and the D10 delegator intersection apply exactly as on find.Security hardening shipped with it:
- plugin-security: new FLS aggregate-INPUT gate — result masking never runs for
aggregate(output rows carry only aliases), so any groupBy / aggregation reference to an FLS-unreadable field is now rejected fail-closed with the offending field names (mirrors the FLS write gate). - runtime:
aggregatemaps to thelistApiMethod in the object exposure gate (an object whoseapiMethodswhitelist excludeslistcannot leak row statistics through GROUP BY), and the aggregate action requires at least one aggregation (the engine's in-memory path would otherwise degrade to raw rows that the FLS masker does not cover).
The bridge seam is optional: a runtime that does not implement
McpDataBridge.aggregatesimply does not register the tool (graceful degradation, same contract as the action tools). - plugin-security: new FLS aggregate-INPUT gate — result masking never runs for
-
f531a26: fix(security): exempt engine referential FK clears from the owner_id transfer guard (#3023)
Follow-up to the #3004 ownership-anchor guard.
owner_idis a lookup tosys_userwith the defaultdeleteBehavior: 'set_null', so deleting asys_usermakescascadeDeleteRelationsnullowner_idon every dependent row. That cascade write re-entered the write middleware under the deleter's context, where the #3004 guard read theowner_id = nullas a user-initiated disown and denied it — aborting the cascade mid-way (no transaction, so partial state) for any deleter without the transfer grant on the child object (e.g. a member clearing apublic_read_writechild that RLS would otherwise have allowed).The cascade FK clear is engine-mandated referential integrity consequent to an already-authorized parent delete, not a user ownership change.
cascadeDeleteRelationsnow tags theset_nullwrite with a server-derived__referentialFieldClearcontext marker (set by the engine, never built from a request — same trust model as__expandRead), and the ownership-anchor guard skips when that marker is present. Ordinary user writes are unaffected; the marker cannot be forged from client input, so it can never slip a real ownership transfer past the guard. -
f531a26: fix(security): guard the
owner_idownership anchor and scope bulk writes to owner-visible rows (#3004, #2982)Two write-path holes on the row-ownership anchor (
owner_id), the column OWD row-level scoping keys off to decide who may update/delete a record.-
#3004 — client-writable, unguarded
owner_id. The anchor is deliberately notreadonly(ownership is transferable), so the static-readonly strip never covered it and FLS doesn't gate it by default. A non-privileged writer could thereforeinserta record under someone else's name (forge) orupdateone to a new owner (transfer / disown), evading the owner gate that governs update/delete. The security middleware (plugin-security step 3.5) now treatsowner_idas system-managed for non-privileged writers: on insert an empty value is auto-stamped to the acting user (batch rows too — previously only the single-record path stamped, leaving bulk-inserted rows NULL-owned and invisible to their creator), and a supplied foreign owner is denied; on update a suppliedowner_idis a transfer/disown and is denied — the unchanged no-op echo of a form save is tolerated via a pre-image compare, and a bulk change-set carryingowner_idfails closed. A non-scalarowner_id(array/object) is rejected outright rather than string-coerced, and the change-set membership test uses own-property semantics so a polluted prototype cannot spoof an ownership write. Both require the transfer grant (allowTransfer, ormodifyAllRecordswhich implies it) to proceed. System context (ctx.isSystem) stays fully exempt (OAuth provisioning / cron snapshots / seed claims / migrations), and under delegation both principals must hold the grant (ADR-0090 D10 intersection). Note a REST import runs under the importer's own context (notisSystem), so a non-privileged user importing a CSV whoseowner_idcolumn names other users is correctly denied unless they hold the transfer grant — administrators (who carrymodifyAllRecords) are unaffected. -
#2982 — bulk writes skipped owner scoping on OWD-
privateobjects. Aupdate({ multi: true })/ bulk delete rebuilt the driver AST fromoptions.whereAFTER the middleware chain, discarding the owner/RLS write filter that plugin-sharing (buildWriteFilter) and plugin-security compose ontoopCtx.ast— so a member's bulk write hit every matching row, including peers'. The engine now seedsopCtx.astfrom the caller's predicate BEFORE the chain (the same seam reads use) and hands the middleware-composed AST todriver.updateMany/driver.deleteMany, so bulk writes are constrained to the rows the caller may edit — matching single-id write behavior.deletenow applies the same scalar-idguardupdatealready had, so an id-list bulk delete (where: { id: { $in: […] } }, multi: true) is owner-scoped too, and both multi branches fail CLOSED (throw) rather than silently rebuilding an unscoped predicate if the row-scoping AST is ever absent.Consequences of routing bulk writes through the AST: the anti-oracle predicate guard now also applies to bulk
update/delete(a bulk write filtering on an FLS-unreadable field is rejected, as reads already are), and a principal-less (no-userId, non-system) bulk write on an owner-scoped object now correctly affects zero rows instead of all of them.
Proven end-to-end on the real showcase app (
packages/qa/dogfood/test/owner-anchor-and-bulk-writes.dogfood.test.ts) and pinned in the ADR-0096 authz-conformance ledger (ownership-anchor-guard,bulk-write-owner-scoping). -
-
f531a26: fix(security): scope the bulk-write predicate guard to the caller's own filter, and dedupe pre-image reads (#3018 review follow-ups)
Two hardening follow-ups from the #3018 adversarial review.
Predicate guard is now middleware-order-independent for writes. #2982 made bulk
update/deletecarry anopCtx.ast, which brought them under the step-2.9 anti-oracle predicate guard for the first time. That guard is documented to run against the caller's own predicate — RLS / sharing filters legitimately reference fields the caller cannot read (e.g.owner_id). But for a bulk write it inspectedopCtx.ast.where, which a sibling middleware (plugin-sharing) may have already had anowner_idowner-match composed into — and the two middlewares' registration order is not contractually guaranteed. On an object whoseowner_idis FLS-hidden, that could 403 a legitimate bulk write purely because the injected filter named the field. The guard now inspectsopCtx.options.where(the caller's untouched predicate) forupdate/delete, so it can never mistake an injected owner/RLS filter for a caller probe, independent of middleware order. Reads are unchanged (the read seed is the caller's query verbatim and the guard runs before this middleware's own injection).Pre-image reads deduplicated. The by-id "read the target row" pattern was inlined at ~5 gates with slightly divergent shapes; a single
readRowByIdhelper (fail-closed: missing engine / null id / thrown read →null, which always denies) now backs the provenance gates, and a memoizedgetCallerPreImagecollapses the owner-anchor echo check (3.5) and the RLScheckpost-image (3.6) — which read the identical(object, id, caller-context)row — into one read per operation. No behavior change; the read shape can no longer drift across sites. -
f531a26: fix(security): public-form submissions can no longer forge server-managed anchors (#3022)
The anonymous public-form surface (ADR-0056 Option A,
POST /forms/:slug/submit) is authorized by the declaration-derivedpublicFormGrant, which short-circuits the security middleware BEFORE every write gate (CRUD, FLS, the owner anchor guard, the tenant CHECK). The only field-side defense was the route's declared-field allow-list — and a FormView with zero declared section fields fell back to merging the raw body wholesale, so an unauthenticated visitor couldPOST owner_id=<victim>(ororganization_id, audit columns,id) and attach the record to another user or tenant — the #3004 insert-forge, with no credentials at all.Server-managed anchors are now enforced on this surface at BOTH layers, from a single shared definition (
PUBLIC_FORM_SERVER_MANAGED_FIELDS, new in@objectstack/spec/security):- Data layer (authoritative) — the
publicFormGrantbranch in@objectstack/plugin-securitystripsid/owner_id/organization_id/tenant_id/ audit columns / soft-delete state /__searchfrom every row of a granted insert (batch included) before admitting the write, so the boundary holds no matter what any route lets through. Ownership stays NULL for object hooks / the first-admin bootstrap to assign, as for other anonymous-seeded rows. - Route layer — the submit allow-list excludes the same set
unconditionally: an explicitly declared
owner_idsection field no longer passes, and the zero-declared-sections fallback keeps its documented all-fields behavior for business columns while refusing the managed set. The resolve route (GET /forms/:slug) drops the managed fields from the rendered sections and the embedded object schema so a form never collects a value the submit refuses, andGET /forms/:slug/lookup/:fieldrefuses apublicPickerdeclared on a managed anchor (which would have opened anonymoussys_usersearch throughowner_id).
Authenticated writes are unaffected — this is the anonymous-surface rule only;
owner_idtransfer semantics for signed-in callers stay governed by the transfer grant (#3004 / PR #3018). - Data layer (authoritative) — the
-
f531a26: fix(security): enforce referenced-object RLS/FLS on $expand (#2850)
expandRelatedRecordsresolved lookup/master_detail/user references via the driver directly, so the referenced object's row- and field-level security never ran — any API/session caller who could read a base row could?expand=a foreign key and receive RLS-hidden rows and FLS-masked fields (tenant isolation was the only surviving boundary).The expand batch now routes through the engine's own
find, so the security middleware applies the referenced object's RLS + FLS to theid $in [...]batch (one query per level, no N+1). The sub-read carries a server-set__expandReadmarker: the middleware waives only the object-level CRUD / requiredPermissions gate for PUBLIC referenced objects (already broadly readable — avoids over-blocking common status/owner lookups), while PRIVATE referenced objects keep the full gate. Covers the list and single-record REST/protocol surfaces. -
f531a26: fix(plugin-security): stop clobbering admin-edited capability
scopeon boot (#2909 T3).scopeis an admin-editable classification select on sys_capability, but the curated seeder refreshed it on every boot alongside label/description — silently reverting admin reclassifications. It is now seed-once: written on insert, never refreshed (a curated scope change in a new platform version requires a data migration; recorded in the ADR-0094 addendum). -
f531a26: fix(plugin-security): re-arm the sys_position system-row write gate after the A4 managed_by rename (#2926 ①). The gate's provenance map still keyed on the legacy
system/configvalues while rows are now stamped (and boot-normalized to)platform/package, so platform/package-managed positions — including theeveryone/guestaudience anchors — could be physically deleted through the data API once their bindings were removed. The map now guards both the canonical and legacy vocabularies, and the misleading "no runtime path branches on legacy values" safety notes were corrected. -
f531a26: fix(plugin-security): bind the fallback permission set to the
everyoneanchor AFTER the anchor is seeded. The baseline auto-bind (ADR-0090 D5) ran earlier inrunBootstrapthanbootstrapBuiltinRoles, which creates theeveryoneposition — so theeveryonelookup returned nothing and the app'sisDefaultset was never bound, leaving a fresh deploy'severyoneempty (personas silently degraded) and a redundantsys_audience_binding_suggestionfiled for the same set. The auto-bind now runs afterbootstrapBuiltinRolesand beforesyncAudienceBindingSuggestions, so the documented app-level auto-bind actually happens and the suggestion sync correctly skips the already-bound set. -
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [3fe9df1]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [4109153]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [627f225]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
- @objectstack/spec@15.1.0
- @objectstack/platform-objects@15.1.0
- @objectstack/core@15.1.0
- @objectstack/formula@15.1.0
-
0fcef9b: ADR-0095 D1: tenant isolation is now Layer 0 — an independent, always-first, AND-composed filter (
tenant-layer.ts), no longer a wildcardtenant_isolationRLS policy OR-merged with business RLS. The effective row filter isLayer0(tenant) AND Layer1(business RLS); the two share no compiler, merge step, or bypass bit. The superuser bypass now exempts the tenant wall only as a Layer 0 rule (platform-admin posture + object posture permits: private / platform-global / better-auth-managed), never via a business-RLS short-circuit.BREAKING (multi-org
tenancy.mode = 'multi'deployments only;singlemode is inert and unchanged). Retiring the OR-merged tenant policy resolves four behavior deltas, all toward stronger/correcter isolation:- (a) Cross-tenant read leak closed. A permissive business RLS policy (e.g.
status == 'public') no longer OR-widens tenant scope; a foreign-org row it matched is now invisible. - (b) Member by-id writes narrow to owner-only. The OR-merge silently widened
owner_only_writes(created_by == me) back to org-wide, so a member could by-id update/delete any record in their org. Writes are now owner-scoped as authored. Migration: if your deployment intentionally relied on members editing each other's records org-wide, grant an explicit per-object edit permission set (position-distributed) where that is wanted — the baselinemember_defaultno longer permits it. - (c) Global-catalog objects visible to members. On a
tenancy.enabled:falseobject, members were scoped by a phantomorganization_idfilter (a column such objects lack); Layer 0 correctly treats them as non-tenant, so the global catalog is visible. - (e) No-active-org writes fail closed. A write by a principal with no active organization on a tenant object is now denied (was owner-scoped only).
tenant_isolationis retired from the seededorganization_admin/member_default/viewer_readonlysets; the_self/_orgidentity-table carve-outs andowner_only_writes/deletesare unchanged. Customized seeded sets keep their overlays (ADR-0094). The driver-levelapplyTenantScopeseam is untouched. See ADR-0095 and framework#2936 (theextractTargetField==blind spot this exposes, tracked separately). - (a) Cross-tenant read leak closed. A permissive business RLS policy (e.g.
-
2ae78c6: fix(plugin-security): #2937 — Layer 0 insert post-image 租户检查(伪造 organization_id 的用户 insert 现被拒)
安全修复 + 行为变更(release-notes callout)。 多组织模式(
tenancy.mode='multi'+@objectstack/organizations)下,一个普通用户此前可以insert一条伪造organization_id(指向别的租户)的业务记录并使其落进受害租户 —— Layer 0 的租户墙 AND-composed 到读 + update/delete 的 pre-image,但 insert 没有 pre-image、也不带 AST,从未被门控(ADR-0095 D1 读侧 W1 的写侧未竟部分)。新增 SecurityPlugin 中间件步骤 3.7:对 insert 的 post-image 复用读侧同一套 Layer 0 决策 (
computeInsertTenantCheckFilter→computeLayeredRlsFilter的layer0)做校验 —— 一个显式提供的organization_id必须等于调用者的活动组织,否则 fail-closed 拒绝。规则与 读侧完全一致:单组织/隔离未激活、非租户对象(无organization_id列或tenancy.enabled:false)、 platform-admin 姿态豁免的对象均不适用;无活动组织的用户提供任意 org_id → 拒(deny sentinel)。行为 delta(需注意): 此前能成功的「带跨租户
organization_id的用户级 insert」现被拒绝。 缺省(不提供organization_id)的 insert 不受影响 —— 补全仍由@objectstack/organizations的 auto-stamp 负责(职责分离,因此本检查与 auto-stamp 中间件的注册顺序无关)。系统上下文 (isSystem,含 import 引擎 / 迁移 / 每-org seed replay·clone·orphan-claim 的SYSTEM_CTX) 在中间件入口即短路,合法的「代客设置 org_id」写路径完全不受影响。矩阵门:
authz-matrix-gate.test.ts新增[#2937] Layer 0 insert post-image tenant guard八格(伪造异租户 → 拒、同租户 → 通过、缺省 → 放行、无活动组织 → 拒、platform-admin 私有对象豁免、 public 业务对象不豁免、tenancy-disabled 对象不适用、单组织模式不检查)。授权一致性 ledger 新增multi-tenant-insert-postimage行。配套 cloud@objectstack/organizations的 auto-stamp 权威覆盖(纵深防御)。Closes #2937。 -
ef70521: fix(plugin-security): 堵跨租户 UPDATE 写 + org_admin 越 private 租户对象墙(security)
安全修复 + 行为变更(release-notes callout)。 修复 security review 确认的两个租户墙授权漏洞,两者同在
security-plugin.ts/tenant-layer.ts的写侧热路径。多组织模式(tenancy.mode='multi'+@objectstack/organizations)下生效。Finding 1 [BLOCKER] — 经 UPDATE 重指
organization_id的跨租户写。 #2937 的 Layer 0 insert post-image 检查(中间件 step 3.7)只管 insert。对称的 update 路径无人管:成员拥有 org A 的记录 R,对 R 发 by-id 或 bulkupdate带{organization_id: 受害者 org B},即可把行移动进任意租户——auto-stamp(insert-only)、FLS、服务端未强制的readonly、Layer 0 pre-image(只校验旧 org)、显式 RLS check 全部漏过。修法(Option B,最小面 + 与 insert 对称):把 step 3.7 的 Layer 0 post-image 检查扩到 update,复用同一套 Layer 0 决策(computeWriteTenantCheckFilter→computeLayeredRlsFilter的layer0)。一个显式提供的organization_id必须过 Layer 0(== 调用者活动组织),否则 fail-closed 拒绝——这令非平台用户上下文里organization_id事实不可变(只有活动组织值能过,而 pre-image 已把目标锁在活动组织内,故重指到任何其他租户被拒)。缺省(不碰 org_id)的 update 不受影响;bulk update 的跨租户 change-set 也被堵。Finding 2 [HIGH] — org_admin 在 private 租户对象上越租户墙。 Layer 0 跨租户豁免门此前用「持有
viewAllRecords/modifyAllRecords」判定。organization_admin(自动授给每个 org owner/admin)经其'*'通配持有这两个超级位,于是在access.default:'private'的租户业务对象上触发豁免 → 零过滤 → 读写所有租户的行。修法:把 Layer 0 豁免门从「超级位」收窄为真正的平台管理员判定(hasPlatformAdminPosture:持有平台专属能力manage_metadata/manage_platform_settings/studio.access/manage_users,即admin_full_access携带而organization_admin刻意不给的那组)。超级位继续只驱动 Layer 1 业务 RLS 短路(TENANT_ADMIN 组织内见全行、无所有权收窄)。因新豁免是旧门的严格子集,只会收窄、绝不放宽(fail-safe)。行为收窄(预期的安全收窄,需注意): org admin 不再在 private/platform-global/better-auth 的租户对象上越租户墙——它现在被 Layer 0 墙到自己的 org。真·平台管理员(
admin_full_access+ 平台 systemPermissions)仍豁免;better-auth 托管身份表 carve-out 不受影响(无organization_id列,Layer 0 本就 inert)。系统上下文(isSystem,含 import/迁移/seed 的合法跨组织移动)在中间件入口即短路,完全不受影响。为何不用
ctx.posture作豁免门: B2 已把PLATFORM_ADMINposture 落进resolve-authz-context.ts的ctx.posture,但该字段未被 plumb 进 enforcement 中间件收到的 ExecutionContext(rest-server 与 runtime dispatcher 都丢弃了它),直接消费会静默 no-op。改用平台专属能力探针,读的是 enforcement 已用的同一套 permission sets,覆盖所有入口,且天然 fail-safe。矩阵门:
authz-matrix-gate.test.ts更新private_obj.org_admin格(readnull→{organization_id:'org-1'})并新增[Finding 1 …](8 格:成员重指异租户 → 拒、同租户 → 通过、不碰 org_id→ 放行、无活动组织 → 拒、org_admin 重指 → 拒、platform-admin private 对象 → 放行、public 对象 → 拒、单组织 → 不检查)与[Finding 2 …](5 格:org_admin private 对象读/写墙到本租户、真平台管理员仍豁免、org_admin public 对象回归、better-auth carve-out 不受影响)。授权一致性 ledger 更新multi-tenant-write-postimage(覆盖 insert+update)并新增multi-tenant-exemption-posture。关联 #2920。
-
5febe3f: feat(plugin-security): A4 — managed_by tri-state unification + listView exposure (#2920)
Unifies the record-level provenance vocabulary across the three RBAC catalogs (
sys_capability,sys_permission_set,sys_position) onto a single tri-state — platform / package / admin — so an administrator reads one vocabulary for "who owns this" everywhere.sys_permission_set.managed_byandsys_position.managed_byconverted from freetextto a constrainedselectmatchingsys_capability(optionsplatform/package/admin,defaultValue: 'admin',readonly).- Writers re-stamped to canonical vocab: built-in identity/anchor positions
now seed
managed_by: 'platform'(was'system'); env/Studio-authored permission sets project asmanaged_by: 'admin'(was'user'). Declared package sets ('package') and platform capabilities ('platform') were already canonical. sys_positionlist views (active/default_positions/custom/all_positions) now surface themanaged_bycolumn, matching the capability and permission-set views.- Back-compat, no destructive migration. No runtime path branches on the
legacy values — every access decision keys on
'package'/'platform'(both unchanged) — so the rename never changes an authorization outcome. Built-in positions and declared sets self-heal on their next bootstrap upsert; a new idempotentkernel:readyreconciler (normalizeManagedByVocab) rewrites the residual legacy values (system→platform,config→package,user→admin) on existingsys_position/sys_permission_setrows. - i18n:
managed_byfield + option labels (platform/package/admin) added forsys_capability/sys_permission_set/sys_positionacross en / zh-CN / ja-JP / es-ES.
Pairs with objectui
feat(app-shell): A4 — provenance tri-state badge(framework#2920). -
e62c233: feat(spec,plugin-security): package-level capability declaration API (ADR-0066 D1)
Packages can now DEFINE their own authorization capabilities explicitly via the new
defineCapabilityfactory and a stack'scapabilitiesarray, instead of relying on the implicit "derive an untitled capability from whatever a permission set references insystemPermissions[]" back-door.@objectstack/spec: newdefineCapability/CapabilityDeclarationSchema({ name, label?, description?, scope, packageId? }) and acapabilitiesfield on the stack definition.@objectstack/plugin-security: newbootstrapDeclaredCapabilitiesseeds declared capabilities intosys_capabilitywithmanaged_by:'package'+package_idprovenance (newpackage_idfield on the object). Idempotent, upgrade-aware; refuses to hijack curated platform capabilities or another package's rows, never clobbers admin-authored rows, and CLAIMS a pre-existing derived placeholder (upgrading it to package provenance). The implicit derive-from-systemPermissionspath still runs for back-compat but now skips any explicitly-declared name so it can't clobber authored metadata.@objectstack/runtime: stack-declaredcapabilitiesare registered into the metadata registry (typecapability) so the boot seeder can read them.@objectstack/lint:validateCapabilityReferencestreatsstack.capabilitiesnames as a known capability source.
A capability is not a contract: DEFINE it (
defineCapability), GRANT it (systemPermissions), REQUIRE it (requiredPermissions) — noinputs. Aligns with ADR-0094 D5 (retire implicitmanaged_by-guessing back-doors). -
a581a65: feat(plugin-security): C2-β — explain 引擎 record 粒度行级归因 (#2920)
explain(principal, object, operation, recordId?)现支持记录级解释。透传recordId时,引擎在对象级流水线之上叠加行级归因,全部复用 enforcement 同一批函数(explained-by-construction):tenant_isolationLayer 0:作为永远最先的层被 prepend;每层打上kernelTier(layer_0_tenantvslayer_1_business),可区分「租户墙挡的」还是「业务 RLS 挡的」。- 每层
record归因(tenant / owd_baseline / sharing / rls):outcome(admitted/excluded/not_evaluated)、有效rowFilter、matchesRecord(用@objectstack/formula的matchesFilterCondition对同一条 FilterCondition 求值)、命中的rules[](tenant_filter/owd_baseline/ownership/record_share/sharing_rule/team/rls_policy,含 grants/via/effect)。 - 顶层
record判定:visible+decidedBy决定性层。读走复合行过滤匹配,写走 sharing service 的canEdit(均为 enforcement 原语)。 principal.posture:ADR-0095 D2 档位(PLATFORM_ADMIN/TENANT_ADMIN/MEMBER/EXTERNAL)的 B2 stand-in 派生(复用resolveAuthzContext已投影的 platform_admin / org 角色证据),待 B2 合并后替换。computeRlsFilter重构为computeLayeredRlsFilter(暴露{ layer0, layer1 }拆分)+ 薄 andCompose 包装,单一代码路径,行级归因不会与执行漂移。- REST
security.explain(GET/POST)接受可选recordId。
向后兼容:无
recordId的对象级请求输出 byte-identical——无tenant_isolation层、无kernelTier、无posture、无record。
-
ca2b2f6: fix(plugin-security): #2936 — RLS field-existence / tenancy-disabled safety nets now recognize canonical
==extractTargetField(the lightweight left-hand-field parser feeding the Layer 1 field-existence net and the tenancy-disabled skip net incomputeLayeredRlsFilter) only matched the legacy single-=/INshape. It returnednullfor canonical CEL==(field == …), which is how real seeds and business policies author equality. Anulltarget field means "keep the policy", so both safety nets were inert for every==policy: a wildcard policy targeting a column the object lacks was NOT failed closed, and a==-formorganization_idpolicy on atenancy.enabled:falseobject was NOT skipped. The regex now recognizes==(listed before=so the ordered alternation does not mis-match the first=), alongside the existing=/IN. Recognition is only extended — the net semantics are unchanged, and!=/>/</>=/<=still returnnull(conservative keep), matching prior behavior for any unmatched shape.Behavior delta (fail-closed strengthening, same effective visibility): the wildcard
owner_only_writes/owner_only_deletesseed policies (created_by == current_user.id) now correctly fail closed on an object that lacks acreated_bycolumn (platform-global / system tables). Previously they slipped the net and compiled to a phantom{ created_by: … }filter against a missing column — a driver-dependent, effectively-deny result; now the net drops the sole applicable write policy and yields the deny sentinel. A member could not by-id write such a column-less object either way, so the visible/writable row set is unchanged; only the mechanism is now an explicit fail-closed deny. All ordinary tenant/business objects carrycreated_by, so they are unaffected (proven green by the dogfood authz-conformance + RLS matrices). The tenancy-disabled skip net has no effect on any current seed (no==seed policy targetsorganization_idon a tenancy-disabled object). The tenant wall itself is Layer 0 (tenant-layer.ts), which never used this parser, so tenant isolation is unaffected (ADR-0095 D1). -
698454e: Security fix: constrain self-delegation (D3) position anchor to prevent lateral visibility escalation (cloud#830 follow-up).
cloud#830 (C1 position-anchor) made
sys_user_position.business_unit_idvisibility load-bearing — it is the readScope depth anchor, so aunit/unit_and_belowholder sees the owner set rooted at that BU (and, forunit_and_below, its whole subtree). The delegated-admin gate's self-service delegation path (assertSelfDelegation) stamped this anchor with no subtree/source constraint: a holder of a delegatable, non-admin-scope position anchored at a LOW business unit could delegate it to a co-conspirator with an ancestor / arbitrary-high anchor, leaking that BU's whole subtree of member records — visibility beyond the delegator's own range. Mutual delegation could grant it both ways.The gate now requires a self-delegated
business_unit_idto fall inside the delegator's own effective anchor for that position (the subtree of their own direct holding's anchor, or of their member BU when the holding is unanchored) — the same "assignments must target your subtree" spirit as the D12 delegated-admin boundary. Fail-closed: an anchor that cannot be proven inside the delegator's range is rejected. Unanchored delegation rows keep prior behavior (the delegate resolves to their own member BU — not a widening). The "anchoring only narrows, never widens" invariant now holds on the D3 path too. -
29a4c90: fix(plugin-security): explain posture 证据对齐 enforcement 派生(消除标签漂移)
Security review 低危项。explain-engine 的
derivePosture(context)之前用松名字匹配作 posture 证据——permissions.includes(ADMIN_FULL_ACCESS)(不校验非作用域)+positions.includes( 'org_owner'/'org_admin')(读 better-auth 角色),比 enforcement(resolve-authz-context.ts的hasPlatformAdminGrant:要求非作用域 admin_full_access user grant;TENANT_ADMIN 用organization_admin能力而非角色)更松,可能让 explain 面板给运维显示偏高的 posture 标签 (作用域 org-admin grant 被误标 PLATFORM/TENANT_ADMIN)。修法——让 explain 的 posture 走 enforcement 已用的同一份证据:
- 优先直接消费
ctx.posture:principal 经完整resolveAuthzContext时已带 enforcement 派生的 rung,逐字返回 → 结构上不可能漂移。 - 回退(explain 用
buildContextForUser自建 context,不经完整 resolveAuthzContext):复制 enforcement 的非作用域 grant 判定——buildContextForUser现按与hasPlatformAdminGrant逐字节 一致的规则(admin_full_access且organization_id == null的 active user grant)计算并挂出hasPlatformAdminGrant;derivePosture以此 + 投影出的platform_admin内建岗位判 PLATFORM_ADMIN, 以organization_admin能力判 TENANT_ADMIN,不再读org_owner/org_admin角色岗位 (ADR-0095 D3:角色只是 provisioning 来源,非裁决输入 — explain 侧同样闭合 #2836 dual-track)。 - 保留 explain 特有的 guest → EXTERNAL 底(enforcement floor 是 MEMBER),且置于最前。
只改 explain 的 posture 标签证据,不改 explain 的 allow/deny verdict(来自复用的 enforcement filter),不改 enforcement。#2947 跟踪的「posture 未 plumb 进 enforcement context」更广缺口不在本 任务范围。关联 #2920。
- 优先直接消费
-
5774a75: 内置行写护栏:
sys_position/sys_capability的平台/应用托管行不再可被客户管理员删改。sys_permission_set早有两道门写护栏(assertPackageManagedWriteGate)拦截对 package 托管行的写入,但sys_position/sys_capability缺失对应保护——平台/应用发布的系统岗位与能力(provenance 记录在managed_by)可被管理员直接 delete / update 直达驱动,静默破坏应用的授权基线(ADR-0049:provenance 字段存在却无强制 = 正是要补的 enforcement gap)。新增
assertSystemRowWriteGate(packages/plugins/plugin-security/src/security-plugin.ts,data-write hook 接线与 package 门同处),对这两个对象的托管行施加一道无条件的数据层边界:- 禁止伪造托管来源:管理员门的 insert / update 载荷(单对象或数组)不得把
managed_by盖成平台/应用值——只有携带isSystem的平台 seeder / 包发布路径可写;同时封堵 update-to-forge(把自建行改 badge 成托管行)。 - 拒绝改删托管行:对
managed_by已是平台/应用值的行,delete/update/transfer/restore/purge一律拒绝。与sys_permission_set不同,这两个对象没有 ADR-0094 overlay write-through,故写护栏必须在此层直接拒绝,而非下放给下游翻译。 - 管理员自建行不受限:
managed_by为user/∅(sys_position)或admin(sys_capability)的行完全归管理员所有(含委派管理员在自己 subtree 内的自建行)。
护栏 fail-closed 且不依赖调用方授权——持
modifyAllRecords的超管也无法删除平台岗位。两对象的managed_by词表不同(sys_position:system/config托管,user/∅ 自建;sys_capability:platform/package托管,admin自建),网关按对象分别判定。错误信息仅含业务文案("此岗位/能力由 平台|应用包 提供,不可删除/修改")。与 delegated-admin 边界不冲突:
GOVERNED_OBJECTS本就不含这两个对象,委派管理仍治理 RBAC 链接表而非定义对象。 - 禁止伪造托管来源:管理员门的 insert / update 载荷(单对象或数组)不得把
-
Updated dependencies [02a014b]
-
Updated dependencies [28b7c28]
-
Updated dependencies [13749ec]
-
Updated dependencies [e62c233]
-
Updated dependencies [ed61c9b]
-
Updated dependencies [31d04d4]
- @objectstack/platform-objects@15.0.0
- @objectstack/spec@15.0.0
- @objectstack/core@15.0.0
- @objectstack/formula@15.0.0
-
f0acf25: Surface a
customizedflag onsys_permission_setso Setup can tell — at a glance — which packaged permission sets have an environment overlay (ADR-0094).- The env projector stamps
customized: trueon amanaged_by:'package'row while an overlay shadows its shipped baseline, and clears it when the overlay is removed (the data-door "reset"). Env-authored rows are never flagged (an env set is the definition, not a customization of one). - The new read-only boolean field is added to
sys_permission_setand to the "All" Setup list view (alongsidemanaged_by), so a packaged-but-customized set is visible without opening the Studio layered diff.
- The env projector stamps
-
712328a: Package-owned permission sets are now customizable through the standard environment metadata overlay (ADR-0094 D5, revised — closes framework#2898 by making the overlay FIRST-CLASS instead of rejecting it).
- An env-scope
saveMetaItem('permission', …)on a package-owned set is a real customization: the awaited projector applies the effective (overlay-wins) body to thesys_permission_setrecord while preserving itsmanaged_by:'package'+package_idprovenance, and the evaluator enforces it. - A data-door edit of a packaged set (Setup PATCH) is translated into exactly that overlay — no more flat 403; a data-door "delete" removes the overlay and RESETS the record to the shipped declaration (the row survives).
- The ADR-0086 two-doors data gate narrows to what stays structurally true: forging package provenance through the admin door remains refused, as do the lifecycle ops with no overlay translation (
transfer/restore/purge) on package rows; kernels without a metadata overlay layer keep the legacy full refusal. - Cross-package roles compose via positions (bind several packages' sets); overlays narrow. Rationale: rejecting the overlay would make
permissionthe one type whose declaredallowOrgOverride: trueis a lie, and clone-to-customize forks away from vendor baseline updates.
Note the standard overlay trade, now applicable to permission sets: while an overlay pins a set, later vendor baseline changes (including tightenings) don't take effect for that name until the overlay is reset or re-authored — surfaced by the Studio layered diff and covered by ADR-0091 recertification.
Also lands a dogfood proof (
showcase-permission-projection) covering the full ADR-0094 invariant set — write-through, awaited projection, declared-set edit becomes an enforced overlay, package-set customize/reset lifecycle — registered in the liveness proof registry. - An env-scope
-
1dede32: Make the
sys_permission_setdata record a pure projection of the metadata layer (ADR-0094; framework#2875) — one authoritative store for permission-set definitions, retiring the two-store split-brain behind the #2857 display-freshness class.@objectstack/metadata-protocol: newregisterMutationProjector(type, fn)— an awaited, best-effort per-type hook invoked after persistence insidesaveMetaItem/publishMetaItem/deleteMetaItem, so a derived data-plane read-model is already consistent when the write returns (outcome surfaced asprojectionAppliedon the response). Complements the fire-and-forgetonMetadataMutationlisteners.@objectstack/plugin-security: every non-system data-door write onsys_permission_set(Setup CRUD, bulk imports, any ObjectQL path) is redirected into the metadata store by an engine middleware; the record is written only by the projector. Boot reconciliation projects env overlays onto records (Studio-created sets now appear in Setup), backfills legacy data-door-only records into metadata once, and re-projects drifted records from the effective body (metadata wins). The projector also syncs the metadata manager's in-memorypermissionentry, so evaluator resolution and the Setup display can no longer disagree.
Behavior changes: "deleting" an artifact-backed permission set through the data door now resets it to its declared body instead of removing the row; renaming a set through the data door is rejected (
400) — clone to a new name instead; record edits that predate this change and are shadowed by a metadata definition are discarded (loud warning) at first boot, since they were never enforced.Moved exports (from
@objectstack/plugin-security):upsertEnvPermissionSetnow lives inpermission-set-projection.js(still re-exported from the package root) and creates missing records;projectEnvPermissionOnMutation/subscribeEnvPermissionProjectionare replaced byprojectPermissionMutation/registerPermissionSetProjection.
-
a199626:
claimSeedOwnershipnow skips external (federated) objects — those with anexternalremote-table binding (ADR-0015) — the same way it already skipsmanagedByandsys_*objects.The seed-ownership backfill walks every registered object that exposes an
owner_idcolumn and re-owns its unowned rows to the first admin. Federated objects getowner_idauto-injected into their schema, so they passed the filter and the backfill issuedselect id from <remote_table> where owner_id is nullagainst a read-only remote datasource whose table may not be provisioned yet at boot — producing startup errors likeFind operation failed … no such table: customers. External objects are read-only (DDL forbidden, writes double-opt-in) and their ownership is not the platform's to reassign, so they are excluded from the scan entirely. -
Updated dependencies [16b4bf6]
-
Updated dependencies [16b4bf6]
-
Updated dependencies [10e8983]
-
Updated dependencies [607aaf4]
-
Updated dependencies [bb71321]
- @objectstack/spec@14.8.0
- @objectstack/platform-objects@14.8.0
- @objectstack/core@14.8.0
- @objectstack/formula@14.8.0
-
824a395: Tenancy mode as a first-class capability + a single owner for the user→membership lifecycle (ADR-0093, Phases 1–3).
Tenancy service (
@objectstack/types,@objectstack/plugin-auth). plugin-auth registers atenancyservice — the single source of truth for tenancy mode (mode,isolationActive,requested,degraded,defaultOrgId()). It derivesisolationActivefrom the presence of theorg-scopingservice, so the enterprise@objectstack/organizationspackage lights it up with no change. SecurityPlugin's RLS-strip gate and/auth/config(features.multiOrgEnabled, newfeatures.degradedTenancy) now consume it instead of re-deriving the fact.Fail-fast on degraded tenancy (
@objectstack/cli, ADR-0093 D5).OS_MULTI_ORG_ENABLED=truewithout a working@objectstack/organizationsnow refuses to boot — a deployment that requested tenant isolation must not serve traffic without it (tenant RLS would be silently stripped). Escape hatch:OS_ALLOW_DEGRADED_TENANCY=1boots in an explicitly branded degraded state (features.degradedTenancy). This may halt upgrades for deployments that were silently degraded — intentionally; install the enterprise package or set the escape hatch.Membership reconciler (
@objectstack/plugin-auth, ADR-0093 D1–D3, D6). A single reconciler composed into better-auth'suser.create.afterhook owns the "every new user gets a membership" invariant across all creation paths (signup, admin create-user, import, SSO JIT). It yields to any existing membership (host hooks win), honors a newmembershipPolicy: 'auto' | 'invite-only'auth option (defaultauto), and binds only to an unambiguous target org (single-org default; multi-org binds nothing). A bounded, idempotentkernel:readybackfill covers pre-existing member-less users in single-org/auto deployments (OS_SKIP_MEMBERSHIP_BACKFILL=1to opt out). The endpoint-level create-user bind from #2882 now delegates to this shared reconciler.New env vars:
OS_ALLOW_DEGRADED_TENANCY,OS_SKIP_MEMBERSHIP_BACKFILL. New docs: Deployment → Tenancy Modes & Membership. -
Updated dependencies [d6a72eb]
- @objectstack/spec@14.7.0
- @objectstack/core@14.7.0
- @objectstack/formula@14.7.0
- @objectstack/platform-objects@14.7.0
-
6e2b8ae: Project env-scope permission-set edits onto the
sys_permission_setrecord (#2857).A
sys_permission_sethas two representations: the authoritative metadata the structured editor writes, and the queryable data record (snake_case JSON-string columns) the admin/Setup surface reads. The metadata→record projection (toRowFields/upsertPackagePermissionSet) ran only at boot and on publish (package door), and the publish path refuses env-authored rows — so an environment-scopesave('permission', …)updated thesys_metadataoverlay (and the layered read) but left thesys_permission_setrecord stale (split-brain). Enforcement reads the authoritative metadata so access stayed correct, but the admin surface showed old values.Adds the environment door:
subscribeEnvPermissionProjectionhooks the protocol's post-persistenceonMetadataMutationchoke point; on an active (non-draft)permissionsave it re-reads the fresh effective body via the layered read (the boot-cached metadata registry would return a stale declared body) andupsertEnvPermissionSetprojects the six facets onto the record. Ownership is decided by the record'smanaged_by— env-authored rows (platform/user/absent) are projected; a package-owned record's baseline is left to boot re-seed / publish, so the two doors never fight. Mirrors the existingauthored-translation-syncmutation-listener pattern. -
Updated dependencies [609cb13]
-
Updated dependencies [ce6d151]
- @objectstack/spec@14.6.0
- @objectstack/platform-objects@14.6.0
- @objectstack/core@14.6.0
- @objectstack/formula@14.6.0
-
f70eb2c: ADR-0090 D10 — agent/service intersection runtime. When a request's principal acts
onBehalfOfa user (an AI agent or a service acting for a person), the effective permission is now the INTERSECTION of the principal's own grants and the delegator's grants — never the union. Confused-deputy prevention: an over-privileged agent may never see or touch anything the user it stands in for could not, and vice-versa. PreviouslyprincipalKind:'agent'/onBehalfOfwas a P1 context shape the evaluator did not read.The intersection is applied at EVERY axis, gated on the presence of the delegation link so the ordinary (non-delegated) path is byte-identical:
- plugin-security middleware — the delegator's effective permission sets are reconstructed once (fail-CLOSED if the delegator no longer exists — a dangling link is denied, not resolved to the additive baseline) and AND-composed into: the required-capability gate, object CRUD, field-level security (read mask + write forbid + predicate-oracle guard), the row-level
usingpre-image on by-id writes, thecheckpost-image, and the RLS read-filter injection. View/Modify-All only survives when BOTH principals hold it. - plugin-sharing middleware — the OWD/record-sharing owner-match is IDENTITY-scoped, so it re-runs the visibility filter (and
canEdit) under the delegator's own identity + depth and AND-s it in. An agent with View-All acting on behalf of a plain member therefore sees exactly that member's own rows — not everyone's, and not nothing. - explain engine — every layer reports the narrower verdict when
onBehalfOfis set, so the D6 access explanation stays truthful for delegated principals; a dangling delegator is reported as a fail-closed deny.
First-cut scope (documented in code + covered by tests): one delegation hop (the
onBehalfOfshape carries a single delegator, and any single-hop intersection is a safe lower bound on a true multi-hop chain); tenant-scoped substitution bags (tenantId,org_user_ids,email) are inherited from the live principal, while person-specific membership bags left unresolved narrow rather than widen. The agent grant-ceiling lint (D10 rule 2) is a follow-up — the runtime intersection already caps the agent regardless of what its own sets carry, and a lint needs an agent-set designation convention that does not yet exist. - plugin-security middleware — the delegator's effective permission sets are reconstructed once (fail-CLOSED if the delegator no longer exists — a dangling link is denied, not resolved to the additive baseline) and AND-composed into: the required-capability gate, object CRUD, field-level security (read mask + write forbid + predicate-oracle guard), the row-level
-
d79ca07: ADR-0090 D10 — activate the agent principal (OAuth →
principalKind:'agent'+ scope-derived ceiling). This wires the producer side of the D10 intersection that shipped in #2838, so it stops being dormant: an MCP request authenticated with an OAuth access token is now resolved as an AI agent acting on behalf of the humansub, and its effective permission is the intersection of a scope-derived capability ceiling AND the user's own grants.resolve-execution-context(producer): when a verified MCP OAuth token names an authorized client (azp), the request resolves toprincipalKind:'agent'withonBehalfOf:{ userId }(the human), and the agent's OWN grants are replaced by the scope-derived ceiling —data:read→ read-only,data:write→ full CRUD, neither → no data access.userIdstays the human so owner-stamping andcurrent_user.*RLS resolve to them; the user-derivedsystemPermissionsare cleared so a cap-gated action can't ride the user's capabilities. A token without a client stays ahumanprincipal.plugin-security: three built-in ceiling sets (mcp_agent_data_read/mcp_agent_data_write/mcp_agent_restricted) — pure CRUD bits, no row-level security (all row/owner/tenant narrowing comes from the delegating user on the other side of the intersection). Anagentprincipal skips the additive human baseline (member_default) — its grants are exactly its ceiling — and its fallback is the restricted (no-object-access) set, so a mis-resolved agent fails CLOSED, never open.spec:MCP_AGENT_PERMISSION_SET_*names +scopesToAgentPermissionSets(), single-sourced next to the OAuth scope constants.
Behaviour change (a security tightening). Previously an MCP OAuth request executed with the FULL authority of the logged-in user, and scopes narrowed only the tool surface. Now the scope is also a real data-layer ceiling: a
data:readtoken can never write ANY record, even via a crafted call, no matter what the user could do. This is strictly consistent with the existing contract that "a scope can never grant more than the user could do" — the intersection only ever narrows — and closes the gap where a compromised or confused agent could act with the user's full reach.Verified end-to-end: a
data:readagent acting for a member who owns a record can read it but cannot edit or create; adata:writeagent for the same user can. Producer mapping unit-tested in@objectstack/runtime; enforcement dogfooded against the served engine (showcase-agent-scope-ceiling).
-
c044f08: Security fix (Critical): the settings HTTP routes no longer trust spoofable identity headers, and writes are now capability-gated.
Previously
GET/PUT/POST /api/settings/*derived the caller's identity fromx-user-id/x-tenant-id/x-permissionsrequest headers (the route default), andsetManyperformed no permission check — so on a standardos serve --serverdeployment (settings + HTTP server composed by default, routes registered on the raw app with no auth middleware) an unauthenticated remote client could write tenant- or platform-scoped settings (including the auth security-policy, localization, and company manifests) and enumerate every namespace.Fixes:
- Verified identity.
SettingsServicePluginnow derives the caller's identity and capabilities from the platform's verified resolution (resolveAuthzContext— session cookie / API key / OAuth), never from request headers. The route default is now SECURE: it trusts no identity header and yields an anonymous, denied context. - Capability gates. Manifest
readPermission/writePermissionare enforced for HTTP callers: reads of a protected namespace, writes, and actions require the declared capability (writes default to at least the read capability, never ungated). Enforced via a newenforcedflag set only at the HTTP boundary — in-process/boot callers (kernel.getService('settings'), seed) are unchanged and keep full trusted access. - Unauthenticated HTTP callers can no longer enumerate protected manifests or write; a
403 SETTINGS_FORBIDDENis returned when the capability is missing.
setup.writecapability now real. Enforcing the manifests' declaredwritePermissionsurfaced a modeling gap:setup.write(the write counterpart tosetup.access, used by the branding / company / localization / feature-flag manifests) was referenced but never declared or granted — so under enforcement nobody, not even an admin, could write those namespaces. It is now a declared platform capability (PLATFORM_CAPABILITIES) held byadmin_full_accessandorganization_admin, alongsidesetup.access.Behaviour change: a deployment that relied on the old header-trusted default must present a real verified session/API-key/OAuth credential (which the console already does). A custom integration may still inject its own
contextFromRequest.Found by an adversarial security review of the request→ExecutionContext trust boundary.
- Verified identity.
-
Updated dependencies [526805e]
-
Updated dependencies [d79ca07]
-
Updated dependencies [33ebd34]
-
Updated dependencies [c044f08]
-
Updated dependencies [01274eb]
-
Updated dependencies [8f23746]
-
Updated dependencies [b97af7e]
-
Updated dependencies [6da03ee]
- @objectstack/spec@14.5.0
- @objectstack/platform-objects@14.5.0
- @objectstack/core@14.5.0
- @objectstack/formula@14.5.0
-
82e745e: ADR-0091 L1 — grant validity windows: effective-dated assignments, resolution-time filtering, explain expired state, authoring lint.
- plugin-security (objects):
sys_user_positionandsys_user_permission_setgain the D1 lifecycle columns —valid_from,valid_until(half-open[from, until), UTC; null = unbounded, existing rows unchanged),reason,delegated_from,last_certified_at,certified_by. - core: new shared predicate
isGrantActive/isGrantExpired(@objectstack/core), andresolveAuthzContextnow filters BOTH grant tables through it (D2, fail-closed — an expired unscopedadmin_full_accessgrant no longer derivesplatform_admin). Present-but-unparseable bounds fail closed. - plugin-security (explain):
buildContextForUserapplies the same filter and returnsexpiredGrants; the principal layer reports the dedicated "held until … — expired" contributor state so "why did access disappear" is self-answering. SpecExplainLayerSchemacontributors gain an optionalstate: 'active' | 'expired'. - plugin-sharing:
PositionGraphService.expandPositionUsersfilters expired holders — sharing-rule recipients stop including them at resolution time. - lint (D7): two new error rules over seed data —
security-grant-expired-at-authoring(avalid_untilin the past, or unparseable, is a grant that can never resolve) andsecurity-delegation-missing-reason(adelegated_fromrow withoutreasonbreaks the D3 dual audit). Also re-exported the missingSECURITY_MASTER_DETAIL_UNGRANTEDconstant.
No background job is involved anywhere — per ADR-0049, an expired grant simply stops resolving, in every edition.
- plugin-security (objects):
-
f3035bd: ADR-0091 L2 — delegation of duty (职务代理): self-service, time-boxed position delegation without administration.
- spec:
PositionSchema.delegatable(default false) + thesys_position.delegatablefield. A position opts in to being self-service delegated. - plugin-security (D12 gate): a new self-service branch — a non-admin holder of a
delegatableposition may insert asys_user_positionrow assigning it to a delegate, WITHOUT anyadminScope, iff the row is a well-formed delegation:delegated_from= the writer (you delegate your OWN authority), a mandatoryvalid_untilin the future and within the 30-day ceiling, a mandatoryreason, and the writer holds the position directly (validity-filtered — a grant that itself arrived via delegation is not re-delegatable). Insert-only, so a delegation is not self-renewable. Adelegatableposition that distributes anadminScope-carrying set is rejected fail-closed — administration is never self-delegated (D12 containment). Dual audit:granted_by(writer) +delegated_from(authority source). - plugin-security (explain):
buildContextForUsersurfaces delegation provenance; the principal layer attributes a delegated position "via delegation from X, until Y". - liveness / proof (ADR-0054):
position.delegatableis a bound high-risk class with an end-to-end dogfood proof (delegation-of-duty) — a gated delegation write over the real HTTP API, then the delegate's grant resolving in-window and dying atvalid_untilvia the real resolver.
Break-glass activation and recertification campaigns stay enterprise (D7); their community shapes are the L1 substrate.
- spec:
- Updated dependencies [7953832]
- Updated dependencies [82e745e]
- Updated dependencies [f3035bd]
- Updated dependencies [82c0d94]
- Updated dependencies [7449476]
- @objectstack/spec@14.4.0
- @objectstack/platform-objects@14.4.0
- @objectstack/core@14.4.0
- @objectstack/formula@14.4.0
-
02f6af4: ADR-0090 follow-through wave: enforce book audience at the read layer; finish the D2/D3 cleanup the P1 rename missed.
- rest:
/meta/book,/meta/doc, and/meta/book/:name/treenow ENFORCE the ADR-0046 §6.7 audience model (ADR-0049 — no unenforced security properties): anonymous callers see onlypublicbooks/docs;{ permissionSet }-gated books require the caller to hold the named set; a doc's effective audience is the union over the books that CLAIM it (unclaimed docs default toorg; orphan rendering never inheritspublic). Gated evaluation fails CLOSED when holdings cannot be resolved.doc/booksingle-item reads bypass the shared meta cache (per-caller gate vs shared ETag). - spec: new pure helpers powering that gate —
audienceAllows,resolveDocAudiences,docAudienceAllows,resolveBookClaimedDocs(+AudienceCaller/AudienceBooktypes). BREAKING but ships as aminorper the launch-window convention (pre-1.0 semantics — breaking changes do not burn a major version number while the whole stack is in lockstep):METADATA_FORM_REGISTRYkeysrole/profileare gone —positionis the registered form (thepositiontype had LOST its form layout in the P1 rename);EnvironmentArtifactMetadataSchemadeclarespositionsinstead of retiredroles/profiles. - plugin-security: the
securityservice exposesresolvePermissionSetNames(ctx)— the same resolution as data-plane enforcement, for the docs gate. - metadata: artifact ingestion maps
positions → 'position'(the staleroles → 'role'mapping matched nothing since the P1 rename, silently dropping compiled positions from metadata registration). - lint: books join the D3 role-word scan (their
audienceis a permission-model reference now), and a new advisory rulesecurity-book-audience-unknown-setflags a{ permissionSet }audience naming a set the stack does not declare (runtime fails closed — the typo cost is "nobody can read the book", so say it at author time). - platform-objects: metadata-form translations regain
position(all four locales) and drop the retiredrole/profilegroups, with a vocabulary regression test.
- rest:
-
8f0b9df: fix(cli,plugin-security):
os meta resyncto re-materialize default permission sets from dist (#2705)The default permission sets (
admin_full_access/member_default/viewer_readonly…) were seeded insert-once at boot:bootstrapPlatformAdminskipped any row that already existed and never wrote the shipped declaration back. So editing a default set's source, recompiling, and restartingos devwithout--freshleft the runtime serving the OLD value — silently, because the runtime authz resolver hydrates permission sets from thesys_permission_setrow (resolve-authz-context.ts), not from the in-memory dist. A permission-gated surface (e.g.setup.access) would keep its stale behavior with no error, which repeatedly misled debugging. Every other metadata seed (declared permission sets, positions, built-in roles, capabilities) already upserts on boot, leaving the platform-default path the lone insert-once holdout — a gap ADR-0090 widened by persisting more facets (system_permissions, delegated-adminadmin_scope) onto the same row.The insert-once posture is deliberate for prod (it protects an admin's Setup edits and keeps the defaults env-authored — the exact posture
bootstrapDeclaredPermissionsrelies on), so this is not switched to a blind upsert. Instead:bootstrapPlatformAdmingains aresyncoption. Default boot behavior is unchanged (insert-once). Underresync, an existing row is reconciled to the shipped dist only when the platform still owns it (managed_byabsent or'platform'); a row an admin took over (managed_by:'user') or a package owns ('package') is an intentional override and is left untouched.- New
os meta resynccommand boots the runtime, reconciles the default permission-set rows to the compiled dist, and reports what was reconciled / preserved / newly seeded — without touching business data and without a--freshwipe. Gated behind a confirmation prompt (--yesto skip;--jsonfor scripting).
Prod boot is unaffected; the fix is entirely opt-in via the new command.
- Updated dependencies [2a71f48]
- Updated dependencies [02f6af4]
- Updated dependencies [c1064f1]
- @objectstack/platform-objects@14.3.0
- @objectstack/spec@14.3.0
- @objectstack/core@14.3.0
- @objectstack/formula@14.3.0
-
ac8f029: Two ADR-0090 D5 closures (#2752, #2753):
GET /me/appssources the engine registry. Stack apps are registered into the engine registry (runtime AppPlugin), not the metadata service —metadata.list('app')returned[]for every principal, leavingtabPermissionsandAppSchema.requiredPermissionswith no enforced consumer. The endpoint now readsregistry.getAllApps()(same authority as the meta routes, nav contributions merged) with the metadata service as an additive fallback; the capability and tab filters are unchanged and now actually run.The default baseline binds to the
everyoneanchor.member_defaultcarriedallowDeleteon its'*'grant — an anchor-forbidden bit — so bootstrap refused theeveryonebinding on every boot and the baseline flowed only through the separate fallback channel D5 explicitly rejected. Two aligned changes:describeHighPrivilegeBits(spec) is calibrated to the exact ADR-0090 D5 bit list (VAMA, delete/purge/transfer, systemPermissions). A plain'*'wildcard is no longer high-privilege by itself; the wildcard ban moves to the GUEST tier where D9 specifies it (describeAnchorForbiddenBits).member_defaultdropsallowDeletefrom the wildcard. Behavior change: deleting records is no longer a baseline right — members keep create/read/edit-own; domains that want member deletes grant them per object via an ordinary position-distributed set. The owner-scoped delete RLS stays as a narrowing defense for members who receive a delete bit elsewhere.
With the baseline anchor-safe, bootstrap's existing binding path succeeds: "what new users get" is now literally "what is bound to
everyone" — same table, same audit, same explain path (proven by the newme-apps-and-everyone-baselinedogfood). -
4ab9958: Position assignment panels as pure SDUI (ADR-0090 follow-through).
RecordRelatedListPropsgainsrelationshipValueField(default'id'): which parent-record field the junction'srelationshipFieldstores — the generic affordance for name-keyed junctions (sys_user_position.positionstoressys_position.name). Used for both the list filter and the Add-picker's parent-side value.sys_userdetail page gains a Positions tab (assign positions to a user; Add picker stores the position machine name viavalueField: 'name'; the D12 delegated-admin gate's denials surface in the dialog).- New
sys_positiondetail page (shipped by plugin-security): Holders (name-keyed viarelationshipValueField: 'name') and Permission Sets (bindings) tabs — zero bespoke UI; ADR-0091 validity columns slot in later as plain column additions.
Renderer note: the generic
record:related_listAdd-picker andrelationshipValueFieldsupport land in objectui alongside the ^14 alignment; with older renderers these tabs degrade to read-only lists.
- Updated dependencies [ac8f029]
- Updated dependencies [4ab9958]
- @objectstack/spec@14.2.0
- @objectstack/platform-objects@14.2.0
- @objectstack/core@14.2.0
- @objectstack/formula@14.2.0
- Updated dependencies [5a8465f]
- Updated dependencies [7f8620b]
- Updated dependencies [82ba3a6]
- @objectstack/spec@14.1.0
- @objectstack/core@14.1.0
- @objectstack/formula@14.1.0
- @objectstack/platform-objects@14.1.0
-
ac08698: ADR-0090 D6 — the explain engine gets its REST face (#2696).
@objectstack/rest: newGET/POST /api/v1/security/explain(object/operation/userId, validated against the spec'sExplainRequestSchema) delegating to thesecurityservice'sexplain(request, callerContext)— the same code paths the enforcement middleware runs, so the returnedExplainDecisionis explained by construction. The route is authenticated-only (401 even onrequireAuth=falsedeployments), returns 501 when no security service exposesexplain, and maps the service'sPermissionDeniedErrorto 403. Registered on scoped (/environments/:environmentId) and unscoped base paths; the env kernel's ownsecurityservice is preferred, with a new host-kernelsecurityServiceProviderfallback wired by the REST plugin.@objectstack/plugin-security:explainAccessForCallernow honors delegated administration (D12) — explaining ANOTHER user is authorized bymanage_usersor a delegatedadminScopewhose business-unit subtree covers the target user (newDelegatedAdminGate.scopesCoverUser, fail-closed on unresolvable scopes/memberships). Self-explain still needs neither. -
bd39dc5: ADR-0090 D5/D9 — suggested audience bindings become a queryable, confirmable surface.
A package permission set declaring
isDefault: trueis an install-time SUGGESTION to bind the set to the built-ineveryoneposition — never auto-bound. Until now the flag was only read at bootstrap as the fallback-set name; after an install there was no way to see or act on the suggestion.@objectstack/plugin-security: newsys_audience_binding_suggestionsystem object (read-only over the data API; unique per package × set × anchor) plus a convergent reconciler (syncAudienceBindingSuggestions) that reads every declaredisDefaultset — boot-declared stack metadata AND installed package manifests, so a runtimePOST /api/v1/packagesinstall is visible immediately — and keeps the table honest: undeclared → pending row pruned, bound out-of-band → markedconfirmed(observed). Thesecurityservice gainslistAudienceBindingSuggestions/confirmAudienceBindingSuggestion/dismissAudienceBindingSuggestion, all pre-gated on tenant-level admin (ADR-0066 superuser wildcard — anchors stay tenant-level only per D12). Confirm writes thesys_position_permission_setrow with the caller's execution context, so the D5/D9 audience-anchor gate (no high-privilege set oneveryone/guest) and the D12 delegated-admin gate enforce the binding; a set not yet materialized (installed this session) is first seeded through the same provenance-checked upsert as the boot seeder (ADR-0086 D4).@objectstack/restand@objectstack/runtime: the HTTP surface, registered on both API layers (the RestServer thatobjectstack dev/hono serves, and the runtime HttpDispatcher used by the adapters) —GET /api/v1/security/suggested-bindings?status=&packageId=,POST /api/v1/security/suggested-bindings/:id/confirm,POST /api/v1/security/suggested-bindings/:id/dismiss(401 unauthenticated, 403/404/409 mapped from the service's typed errors, 501/503 without plugin-security). -
1056c5f: Package uninstall now revokes the package's data-plane permission rows (#2747, ADR-0086 D3 / ADR-0090 D5 "no ghost grants").
@objectstack/metadata-protocol:deletePackagegains an uninstall-cleanup seam — the exact mirror of the publish materializer: domain plugins register named cleanups viaregisterUninstallCleanup(name, fn)and every cleanup runs with the uninstalled package id, its outcome reported on the newcleanupsarray of the response (a failed revocation is visible, never silent).deletePackagealso unregisters the package from the in-memory SchemaRegistry (best-effort), so the running kernel stops serving it without waiting for a restart.@objectstack/plugin-security: registers thesecurity.package-permissionscleanup — deletes the package's ownsys_permission_setrows (managed_by: 'package'+ matchingpackage_idonly; env-authored and foreign-package rows are never touched, ADR-0086 D4), theirsys_position_permission_set/sys_user_permission_setbindings (bindings first, so no dangling grants), and the package'ssys_audience_binding_suggestionrows (a reinstall re-prompts fresh). Also fixes the engine-call signature in the suggestion module:find/deletereadcontextfrom their second argument — the previous trailing{ context }argument was ignored, so deletes ran principal-less.@objectstack/rest:DELETE /api/v1/packages/:id(no version pin) now goes throughprotocol.deletePackage— one uninstall semantic instead of a baresys_packagesrow delete — removing the package's metadata, durable record, registry entry, and running the cleanups; the response carriesdeletedCount+cleanups. A version-scoped delete keeps the narrow durable-registry semantics.
-
0a8e685: ADR-0090 permission-model zoo + docs alignment.
Showcase (
@objectstack/example-showcase) now exercises the full Permission Model v2 authoring surface and is guarded by a new runtime dogfood test (showcase-permission-zoo.dogfood.test.ts): typeddefinePosition/definePermissionSet/defineSharingRulefactories; six flat positions (the stale pre-D3parentfields are gone); permission sets covering CRUD+FLS+RLS, org-depth read/write asymmetry (readScope: 'org'/writeScope: 'own'), View-All (auditor) and Modify-All (ops) bypasses,systemPermissions(setup.access), theisDefaulteveryone-suggestion (incl. personal-data grants on theprivate-OWD note object), a guest-safe set for theguestanchor (D9), and a delegated-administrationadminScopebounded to a seededsys_business_unitsubtree (D12). Objects gainexternalSharingModeldials (D11). A committedaccess-matrix.jsonopts the showcase into the D6 snapshot gate. Hierarchy depths (own_and_reports/unit/unit_and_below) are deliberately NOT authored — they are enterprise (hierarchy-security) and the open runtime fails closed; BU-shaped visibility is demonstrated via the enforcedunit_and_subordinatessharing-rule recipient instead.@objectstack/spec:defineStackstrict cross-reference validation no longer rejects permission grants or seed datasets that target platform-provided objects (sys_/cloud_/ai_prefixes) — a delegated-admin set carrying CRUD on the RBAC link tables (ADR-0090 D12) and an app seeding the business-unit tree are legitimate shapes; the typo net stays intact for the stack's own objects. Stale pre-ADR-0090 vocabulary in zod docstrings (rls/territory/ sharing/tool/agent) is rewritten; the auto-generated references (including the previously missingsecurity/explain.mdx) are regenerated.Docs:
protocol/objectql/security.mdxrewritten to the v2 model (no profiles, positions, canonical OWD four + D1 private default +externalSharingModel, position-scoped RLS, enforced sharing recipients);isProfilescrubbed from every authoring example; the dead/docs/references/identity/rolelink fixed; implementation-status and plugin READMEs aligned. Remaining rename misses are tracked in #2722 (RLSUserContext.role), #2723 (portalprofiles), #2724 (sys_record_shareroleenum). -
d0531c4: Setup → Access Control nav: the
sys_positionentry is renamednav_roles/"Roles" →nav_positions/"Positions" (岗位 / ポジション / Posiciones) — the last "role" leftover in platform UI copy (ADR-0090 D3; the Studio-side relabel already landed in objectui). The framework's.objectui-shapin is bumped to pick up the Studio Access-pillar explain panel ("why can this user access?", ADR-0090 D6) and the suggested audience-binding install prompt (D5/D9). -
cff5aac: Setup navigation: the Access Control menu entry for
sys_positionis now labeled "Positions" (was still "Roles" after the ADR-0090 D3 rename) —nav_roles→nav_positions, with zh-CN 岗位 / ja-JP ポジション / es-ES Posiciones translations updated to match the position vocabulary. -
Updated dependencies [0a8e685]
-
Updated dependencies [afa8115]
-
Updated dependencies [80f12ca]
-
Updated dependencies [332b711]
-
Updated dependencies [e2fa074]
-
Updated dependencies [23c8668]
-
Updated dependencies [29f017d]
-
Updated dependencies [216fa9a]
-
Updated dependencies [6c22b12]
-
Updated dependencies [d0531c4]
-
Updated dependencies [cff5aac]
- @objectstack/spec@14.0.0
- @objectstack/platform-objects@14.0.0
- @objectstack/core@14.0.0
- @objectstack/formula@14.0.0
-
6d83431: ADR-0090 P1 breaking wave — permission model v2 concept convergence.
Pre-launch one-step renames and secure defaults (no compatibility aliases, per ADR-0090 D3/D4 superseding ADR-0057 D5/D7's alias discipline):
sys_role→sys_position,sys_user_role→sys_user_position(fieldrole→position),sys_role_permission_set→sys_position_permission_set(fieldrole_id→position_id);RoleSchema/defineRole→PositionSchema/definePositionwith noparent(positions are flat; hierarchy lives on the business-unit tree).ExecutionContext.roles[]→positions[]; the EvalUser/CEL contractcurrent_user.roles→current_user.positions(formula validators updated); stack propertyroles:→positions:; metadata kindsrole/profile→position(profile kind removed).isProfileremoved fromPermissionSetSchema(ADR-0090 D2);isDefaultnarrows to an install-time suggestion;appDefaultProfileName→appDefaultPermissionSetName(isDefault-only).- OWD enum drops legacy aliases
read/read_write/full; new optionalexternalSharingModel(external dial,privatedefault) lands as P1 spec shape (ADR-0090 D11). - Secure default (D1): a custom object with an owner field and NO
sharingModelnow resolvesprivate(was: fully public). System objects keep their explicit posture. Unrecognised stored values fail closed. - ExecutionContext gains the P1 principal-taxonomy shape (D10):
principalKind/audience/onBehalfOf(optional, semantics phase in later). - Sharing recipients:
role→position(expanded viasys_user_position∪ the better-auth membership transition source);role_and_subordinatesremoved —unit_and_subordinatesnow expands the business-unit subtree (finishes ADR-0057 D5's re-homing).
-
b271691: ADR-0090 P3 — security-domain publish linter (D7) and delegated administration (D12).
D7 —
validateSecurityPosture(@objectstack/lint), wired intoos compile(errors gate the build) andos lint. Rules, each with a failing fixture:security-owd-unset(custom object with nosharingModel— the objectui#2348 leave_request shape),security-owd-alias(retired D4 alias values, with fix-it),security-external-wider-than-internal(D11external ≤ internal),security-wildcard-vama('*'+ View/Modify All outside the platform admin set, ADR-0066),security-anchor-high-privilege(anisDefault/everyone-suggested set carrying anchor-forbidden bits),security-role-word(D3 vocabulary freeze in security identifiers/labels; ARIA/page roles exempt), and advisorysecurity-private-no-readscope.D12 — delegated administration (@objectstack/plugin-security
DelegatedAdminGate).PermissionSetSchema.adminScope(new in spec, persisted assys_permission_set.admin_scope) declares WHERE (asys_business_unitsubtree), WHAT (manageAssignments/manageBindings/authorEnvironmentSets), and WHICH sets a delegate may hand out (assignablePermissionSetsallowlist). Writes tosys_user_position,sys_position_permission_set,sys_user_permission_set, andsys_permission_setare now governed: tenant-level admins (ADR-0066 superuser wildcard) pass through; delegates need a covering scope — inside their subtree, allowlisted sets only (to others AND themselves), single-row writes,granted_byaudit-stamped; everyone else (including holders of plain CRUD on RBAC tables) is denied. Granting or authoring a set that itself carries anadminScoperequires a held scope that STRICTLY contains it. Theeveryone/guestanchors stay tenant-level only, and direct position assignments to an anchor are rejected for every caller.ADR-0090 Addendum — assignment-level BU anchor.
sys_user_position.business_unit_idlands with its three consumers scoped: D12 delegation boundary (enforced here), audit fact, and the depth-anchor contract for enterprisehierarchy-scope-resolverimplementations (documented onIHierarchyScopeResolver).D9 tier tightening.
describeHighPrivilegeBitsmoved to@objectstack/spec/security(re-exported from plugin-security) alongside newdescribeAnchorForbiddenBits:guestbindings now additionally reject edit bits (read-only by default; create stays the case-by-case exception).BREAKING (@objectstack/plugin-security): exports renamed to the ADR-0090 D3 vocabulary —
SysRole→SysPosition,SysUserRole→SysUserPosition,SysRolePermissionSet→SysPositionPermissionSet(no aliases, pre-launch one-step rename).sys_positionrow actions/list views renamed (activate_position, …), labels relabeled Role→Position. Non-tenant-admin writes to the RBAC link tables without anadminScopeare now denied (previously any CRUD grant on those tables sufficed).BREAKING (@objectstack/platform-objects):
sys_business_unit_member.role_in_business_unit→function_in_business_unit(D3 reserved-word sweep; values member/lead/deputy unchanged).
-
01917c2: ADR-0090 P2 — audience anchors:
everyone/guestbuiltin positions.EVERYONE_POSITION/GUEST_POSITIONconstants in@objectstack/spec; both anchors seeded (system-managed) alongside the builtin identity names.- Every authenticated principal implicitly holds
everyoneinctx.positions, so sets bound to it resolve as ordinary position-bound grants — ADDITIVE. The fallback CLIFF is abolished: the configured baseline (fallbackPermissionSet, defaultmember_default) now applies in addition to explicit grants instead of only when the user had none, and is also seeded as aneveryonebinding (same table/audit/explain path as admin-authored defaults). - Sessionless HTTP principals resolve as
principalKind: 'guest'holding exactly['guest']; internal bare contexts are untouched. - Audience-anchor binding gate:
sys_position_permission_setwrites that would bind a high-privilege set (VAMA, delete/purge/transfer, system permissions,'*'wildcard) toeveryone/guestare rejected at the data layer, unconditionally (describeHighPrivilegeBitspredicate is exported and shared with the seed-time validation).
-
a5a1e41: ADR-0090 P4 — explain engine (D6), access-matrix snapshot gate, recalibrated benchmark.
Explain contract (@objectstack/spec).
ExplainRequestSchema/ExplainDecisionSchema/ExplainLayerSchema:explain(principal, object, operation)reports the verdict of every evaluation-pipeline layer in order (principal → required_permissions → object_crud → fls → owd_baseline → depth → sharing → vama_bypass → rls), with per-layer contributor attribution (which permission set, reached via which position/baseline) and — for reads — the composed row filter as the machine artifact. Carries the D10 dual attribution (principalKind,onBehalfOf).Explain engine (@objectstack/plugin-security).
explainAccessis "explained by construction": it calls the SAME permission-set resolution, evaluator, FLS mask, and RLS composition the enforcement middleware calls (injected fromSecurityPlugin), so the report cannot drift from enforcement. Exposed on thesecuritykernel service asexplain(request, callerContext); explaining another user requiresmanage_users(the target's context is reconstructed fromsys_user_position/sys_user_permission_setwith everyone-anchor semantics viabuildContextForUser).Access-matrix snapshot gate (@objectstack/lint + os compile).
buildAccessMatrix(stack)derives the (permission set × object) capability matrix purely from metadata;diffAccessMatrixrenders semantic review lines ("'crm_admin' gains delete on 'crm_lead'", depth changes, OWD swings, entry add/remove).os compilegains an opt-in gate: withaccess-matrix.jsoncommitted next to the config, any drift fails the build with those lines until re-snapshotted via--update-access-matrix— every capability change becomes a reviewable diff. Seeded forexamples/app-crm.Benchmark (ADR-0090 Addendum).
scripts/bench/permission-bench.mts— single-org 10k users × 1M rows per the recalibrated topology; asserts the O()-shape property (per-request cost independent of user population; unit-depth IN-set cost tracks unit size). Passing at 0.1µs/eval and 59ms/1M-row IN-set scan. -
466adf6: Per-operation object
requiredPermissions(ADR-0066 ⑤) — an object can now be read-open / write-gated instead of gating all of CRUD on one capability set.Object.requiredPermissionsaccepts either the originalstring[](capabilities required for all operations) or a{ read?, create?, update?, delete? }map that gates each operation class independently — mirroring how Salesforce and Dataverse separate capability by operation. plugin-security enforces the caps for the request's operation class as the same D3 AND-gate (checked before the CRUD grant, fail-closed). The mapping foldstransfer/restoreintoupdateandpurgeintodelete, derived from the existing CRUD permission bits so it stays in lockstep with them.Backward-compatible: the
string[]form keeps its gate-every-operation semantics (normalized into anallbucket that unions with the per-operation bucket), so existing objects are unaffected. The per-operation map's keys are validated.strict(), so a mistyped key (e.g.reads) is rejected at author time rather than silently ignored.
-
466adf6: Author-time capability-reference lint (ADR-0066 ⑨) —
os validate/os lintnow warn when arequiredPermissionsnames a capability that is registered nowhere.requiredPermissions(on objects, fields, apps, actions) is a free string, so a typo likemange_usersis schema-valid and fails closed at runtime (the caller is denied) — safe, but silent. The newvalidateCapabilityReferencesrule (@objectstack/lint) resolves every reference against the author-time known set and warns on the unresolved ones:- built-in platform capabilities — now sourced from a single canonical list in
@objectstack/spec(security/capabilities.ts:PLATFORM_CAPABILITIES/PLATFORM_CAPABILITY_NAMES), which@objectstack/plugin-security'sbootstrapSystemCapabilitiesalso seeds from (one source of truth, no drift), - any capability a permission set in the stack grants via
systemPermissions(granting is what declares it — mirrors the runtime derived-defaults rule), and - any
sys_capabilityrow shipped as seed data.
It is a warning, not an error: a single package can't see capabilities declared by other installed packages, and the reference fails closed anyway.
systemPermissionsitself is never flagged — it is the declaration side, and a package legitimately introduces new capabilities there. The object case also understands the per-operationrequiredPermissionsmap form (ADR-0066 ⑤) and points a finding at the exact operation slice. - built-in platform capabilities — now sourced from a single canonical list in
-
799b285: Fix field-level-security read leak on mutation responses. The security middleware only masked read-protected fields on
find/findOneresults, so a caller with edit-but-not-field-read couldinsert/updatea record and read a read-protected field back out of the echoed post-image (field WRITES were already blocked, but the response image was not masked). The mask now also coversinsert/updateresults, matching read behavior. -
Updated dependencies [6d83431]
-
Updated dependencies [01917c2]
-
Updated dependencies [b271691]
-
Updated dependencies [a5a1e41]
-
Updated dependencies [466adf6]
-
Updated dependencies [5be00c3]
-
Updated dependencies [466adf6]
-
Updated dependencies [2bee609]
-
Updated dependencies [9fa84f9]
-
Updated dependencies [fc7e7f7]
- @objectstack/spec@13.0.0
- @objectstack/core@13.0.0
- @objectstack/formula@13.0.0
- @objectstack/platform-objects@13.0.0
- Updated dependencies [6cebf22]
- Updated dependencies [21420d9]
- @objectstack/spec@12.6.0
- @objectstack/core@12.6.0
- @objectstack/formula@12.6.0
- @objectstack/platform-objects@12.6.0
- Updated dependencies [8b3d363]
- @objectstack/spec@12.5.0
- @objectstack/core@12.5.0
- @objectstack/formula@12.5.0
- @objectstack/platform-objects@12.5.0
- Updated dependencies [60dc3ba]
- @objectstack/spec@12.4.0
- @objectstack/core@12.4.0
- @objectstack/formula@12.4.0
- @objectstack/platform-objects@12.4.0
- Updated dependencies [e7eceec]
- @objectstack/spec@12.3.0
- @objectstack/core@12.3.0
- @objectstack/formula@12.3.0
- @objectstack/platform-objects@12.3.0
-
3962023: feat(spec,security): make ambiguous nav landings unrepresentable + close the field-permission filter oracle (objectui#2251, objectui ADR-0055).
spec —
ObjectNavItemtarget exclusivity.NavigationItemSchemanow rejects an object nav item that combinesfilterswithrecordIdorviewName(custom issue onfilterswith the fix in the message). Runtime precedence would silently ignore the extras — a stalerecordIdhijacking a configuredfiltersslice — so the ambiguous combination is now unwritable (ADR-0053 correct-by-construction). FROM{ filters, viewName }/{ filters, recordId }TO exactly one landing field; the legacyrecordId+viewNamecombination stays tolerated (documented:viewNameis ignored).filtersshipped in the same unreleased minor, so no released metadata is affected.plugin-security — field-level predicate guard.
FieldMaskerstrips non-readable fields from RESULTS, but predicates still leaked their values: filtering / sorting / grouping / aggregating by a hidden field changes row presence (a filter oracle — probesalary >= Xeven though the column is masked). The security middleware now rejects (403PermissionDeniedError,reason: 'field_predicate_denied') any caller query whosewhere/orderBy/groupBy/having/aggregations/windowFunctionsreference a field the caller cannot read — evaluated against the caller's AST before RLS injection, so RLS policies may keep referencing hidden fields (e.g.owner_id). Rejection over silent predicate dropping: removing an$andbranch widens results and re-opens the oracle. New exports:assertReadableQueryFields,collectQueryFields,collectConditionFields.
- Updated dependencies [fce8ff4]
- Updated dependencies [3962023]
- Updated dependencies [2bb193d]
- Updated dependencies [0426d27]
- Updated dependencies [da807f7]
- Updated dependencies [4f5b791]
- @objectstack/spec@12.2.0
- @objectstack/core@12.2.0
- @objectstack/formula@12.2.0
- @objectstack/platform-objects@12.2.0
- Updated dependencies [93e6d02]
- @objectstack/spec@12.1.0
- @objectstack/core@12.1.0
- @objectstack/formula@12.1.0
- @objectstack/platform-objects@12.1.0
-
9796e7c: feat(security): two-doors separation for permission sets (ADR-0086 P2)
Splits who may change a permission set into two non-overlapping doors, enforced at the data layer instead of by convention:
块 1 — the package door (publish-time materialization).
ObjectStackProtocolImplementationgains a generic publish-time materializer registry (registerPublishMaterializer(type, fn)). When a draft of a registered type is published, its body is projected into a data-plane row and the result is surfaced on the publish response asmaterializeApplied(best-effort, never thrown — same contract asseedApplied).promoteDraftnow returns the draft'spackageIdso the materializer can stamp the owning package.plugin-securityregisters apermissionmaterializer that upserts the published set intosys_permission_setwithmanaged_by:'package'+package_id— so a set authored through the studio package door (saved as apermissiondraft, then published) lands in the admin surface with the exact provenance the boot seeder already stamps, now on the runtime publish path too. The single-set upsert is shared withbootstrapDeclaredPermissions(upsertPackagePermissionSet), so both paths apply the same own-row / foreign-package / env-authored rules.块 2 — the admin door (data-layer write gate). The security middleware now refuses any admin-door write (
update/delete/transfer/restore/purge) to asys_permission_setrow withmanaged_by:'package', and refuses aninsertthat forgesmanaged_by:'package'. The gate fails closed regardless of the caller's grants (a platform admin withmodifyAllRecordsis blocked just the same), so it is a real data-layer boundary rather than a UI hint. System/boot writes carryisSystemand bypass the whole middleware, so the boot seeder and the publish materializer are unaffected. Env-authored sets (managed_byuser/platformor absent) stay freely editable through the admin door — the two doors never overwrite each other. -
7c09621: feat(security): pre-map
transfer/restore/purgeto their RBAC bits (#1883)The permission evaluator now maps the destructive record-lifecycle operations to their spec permission bits (
transfer→allowTransfer,restore→allowRestore,purge→allowPurge) and extends themodifyAllRecordssuper-user bypass to cover them. The ObjectQL operations themselves are still roadmap M2 — but the gate now exists ahead of them: the moment such an operation is dispatched through the security middleware it is denied unless a resolved permission set grants the matching bit. Unmapped destructive operations continue to fail closed (ADR-0049). Spec descriptions updated from[EXPERIMENTAL — not enforced]to[RBAC-gated; operation pending M2]. -
7709db4: feat(security): permission-set package provenance + declared-permission seeding (ADR-0086 P1)
Packages now ship working default access for their own objects, with a machine-checkable metadata↔config boundary:
- Spec (ADR-0086 D3):
PermissionSetSchema.packageId(owning package for a package-shipped set; absent = env-authored) and per-record provenancemanagedBy: 'package' | 'platform' | 'user'on the existing metadata-persistence axis. Persisted onsys_permission_setaspackage_id/managed_by(new columns +package_idindex). - Seeding (ADR-0086 D5): new
bootstrapDeclaredPermissions— the sibling ofbootstrapDeclaredRoles— materializesstack.permissionsintosys_permission_setat boot withmanaged_by:'package'+package_id. Idempotent and upgrade-aware: rows the seeder owns are re-seeded to the shipped declaration on every boot; rows owned by a different package are refused loudly; env-authoredplatform/user/legacy rows are never clobbered. Closes the ADR-0078 inert-metadata violation forstack.permissions(declared sets were runtime-enforced but never materialized — invisible to the admin surface, uninstall undefined). - Conformance matrix row
declarative-permission-seeding(ADR-0056 D10) + dogfood proof pin the behavior so it cannot regress to inert.
- Spec (ADR-0086 D3):
-
48ad533: fix(security): surface swallowed permission-set resolution failures (#2565)
PermissionEvaluator.resolvePermissionSetsswallowed metadatalist()andsys_permission_setdbLoader failures silently — fail-closed (unresolvable sets grant nothing), but a transient DB error made custom permission sets vanish with no trace, leaving the resulting 403s undiagnosable. The evaluator now accepts an optional{ logger }and emits onewarnper failed source, naming the unresolved permission sets and the error. SecurityPlugin wires its plugin logger into both call sites. Resolution behavior is byte-identical. -
Updated dependencies [a8df396]
-
Updated dependencies [e695fe0]
-
Updated dependencies [07f055c]
-
Updated dependencies [7c09621]
-
Updated dependencies [7709db4]
-
Updated dependencies [2082109]
-
Updated dependencies [7c09621]
-
Updated dependencies [9860de4]
-
Updated dependencies [069c205]
- @objectstack/spec@12.0.0
- @objectstack/platform-objects@12.0.0
- @objectstack/core@12.0.0
- @objectstack/formula@12.0.0
-
6a9397e: Retire the deprecated
compactLayoutalias forhighlightFields(framework#2536, closes the ADR-0085 deprecation window).ObjectSchemano longer declarescompactLayout:create()rejects it like any unknown key; lenientparse()strips it (no silent aliasing).- The parse-time alias AND the
highlightFields → compactLayoutback-fill transition mirror are removed fromnormalizeSemanticRoleAliases. Served metadata now carries the canonical key only. - All remaining first-party authors (27 system objects across plugin-audit / approvals / security / sharing / webhooks / service-storage / automation / messaging / realtime — missed by the #2521 sweep, caught by the type gate) renamed to
highlightFields. - The downstream smoke pin moves to hotcrm v1.2.2 (hotcrm#424: same rename + deps ^11.7.0).
- Consumers were switched in objectui#2168 and shipped via the console pin bump (#2526); this closes the window scheduled there. The dogfood mirror assertion (#2528) flips to
compactLayout: undefinedin this same change, per the plan it carried.
Version note: minor, not major — the key was deprecated-with-alias for a full release window, all first-party consumers/authors are migrated, and the spec api-surface gate reports no export changes (same documented-exception path as the ADR-0085 removals in 11.7.0). External metadata still authoring
compactLayoutwill now failcreate()loudly with the standard unknown-key error naming the key. -
Updated dependencies [6a9397e]
-
Updated dependencies [c0efe5d]
- @objectstack/spec@11.10.0
- @objectstack/core@11.10.0
- @objectstack/formula@11.10.0
- @objectstack/platform-objects@11.10.0
- Updated dependencies [d3595d9]
- @objectstack/spec@11.9.0
- @objectstack/core@11.9.0
- @objectstack/formula@11.9.0
- @objectstack/platform-objects@11.9.0
- Updated dependencies [53d491a]
- Updated dependencies [b84726b]
- @objectstack/platform-objects@11.8.0
- @objectstack/spec@11.8.0
- @objectstack/core@11.8.0
- @objectstack/formula@11.8.0
- Updated dependencies [5178906]
- @objectstack/spec@11.7.0
- @objectstack/platform-objects@11.7.0
- @objectstack/core@11.7.0
- @objectstack/formula@11.7.0
- @objectstack/spec@11.6.0
- @objectstack/core@11.6.0
- @objectstack/formula@11.6.0
- @objectstack/platform-objects@11.6.0
- Updated dependencies [6ee4f04]
- Updated dependencies [c1e3a65]
- @objectstack/spec@11.5.0
- @objectstack/core@11.5.0
- @objectstack/formula@11.5.0
- @objectstack/platform-objects@11.5.0
- Updated dependencies [5821c51]
- Updated dependencies [a0fce3f]
- @objectstack/spec@11.4.0
- @objectstack/core@11.4.0
- @objectstack/formula@11.4.0
- @objectstack/platform-objects@11.4.0
- Updated dependencies [58e8e31]
- Updated dependencies [b4a5df0]
- @objectstack/spec@11.3.0
- @objectstack/core@11.3.0
- @objectstack/formula@11.3.0
- @objectstack/platform-objects@11.3.0
- Updated dependencies [d0f4b13]
- Updated dependencies [302bdab]
- @objectstack/spec@11.2.0
- @objectstack/core@11.2.0
- @objectstack/formula@11.2.0
- @objectstack/platform-objects@11.2.0
-
574e7a3: Security: platform admins see all rows of better-auth-managed identity objects (ADR-0024 / cloud#551)
Identity tables managed by the auth library (
managedBy: 'better-auth'—sys_oauth_application,sys_account,sys_session,sys_sso_provider, …) are written by better-auth's own adapter with no tenant context, soorganization_idis never stamped andmember_default's wildcardtenant_isolationRLS denies every row — a platform admin's Setup list (OAuth Applications, Identity Links, …) renders empty.These objects now get the same posture-gated superuser bypass as
private/tenancy.enabled:falseobjects, so a platform admin'sviewAllRecordssees all identity rows env-wide. This is admin-only: non-admins never trigger the bypass — their_selfcarve-outs /tenant_isolationstill apply (verified by a regression test that a member stays tenant-scoped), and the flag is deliberately not used for the wildcard-policy drop, so it can never leak rows to members.Fixes the empty-list symptom across all better-auth-managed Setup objects without per-object
tenancychanges (which would risk the control plane, where some of these objects ARE cross-env-isolated). -
Updated dependencies [cbc8c02]
-
Updated dependencies [07c2773]
-
Updated dependencies [d7a88df]
-
Updated dependencies [4f8f108]
-
Updated dependencies [ce0b4f6]
-
Updated dependencies [90bce88]
-
Updated dependencies [3209ec6]
-
Updated dependencies [e011d42]
-
Updated dependencies [6e5bdd5]
-
Updated dependencies [9ccfcd6]
-
Updated dependencies [ecf193f]
-
Updated dependencies [51bec81]
-
Updated dependencies [3e593a7]
-
Updated dependencies [63d5403]
- @objectstack/platform-objects@11.1.0
- @objectstack/core@11.1.0
- @objectstack/spec@11.1.0
- @objectstack/formula@11.1.0
- Updated dependencies [9b5bf3d]
- Updated dependencies [cb5b393]
- Updated dependencies [ab5718a]
- Updated dependencies [4845c12]
- Updated dependencies [c1a754a]
- Updated dependencies [6fbe91f]
- Updated dependencies [715d667]
- Updated dependencies [5eef4cf]
- Updated dependencies [72759e1]
- Updated dependencies [6c4fbd9]
- Updated dependencies [ef3ed67]
- Updated dependencies [cd51229]
- Updated dependencies [7697a0e]
- Updated dependencies [e7e04f1]
- Updated dependencies [cfd5ac4]
- Updated dependencies [2be5c1f]
- Updated dependencies [ad143ce]
- Updated dependencies [5c4a8c8]
- Updated dependencies [3afaeed]
- Updated dependencies [5737261]
- Updated dependencies [a619a3a]
- Updated dependencies [f44c1bd]
- Updated dependencies [8801c02]
- Updated dependencies [3d04e06]
- Updated dependencies [4a84c98]
- Updated dependencies [c715d25]
- Updated dependencies [aa33b02]
- Updated dependencies [d980f0d]
- Updated dependencies [a658523]
- Updated dependencies [82ff91c]
- Updated dependencies [638f472]
- @objectstack/platform-objects@11.0.0
- @objectstack/spec@11.0.0
- @objectstack/formula@11.0.0
- @objectstack/core@11.0.0
- @objectstack/spec@10.3.0
- @objectstack/core@10.3.0
- @objectstack/formula@10.3.0
- @objectstack/platform-objects@10.3.0
- Updated dependencies [b496498]
- @objectstack/spec@10.2.0
- @objectstack/core@10.2.0
- @objectstack/formula@10.2.0
- @objectstack/platform-objects@10.2.0
- Updated dependencies [49da36e]
- Updated dependencies [ac79f16]
- @objectstack/spec@10.1.0
- @objectstack/core@10.1.0
- @objectstack/formula@10.1.0
- @objectstack/platform-objects@10.1.0
-
e16f2a8: BREAKING: the system object
sys_departmentis renamed tosys_business_unit— object + member table (sys_department_member→sys_business_unit_member), fields, and i18n — with no compatibility alias. Any deployment holdingsys_departmentrows, or metadata that references the object by name (lookups, list views, queries, sharing/approval scopes), must migrate tosys_business_unit. A renamed shipped system object is a breaking change to the platform's public data surface, so this lands as a major. Verified per ADR-0059's pre-publish hotcrm gate: no published downstream consumer references the old name.ADR-0057 — ERP authorization core. Adds permission-grant access DEPTH (
own/own_and_reports/unit/unit_and_below/org), renamessys_department→sys_business_unit(no aliases — see BREAKING above), introduces the platform-ownedsys_user_positionassignment, and seeds stack-declaredroles/sharingRulesintosys_position/sys_sharing_ruleat boot (closes #2077). Hierarchy-relative scopes are delegated to a pluggableIHierarchyScopeResolver(open edition fails closed to owner-only;defineStackerrors withoutrequires: ['hierarchy-security']). Also fixes a latent over-grant whereengine.find({ filter })was ignored (driver readswhere) — normalizedfilter→wherein the engine. -
cfd86ce: ADR-0058 — expression & predicate surface unification. Adds the canonical CEL→FilterCondition pushdown compiler in
@objectstack/formula(compileCelToFilter,isPushdownableCel,lowerCelAst) plus an in-memorymatchesFilterConditionbackend (one AST, three backends).plugin-security(RLSusing, via a SQL bridge) andplugin-sharing(celToFilter) cut over to it, retiring the bespoke regex/field-equality front-ends. Compound sharing conditions now compile and enforce end-to-end (closes #1887). The RLScheckclause is now enforced on the write post-image (insert/by-id update), fail-closed. Non-pushdownable predicates (arithmetic, functions, subqueries, cross-object) are an authoring compile error, never silently dropped (ADR-0049/0055). -
6ca20b3: ADR-0058 D1 follow-through — RLS predicates are now canonical CEL. Migrated every seeded RLS
using/check(default permission sets, showcase, and theRLS.ownerPolicy/tenantPolicy/allowAllPolicyhelper factories) from the legacy SQL-ish form (=,IN (...)) to pure CEL (==,in), so authors and AI learn ONE expression language. ThesqlPredicateToCelbridge is retained as a DEPRECATED transitional shim: a stored SQL-style predicate still compiles (no silent deny on legacy data) but emits a deprecation warn; canonical CEL passes through as a no-op. No runtime behavior change — CEL and the old SQL form compile to the identical FilterCondition.
- Updated dependencies [d7ff626]
- Updated dependencies [2a1b16b]
- Updated dependencies [2256e93]
- Updated dependencies [7108ff3]
- Updated dependencies [30c0313]
- Updated dependencies [e16f2a8]
- Updated dependencies [cfd86ce]
- Updated dependencies [e411a82]
- Updated dependencies [ae271d0]
- Updated dependencies [61ed5c7]
- Updated dependencies [a581385]
- Updated dependencies [d5f6d29]
- Updated dependencies [220ce5b]
- Updated dependencies [3efe334]
- Updated dependencies [0df063e]
- Updated dependencies [ce13bb8]
- Updated dependencies [feead7e]
- Updated dependencies [6ca20b3]
- Updated dependencies [5f875fe]
- Updated dependencies [b469950]
- Updated dependencies [47d978a]
- Updated dependencies [48a307a]
- Updated dependencies [25fc0e4]
- @objectstack/spec@10.0.0
- @objectstack/platform-objects@10.0.0
- @objectstack/formula@10.0.0
- @objectstack/core@10.0.0
-
fa8964d: feat(security): RLS predicates that won't compile are surfaced, not silently dropped (ADR-0056 D4)
The RLS compiler previously dropped any
using/checkit could not parse (e.g.==,AND/OR, ranges) in silence — if it was the only policy, the object lost protection with no signal (the class of bug that left a showcase owner predicate inert for two PRs). Now the compiler WARNS (via the security plugin's logger) when an unsupported-shape predicate is dropped, distinguishing it from the intentional "context variable absent" fail-closed skip. Also exportsisSupportedRlsExpression(expr)so an authoring-time gate (objectstack compile) can reject a predicate the runtime would never enforce. No change to compiled filters for valid predicates; fail-closed semantics preserved. -
6595b53: feat(security): app-declarable default profile (
isDefault, ADR-0056 D7)An app can now declare its default access posture for authenticated users who have no explicit grants, via
isDefault: trueon a permission set — instead of always inheriting the built-inmember_default. The SecurityPlugin resolves the fallback from theisDefaultprofile when no explicitfallbackPermissionSetis configured (falling back tomember_defaultwhen none is declared — non-breaking). This is the foundation for SSO/JIT provisioning (mapping IdP claims → a declared default profile). Proven by theshowcase-default-profiledogfood test: a sign-up governed by a custom default that grants onlyshowcase_announcementcan read it but is deniedshowcase_private_note(which themember_defaultwildcard would have allowed). -
751f5cf: feat(security): declaration-derived public-form authorization (ADR-0056, Option A)
Public form submissions are now authorized by the declaration, not by a deployment-configured
guest_portalprofile. The form-submit route derives a narrowpublicFormGrant: { object }from the matched form's target object; the SecurityPlugin honors it as a least-privilege capability — create + the immediate read-back on THAT object only, with no userId, and crucially NOT the anonymous fall-open. This makes public forms work under secure-by-default (requireAuth) without a hand-configuredguest_portal, scoped to exactly the declared object (the field allow-list is still enforced at the route;guest_portal/anonymousare kept on the context for back-compat with guest-detection hooks). It is the prerequisite that unblocks the eventualrequireAuthdefault flip, and generalizes the platform principle "public access = declared + runtime-derived scoped grant" (the same shape share-links already use). Proven byform-self-authdogfood (create on target allowed; cross-object + update/delete denied). plugin-security 108, rest 121, full dogfood 98 — no regression. -
5a5a9fe: feat(security): public-form demo (Option A) + app-declared default profile wiring (ADR-0056 D7)
Wires ADR-0056's app-declarable default profile through the CLI so it actually takes effect under
pnpm dev.@objectstack/plugin-securityexports a newappDefaultProfileName(permissions)helper that extracts the firstisProfile && isDefaultprofile name from a stack;@objectstack/cli(serve.ts) passes it as the SecurityPluginfallbackPermissionSet(undefined → built-inmember_defaultpreserved, so apps that declare no default are unaffected).The showcase gains a working web-to-lead public form (
showcase_inquiry+ anallowAnonymousFormView authorized by the declaration-derivedpublicFormGrant, noguest_portalprofile) and an app-declared default profile (showcase_member_default), each covered by a dogfood proof over the real HTTP stack. -
4c213c2: Master-detail "controlled by parent" permissions (ADR-0055).
A detail object can now declare
sharingModel: 'controlled_by_parent': its read/write access is derived from its master record, with no authored RLS.@objectstack/spec:controlled_by_parentadded to the authorableobject.sharingModelenum.@objectstack/plugin-security: reads injectmasterFK IN (accessible master ids)(resolved from the master's own RLS, reusing the existing filter machinery — zero RLS-compiler changes); by-id writes (insert/update/delete) to a detail now require edit access to its master, closing the #1994-class by-id hole for derived access.@objectstack/verify: related-record topological synthesis —deriveCrudCasesno longer skips objects with required relations; it builds the object dependency graph, orders it topologically, and threads real target ids, so relationship-dense objects (and the master-detail RLS proof) are verifiable. Honestblockedverdicts remain for required-reference cycles and external/missing targets.
v1 limits (per ADR-0055): the accessible-master id set is unbounded (large-tenant scale is a documented future limit), and master-detail chains are single-level (not transitively traversed).
-
2afb612: feat(security): resolve
current_user.emailin RLS owner policiesRLS
usingpredicates can now referencecurrent_user.email— a unique, human-readable, seedable owner anchor (owner = current_user.email). Previously the RLS compiler resolved onlycurrent_user.id/organization_id/roles/org_user_ids, so any owner-by-name/email predicate silently compiled to the deny sentinel (fail-closed → the user saw nothing). Email is sourced for free from the auth session (with a boundedsys_userfallback for the API-key path) and threaded onto theExecutionContextin both identity resolvers — the REST data path (rest-server) and the dispatcher path (resolve-execution-context).Display
nameis deliberately not exposed to RLS: names collide, and a collision on an ownership predicate is an access-control leak. Only unique identifiers (id,email) are resolvable.This makes owner-scoped row-level security work with seed data (no per-user ids needed) and, combined with
controlled_by_parent(ADR-0055), lets a master's owner scoping flow to its detail records. The example-showcase demonstrates it:showcase_invoicecarries anowneremail + an owner RLS policy, its lines are controlled-by-parent, and invoices/lines are seeded per owner. It also fixes the showcase's previously inert owner predicates (they used==andcurrent_user.name, neither of which the compiler accepts) to= current_user.email.
- Updated dependencies [e7f6539]
- Updated dependencies [2365d07]
- Updated dependencies [6595b53]
- Updated dependencies [fa8964d]
- Updated dependencies [36138c7]
- Updated dependencies [a8e4f3b]
- Updated dependencies [4c213c2]
- Updated dependencies [2afb612]
- @objectstack/spec@9.11.0
- @objectstack/core@9.11.0
- @objectstack/platform-objects@9.11.0
-
1f88fd9: Converge the RLS contract with the reference compiler, and wire §7.3.1 dynamic membership.
- spec (docs): narrow
rls.zod.tsto the four expression forms the compiler actually implements —field = current_user.<prop>,field = 'literal',field IN (current_user.<array>), and1 = 1. Removed the over-promised surface (subqueries,AND/OR/NOT,LIKE/ILIKE, regex,ANY/ALL,NOT IN,IS NULL,NOW()/CURRENT_DATE) from the operator list, context-variable list, and@examplepolicies, and documented the fail-closed behaviour explicitly. - spec (schema):
ExecutionContextgainsrlsMembership?: Record<string, string[]>— a bag of pre-resolved dynamic-membership id arrays (team members, territory accounts, shared records) that the runtime stages so RLS can scope viafield IN (current_user.<key>)without subquery support. Generalizes the previously hard-codedorg_user_ids. - plugin-security:
RLSCompiler.compileFiltermergesrlsMembershipkeys into the user context (arrays only, never clobbering the namedid/organization_id/roles/org_user_idsfields), so §7.3.1 hierarchy- and sharing-based policies compile.compileExpressionnow recognizes1 = 1as always-true (empty filter), makingRLS.allowAllPolicygrant access instead of silently failing closed. Missing/empty membership sets still fail closed.
- spec (docs): narrow
-
e2b5324: feat(ownership): auto-provision a canonical
owner_idand hand seeded records to the first adminOwnership is now correct-by-default instead of opt-in — closing the gap where seeded demo data ended up owned by nobody a human can log in as (so "My" views, owner reports and owner notifications were empty out of the box) and where author-written objects silently shipped with no working ownership at all.
-
applySystemFields(objectql) now auto-injects a canonical, reassignableowner_idlookup (→sys_user) on user-authored business objects, alongside the existing tenant/audit fields. Unlike the audit*_bylookups it is NOT readonly — ownership transfers. Withheld formanagedBy/sys_*tables and for objects that opt out viaownership: 'org' | 'none'(Dataverse-style). The safe default direction: forgetting the opt-out leaves a harmless spare column, whereas the old opt-IN model let authors ship objects with broken ownership. Once present, the existing machinery engages automatically (insert auto-stamp, owner-scoped RLS, owner-keyed views/reports). -
claimSeedOwnership(plugin-security), invoked frombootstrapPlatformAdminright after the first human is promoted to platform admin, transfers ownership of seeded rows (owner_idNULL orusr_system) to that admin. The ownership twin of org-scoping'sclaimOrphanOrgRows. Idempotent; skipsmanagedBy/sys_*. Authors write plain seed records (noowner_id) and the platform — not the author — performs the handoff, so there is nothing to remember or mistype. -
usr_systemis never minted (runtime + objectql). The seed loader bindsos.userto a NULL identity, socelos.user.id``resolves to NULL at seed time (the owning admin does not exist yet) and the row seeds NULL-owned — then the handoff above fills it. The runtime'sensureSeedIdentity(the only code that inserted a`usr_system`row) is removed.`SystemUserId.SYSTEM`survives only as a reserved id so legacy DBs' exclusion guards / ownership handoff still recognize a pre-existing row.`os.org`is unaffected (derived from`organizationId`).
Also hardens
bootstrapPlatformAdminagainst a latent dts typecheck error (defensive read of the untypeddescriptionon seed permission sets). -
- Updated dependencies [db02bd5]
- Updated dependencies [641675d]
- Updated dependencies [94e9040]
- Updated dependencies [4331adb]
- Updated dependencies [1f88fd9]
- Updated dependencies [1f88fd9]
- @objectstack/spec@9.10.0
- @objectstack/platform-objects@9.10.0
- @objectstack/core@9.10.0
- @objectstack/spec@9.9.1
- @objectstack/core@9.9.1
- @objectstack/platform-objects@9.9.1
-
92d75ca: fix(security): enforce row-level security on by-id writes — close the member-can-edit-others'-records hole (#1985).
A single-id
update/deletegoes straight todriver.update(object, id, …)/driver.delete(object, id)and builds no queryast, so the RLSwherefilter the middleware injects on the read path was never applied to by-id writes. Combined withmember_defaultgranting*: { edit, delete }(scoped, by design, via theowner_only_writes/deletesRLS), this meant the owner predicate was silently bypassed: any authenticated member could modify or delete another user's records (verified end-to-end — a member PATCH'd an admin's record and the change persisted).Two coordinated changes:
- Enforce a pre-image authorization check. Before a single-id
update/delete, the security middleware computes the write-operation RLS filter and re-reads the target row with{ id } AND <writeFilter>; if the row isn't visible (someone else's, or RLS-hidden) it throwsPermissionDeniedError(403). Reuses the existing RLS/tenant machinery, is recursion-safe (afinddoesn't trigger the check), and is skipped when no RLS policy applies (e.g. admin sets,modifyAllRecords) so admins and unguarded objects are unchanged. - Repoint owner scoping to a column that exists.
owner_only_writes/owner_only_deleteskeyed onowner_id, which author-defined objects almost never declare — so the policy referenced a missing column andcomputeRlsFilterdropped it (the no-op that made the bypass invisible). Now keyed oncreated_by, the ownership column the engine stamps on every object.
Result: a member may edit/delete the records they created, not others'; admins (and any set with
modifyAllRecordsor no RLS) are unrestricted. Objects that opt out of audit fields (systemFields.audit: false) have nocreated_byand now fail closed for member writes (grantmodifyAllRecordsor a per-object policy to allow). Objects modeling transferable ownership should override with a per-object owner policy.Verified live on app-crm (2 users): member→others' record PATCH/DELETE = 403 (unmutated); member→own = 200; admin→any = 200. Note: cross-tenant write isolation additionally depends on an organization being assigned at sign-up (tracked separately in #1985).
- Enforce a pre-image authorization check. Before a single-id
- Updated dependencies [84249a4]
- Updated dependencies [11af299]
- Updated dependencies [d5774b5]
- Updated dependencies [134043a]
- Updated dependencies [90108e0]
- Updated dependencies [9afeb2d]
- Updated dependencies [6bec07e]
- Updated dependencies [601cc11]
- Updated dependencies [575448d]
- @objectstack/spec@9.9.0
- @objectstack/core@9.9.0
- @objectstack/platform-objects@9.9.0
- Updated dependencies [97c55b3]
- Updated dependencies [1b1f490]
- @objectstack/spec@9.8.0
- @objectstack/core@9.8.0
- @objectstack/platform-objects@9.8.0
- @objectstack/spec@9.7.0
- @objectstack/core@9.7.0
- @objectstack/platform-objects@9.7.0
- Updated dependencies [d1e930a]
- Updated dependencies [71578f2]
- Updated dependencies [5e3a301]
- Updated dependencies [5db2742]
- @objectstack/spec@9.6.0
- @objectstack/core@9.6.0
- @objectstack/platform-objects@9.6.0
- Updated dependencies [ee72aae]
- @objectstack/spec@9.5.1
- @objectstack/core@9.5.1
- @objectstack/platform-objects@9.5.1
- Updated dependencies [d08551c]
- Updated dependencies [5be7102]
- Updated dependencies [707aeed]
- Updated dependencies [7a103d4]
- Updated dependencies [4b01250]
- @objectstack/spec@9.5.0
- @objectstack/platform-objects@9.5.0
- @objectstack/core@9.5.0
- Updated dependencies [060467a]
- Updated dependencies [0856476]
- Updated dependencies [b678d8c]
- Updated dependencies [b678d8c]
- Updated dependencies [b678d8c]
- @objectstack/spec@9.4.0
- @objectstack/core@9.4.0
- @objectstack/platform-objects@9.4.0
- Updated dependencies [1ada658]
- Updated dependencies [3219191]
- Updated dependencies [290f631]
- Updated dependencies [50b7b47]
- Updated dependencies [f15d6f6]
- Updated dependencies [f8684ea]
- Updated dependencies [c802327]
- Updated dependencies [b4765be]
- @objectstack/spec@9.3.0
- @objectstack/platform-objects@9.3.0
- @objectstack/core@9.3.0
- Updated dependencies [2f57b75]
- Updated dependencies [2f57b75]
- @objectstack/spec@9.2.0
- @objectstack/core@9.2.0
- @objectstack/platform-objects@9.2.0
- Updated dependencies [b9062c9]
- @objectstack/spec@9.1.0
- @objectstack/core@9.1.0
- @objectstack/platform-objects@9.1.0
- Updated dependencies [1817845]
- @objectstack/spec@9.0.1
- @objectstack/core@9.0.1
- @objectstack/platform-objects@9.0.1
- Updated dependencies [4c3f693]
- Updated dependencies [0bf39f1]
- Updated dependencies [f533f42]
- Updated dependencies [1c83ee8]
- @objectstack/spec@9.0.0
- @objectstack/core@9.0.0
- @objectstack/platform-objects@9.0.0
- @objectstack/spec@8.0.1
- @objectstack/core@8.0.1
- @objectstack/platform-objects@8.0.1
-
1e8b680: fix(security): close four P0 launch-readiness findings
- plugin-auth (P0-1):
generateSecret()now throws (fails boot) when noOS_AUTH_SECRETis set andNODE_ENV==='production', instead of silently falling back to a predictabledev-secret-<timestamp>(session forgery). The dev/test fallback is unchanged. - plugin-security (P0-2): the permission-resolution
catchnow fails closed — it logs at ERROR and throwsPermissionDeniedErrorrather thanreturn next(). A degraded metadata service can no longer let every authenticated request bypass RBAC/RLS. System operations still bypass as before. - driver-sql (P0-3): the
contains/$containsoperator now escapes LIKE metacharacters (%/_/\) in the user value and binds an explicitESCAPE '\', so a value of%matches literally instead of every row (filter bypass). Correct across SQLite/MySQL/Postgres. - driver-mongodb (P0-4): the field-operator translator now rejects unknown
$-operators instead of passing them through, blocking$where/$function/$expr(server-side JS execution / query-intent bypass). All legitimate ObjectQL operators remain allowlisted.
+12 regression tests across the four packages.
- plugin-auth (P0-1):
-
Updated dependencies [a46c017]
-
Updated dependencies [b990b89]
-
Updated dependencies [99111ec]
-
Updated dependencies [d5a8161]
-
Updated dependencies [5cf1f1b]
-
Updated dependencies [9ef89d4]
-
Updated dependencies [3306d2f]
-
Updated dependencies [c262301]
-
Updated dependencies [bc44195]
-
Updated dependencies [9e2e229]
- @objectstack/spec@8.0.0
- @objectstack/core@8.0.0
- @objectstack/platform-objects@8.0.0
- @objectstack/spec@7.9.0
- @objectstack/core@7.9.0
- @objectstack/platform-objects@7.9.0
- Updated dependencies [06f2bbb]
- Updated dependencies [36719db]
- Updated dependencies [424ab26]
- @objectstack/spec@7.8.0
- @objectstack/core@7.8.0
- @objectstack/platform-objects@7.8.0
- Updated dependencies [b391955]
- Updated dependencies [f06b64e]
- Updated dependencies [023bf93]
- Updated dependencies [764c747]
- @objectstack/spec@7.7.0
- @objectstack/platform-objects@7.7.0
- @objectstack/core@7.7.0
- Updated dependencies [955d4c8]
- Updated dependencies [c4a4cbd]
- Updated dependencies [b046ec2]
- Updated dependencies [2170ad9]
- Updated dependencies [02d6359]
- Updated dependencies [7648242]
- Updated dependencies [8fa1e7f]
- Updated dependencies [7ae6abc]
- Updated dependencies [55866f5]
- Updated dependencies [60f9c45]
- @objectstack/spec@7.6.0
- @objectstack/platform-objects@7.6.0
- @objectstack/core@7.6.0
- @objectstack/spec@7.5.0
- @objectstack/core@7.5.0
- @objectstack/platform-objects@7.5.0
- @objectstack/spec@7.4.1
- @objectstack/core@7.4.1
- @objectstack/platform-objects@7.4.1
-
e478e0c: ADR-0029 K2 — security domain ownership (RBAC + sharing) + Setup nav contributions.
Moves the security objects out of the
@objectstack/platform-objectsmonolith into the two capability plugins that already register and operate them, split by concern (the two are orthogonal — sharing objects never reference RBAC objects):@objectstack/plugin-security(RBAC) gainssys_position,sys_permission_set,sys_user_permission_set,sys_position_permission_set, and thedefaultPermissionSetsseed (which itsbootstrap-platform-adminalready consumes). The RBAC + default-permission-set tests move with them.@objectstack/plugin-sharinggainssys_record_share,sys_sharing_rule,sys_share_link.@objectstack/platform-objectsno longer defines/exports any security objects; the/securitysubpath is now an empty barrel. Runtime is unchanged (both plugins already registered these objects at runtime).
D7 navigation — the Setup app's
group_access_controlis now assembled from three sources:plugin-securitycontributes Roles / Permission Sets (priority 100),plugin-sharingcontributes Sharing Rules / Record Shares (priority 200), andplatform-objectskeeps only API Keys (sys_api_key, an identity object, priority 300) — preserving the original menu order.i18n (D8) — the objects are removed from the
platform-objectsi18n extract config; existing generated bundles keep working at runtime (object-name keyed). Migrating the i18n extraction to the owning plugins remains the tracked follow-up.
-
4404572: ADR-0029 D8 — migrate i18n ownership for the moved domains to their plugins.
The object translations for the domains decomposed in K2.a/K2.b/K2 previously lived in the
@objectstack/platform-objectsgenerated bundles even though the objects now live in their capability plugins. This moves each domain's i18n extraction + bundles to the owning plugin, preserving every hand-translated string (zh-CN / ja-JP / es-ES):- Each plugin gains a build-time
scripts/i18n-extract.config.tsand asrc/translations/bundle ({locale}.objects.generated.ts+ anindex.tsbarrel), generated withos i18n extractand self-baselined so re-runs preserve translations. - Each plugin loads its bundle at runtime on
kernel:readyviai18n.loadTranslations(the i18n service is optional — load is best-effort).plugin-webhooks←sys_webhook,sys_webhook_deliveryplugin-approvals←sys_approval_request,sys_approval_actionplugin-security←sys_position,sys_permission_set,sys_user_permission_set,sys_position_permission_setplugin-sharing←sys_record_share,sys_sharing_rule,sys_share_link
@objectstack/platform-objectstranslation bundles are regenerated to drop those objects' keys (its extract config already excluded them); all other objects' translations and the metadata-form bundles are preserved.
Net runtime effect is unchanged (same translations load, now contributed by the package that owns each object) — closing the D8 follow-up tracked since K2.a.
- Each plugin gains a build-time
-
08fbbb4: Fix: the first-boot platform-admin promotion no longer gets stolen by the
usr_systemseed identity, and the dev seed admin uses fixed, well-known credentials.@objectstack/plugin-security—bootstrapPlatformAdminskips the system user5e831dea3(#1392) addedensureSeedIdentityto the runtime SeedLoader, which upserts a non-loginable system identity (usr_system, rolesystem,system@objectstack.local) to own seeded records — created before the first human sign-up. BecausebootstrapPlatformAdminpromoted the earliest-createdsys_user, on any app that ships seed datausr_systemwon the promotion and the real admin login stayed atrole: user. Login succeeded but Setup and Studio (gated bysetup.access/studio.accessonadmin_full_access) were invisible — a silent, confusing regression.bootstrap-platform-admin.tsnow filters out the system account (id === SystemUserId.SYSTEM || role === 'system') when picking the first user to promote, and the "an admin already exists" short-circuit ignores anyadmin_full_accessgrant held byusr_system— so a database where it was wrongly promoted self-heals on the next boot.@objectstack/cli—os devseedsadmin@objectos.ai/admin123The
--admin-email/--admin-passworddefaults changed fromadmin@dev.local/admin12345to the fixed, well-knownadmin@objectos.ai/admin123, so tooling and docs never have to guess the seeded credentials. Override with--admin-email/--admin-password. -
Updated dependencies [23c7107]
-
Updated dependencies [c72daad]
-
Updated dependencies [4404572]
-
Updated dependencies [eea3f1b]
-
Updated dependencies [e478e0c]
-
Updated dependencies [4cc2ced]
-
Updated dependencies [13632b1]
-
Updated dependencies [f115182]
-
Updated dependencies [2faf9f2]
-
Updated dependencies [2faf9f2]
-
Updated dependencies [2faf9f2]
-
Updated dependencies [58b450b]
-
Updated dependencies [82eb6cf]
-
Updated dependencies [c381977]
-
Updated dependencies [13d8653]
-
Updated dependencies [ff3d006]
-
Updated dependencies [5e831de]
- @objectstack/spec@7.4.0
- @objectstack/platform-objects@7.4.0
- @objectstack/core@7.4.0
- Updated dependencies [5e7c554]
- @objectstack/spec@7.3.0
- @objectstack/core@7.3.0
- @objectstack/platform-objects@7.3.0
- @objectstack/spec@7.2.1
- @objectstack/core@7.2.1
- @objectstack/platform-objects@7.2.1
- @objectstack/spec@7.2.0
- @objectstack/core@7.2.0
- @objectstack/platform-objects@7.2.0
- Updated dependencies [6228609]
- Updated dependencies [47a92f4]
- @objectstack/platform-objects@7.1.0
- @objectstack/spec@7.1.0
- @objectstack/core@7.1.0
-
3a630b6: Split organization-scoping from
@objectstack/plugin-securityinto a new@objectstack/plugin-org-scopingpackage.Per ADR-0002, "tenant" in ObjectStack means physical isolation (one Environment = one database, handled by
@objectstack/driver-turso's multi-tenant router). The row-levelorganization_idscoping that previously lived inside SecurityPlugin is a different concept — logical scoping inside a single DB — and now ships as its own plugin.- Removed the
multiTenantconstructor option. SecurityPlugin no longer touchesorganization_idon insert and no longer registers thesys_organizationpost-create seed pipeline. - Wildcard
current_user.organization_idRLS policies in the default permission sets are now stripped UNLESS the neworg-scopingservice is registered (i.e. unlessOrgScopingPluginis also installed). - Removed export
cloneTenantSeedData(now exposed ascloneOrgSeedDatafrom@objectstack/plugin-org-scoping). bootstrapPlatformAdmin()no longer accepts amultiTenantflag and no longer auto-creates a default organization — that behavior moved toensureDefaultOrganization()in the new plugin.
Single-tenant deployments — no action required.
Multi-tenant deployments (previously
new SecurityPlugin({ multiTenant: true })):+ import { OrgScopingPlugin } from '@objectstack/plugin-org-scoping'; import { SecurityPlugin } from '@objectstack/plugin-security'; + await kernel.use(new OrgScopingPlugin()); // MUST be BEFORE SecurityPlugin - await kernel.use(new SecurityPlugin({ multiTenant: true })); + await kernel.use(new SecurityPlugin());
The runtime's
OS_MULTI_TENANTenv switch — read by@objectstack/runtime/cloud/ArtifactKernelFactory,@objectstack/plugin-dev, and theobjectstackCLI'sserve/dev/startcommands — automatically registersOrgScopingPluginwhen set totrue, so projects driven by the CLI need no code changes. - Removed the
- Updated dependencies [74470ad]
- Updated dependencies [d29617e]
- Updated dependencies [dc72172]
- Updated dependencies [d29617e]
- Updated dependencies [010757b]
- Updated dependencies [257954d]
- @objectstack/spec@7.0.0
- @objectstack/platform-objects@7.0.0
- @objectstack/core@7.0.0
- @objectstack/spec@6.9.0
- @objectstack/core@6.9.0
- @objectstack/platform-objects@6.9.0
- @objectstack/spec@6.8.1
- @objectstack/core@6.8.1
- @objectstack/platform-objects@6.8.1
- Updated dependencies [6e88f77]
- Updated dependencies [c8b9f57]
- Updated dependencies [45d27c5]
- @objectstack/spec@6.8.0
- @objectstack/platform-objects@6.8.0
- @objectstack/core@6.8.0
- @objectstack/spec@6.7.1
- @objectstack/core@6.7.1
- @objectstack/platform-objects@6.7.1
- Updated dependencies [430067b]
- Updated dependencies [4f9e9d4]
- Updated dependencies [4f9e9d4]
- @objectstack/spec@6.7.0
- @objectstack/platform-objects@6.7.0
- @objectstack/core@6.7.0
- Updated dependencies [a49cfc2]
- @objectstack/spec@6.6.0
- @objectstack/core@6.6.0
- @objectstack/platform-objects@6.6.0
- @objectstack/spec@6.5.1
- @objectstack/core@6.5.1
- @objectstack/platform-objects@6.5.1
- @objectstack/spec@6.5.0
- @objectstack/core@6.5.0
- @objectstack/platform-objects@6.5.0
- Updated dependencies [f8651cc]
- Updated dependencies [f8651cc]
- Updated dependencies [0bf6f9a]
- @objectstack/spec@6.4.0
- @objectstack/core@6.4.0
- @objectstack/platform-objects@6.4.0
- @objectstack/spec@6.3.0
- @objectstack/core@6.3.0
- @objectstack/platform-objects@6.3.0
- Updated dependencies [b4c74a9]
- @objectstack/spec@6.2.0
- @objectstack/core@6.2.0
- @objectstack/platform-objects@6.2.0
- @objectstack/spec@6.1.1
- @objectstack/core@6.1.1
- @objectstack/platform-objects@6.1.1
- Updated dependencies [93c0589]
- @objectstack/spec@6.1.0
- @objectstack/core@6.1.0
- @objectstack/platform-objects@6.1.0
- Updated dependencies [629a716]
- Updated dependencies [dbc4f7d]
- Updated dependencies [944f187]
- @objectstack/spec@6.0.0
- @objectstack/platform-objects@6.0.0
- @objectstack/core@6.0.0
-
b806f58: Scope
sys_uservisibility to fellow organization members.The default RLS policy on
sys_userwasid = current_user.id, which meant @-mention pickers, owner/assignee lookups, reviewer selectors and the user roster all returned just the current user. The RLS compiler doesn't support subqueries, so aid IN (SELECT user_id FROM sys_member ...)policy isn't expressible.This change:
- Pre-resolves
org_user_ids(the IDs of all users in the active org) intoExecutionContextin all three REST entry-point resolvers (@objectstack/rest,@objectstack/runtime,@objectstack/plugin-hono-server). - Adds the field to
ExecutionContextSchemaso it survives Zod parsing. - Adds an
org_user_idsfield to the RLS compiler's user context. - Adds a new
sys_user_org_memberspolicy (id IN (current_user.org_user_ids)) to bothmember_defaultandviewer_readonlypermission sets, alongside the existingsys_user_selfpolicy. The RLS compiler OR-combines them, so users see themselves AND their org collaborators.
Capped at 1000 members per request. Large enterprises should plug in a directory cache or split per workspace.
- Pre-resolves
-
Updated dependencies [bab2b20]
-
Updated dependencies [fa011d8]
-
Updated dependencies [f0f7c27]
-
Updated dependencies [b806f58]
- @objectstack/platform-objects@5.2.0
- @objectstack/spec@5.2.0
- @objectstack/core@5.2.0
- Updated dependencies [75f4ee6]
- Updated dependencies [823d559]
- @objectstack/spec@5.1.0
- @objectstack/platform-objects@5.1.0
- @objectstack/core@5.1.0
- Updated dependencies [888a5c1]
- Updated dependencies [2f9073a]
- @objectstack/platform-objects@5.0.0
- @objectstack/spec@5.0.0
- @objectstack/core@5.0.0
- Updated dependencies [2869891]
- @objectstack/spec@4.2.0
- @objectstack/core@4.2.0
- @objectstack/platform-objects@4.2.0
- @objectstack/spec@4.1.1
- @objectstack/core@4.1.1
- @objectstack/platform-objects@4.1.1
-
d3b455f: Add server-side Field-Level Security write enforcement. Client-side ObjectForm / inline-grid already hides non-editable fields, but the SecurityPlugin middleware previously only enforced FLS on read (
maskResultson find/findOne). Insert and update operations could target any field — a hand-crafted POST bypassed FLS entirely.The middleware now runs
FieldMasker.detectForbiddenWriteson every insert / update payload (single record or bulk array) and throwsPermissionDeniedError(HTTP 403) when the payload references a field the caller is not permitted to edit. The offending field list is exposed viadetails.forbiddenFieldsfor actionable client error UI.Allow-list semantics: only fields explicitly enumerated in a permission set's
fieldsmap are constrained. System operations (ExecutionContext.isSystem) continue to bypass the check.Why throw vs. silently stripping: silent strip hides the boundary from honest clients (partial-save confusion) AND gives probing clients no signal that the field exists. Throwing makes the boundary observable in both directions.
Also exposes
FieldMasker.detectForbiddenWrites(data, fieldPermissions)as a standalone helper for callers that want to do the check out-of-band (e.g., adapters that strip-then-warn instead of fail-closed).
- Updated dependencies [2108c30]
- Updated dependencies [23db640]
- @objectstack/spec@4.1.0
- @objectstack/core@4.1.0
- @objectstack/platform-objects@4.1.0
- 15e0df6: chore: unify all package versions to a single patch release
- Updated dependencies [15e0df6]
- @objectstack/spec@4.0.5
- @objectstack/core@4.0.5
- @objectstack/platform-objects@4.0.5
- Updated dependencies [326b66b]
- @objectstack/spec@4.0.4
- @objectstack/core@4.0.4
- @objectstack/spec@4.0.3
- @objectstack/core@4.0.3
- Updated dependencies [5f659e9]
- @objectstack/spec@4.0.2
- @objectstack/core@4.0.2
- Updated dependencies [f08ffc3]
- Updated dependencies [e0b0a78]
- @objectstack/spec@4.0.0
- @objectstack/core@4.0.0
- @objectstack/spec@3.3.1
- @objectstack/core@3.3.1
- @objectstack/spec@3.3.0
- @objectstack/core@3.3.0
- @objectstack/spec@3.2.9
- @objectstack/core@3.2.9
- @objectstack/spec@3.2.8
- @objectstack/core@3.2.8
- @objectstack/spec@3.2.7
- @objectstack/core@3.2.7
- @objectstack/spec@3.2.6
- @objectstack/core@3.2.6
- @objectstack/spec@3.2.5
- @objectstack/core@3.2.5
- @objectstack/spec@3.2.4
- @objectstack/core@3.2.4
- @objectstack/spec@3.2.3
- @objectstack/core@3.2.3
- Updated dependencies [46defbb]
- @objectstack/spec@3.2.2
- @objectstack/core@3.2.2
- Updated dependencies [850b546]
- @objectstack/spec@3.2.1
- @objectstack/core@3.2.1
- Updated dependencies [5901c29]
- @objectstack/spec@3.2.0
- @objectstack/core@3.2.0
- Updated dependencies [953d667]
- @objectstack/spec@3.1.1
- @objectstack/core@3.1.1
- Updated dependencies [0088830]
- @objectstack/spec@3.1.0
- @objectstack/core@3.1.0
- Updated dependencies [92d9d99]
- @objectstack/spec@3.0.11
- @objectstack/core@3.0.11
- Updated dependencies [d1e5d31]
- @objectstack/spec@3.0.10
- @objectstack/core@3.0.10
- Updated dependencies [15e0df6]
- @objectstack/spec@3.0.9
- @objectstack/core@3.0.9
- Updated dependencies [5a968a2]
- @objectstack/spec@3.0.8
- @objectstack/core@3.0.8
- Updated dependencies [0119bd7]
- Updated dependencies [5426bdf]
- @objectstack/spec@3.0.7
- @objectstack/core@3.0.7
- Updated dependencies [5df254c]
- @objectstack/spec@3.0.6
- @objectstack/core@3.0.6
- Updated dependencies [23a4a68]
- @objectstack/spec@3.0.5
- @objectstack/core@3.0.5
- Updated dependencies [d738987]
- @objectstack/spec@3.0.4
- @objectstack/core@3.0.4
- c7267f6: Patch release for maintenance updates and improvements.
- Updated dependencies [c7267f6]
- @objectstack/spec@3.0.3
- @objectstack/core@3.0.3
- Updated dependencies [28985f5]
- @objectstack/spec@3.0.2
- @objectstack/core@3.0.2
- Updated dependencies [389725a]
- @objectstack/spec@3.0.1
- @objectstack/core@3.0.1
- Release v3.0.0 — unified version bump for all ObjectStack packages.
- Updated dependencies
- @objectstack/spec@3.0.0
- @objectstack/core@3.0.0
- Updated dependencies
- @objectstack/spec@2.0.7
- @objectstack/core@2.0.7
- Patch release for maintenance and stability improvements
- Updated dependencies
- @objectstack/spec@2.0.6
- @objectstack/core@2.0.6
- Unify all package versions with a patch release
- Updated dependencies
- @objectstack/spec@2.0.5
- @objectstack/core@2.0.5