Skip to content

Latest commit

 

History

History
2961 lines (2281 loc) · 169 KB

File metadata and controls

2961 lines (2281 loc) · 169 KB

@objectstack/plugin-security

16.1.0

Patch Changes

  • 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

16.0.0

Minor Changes

  • 22013aa: Split the overloaded managedBy: 'system' bucket into engine-owned vs. admin-writable, and enforce engine-owned writes (ADR-0103, #3220). The system bucket 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 as better-auth/append-only but, unlike better-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 managedBy enum value (which would fall through to fully-editable platform defaults on already-deployed Console clients), the write policy is now the resolved affordance (resolveCrudAffordances = bucket default + userActions), and engine-owned is defined as a system/append-only object 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 declare userActions: { create, edit, delete: true }. The affordance is a declaration only — the DelegatedAdminGate / RLS / permission sets remain the authz.
    • Engine-owned objects locked to readsapiMethods: ['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_secret is explicitly read-locked (an empty apiMethods array fails open).
    • sys_import_job stays engine-owned: the REST import route now writes its job rows isSystem-elevated (attribution preserved via the explicit created_by stamp) and the object is locked to ['get','list'].
    • New engine write guard (assertEngineOwnedWriteAllowed, plugin-security) fail-closed rejects user-context generic writes to engine-owned system/append-only objects, keyed off the resolved affordance; isSystem and 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 just better-auth: any advertised write verb an object's resolved affordances forbid is stripped at registration with a warning (the drift backstop, ADR-0049).
    • /me/permissions clamp (plugin-hono-server) now clamps system/append-only as well as better-auth, so the client hint reflects permission ∩ guard.

    Potentially breaking: a downstream/third-party system object 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. Declare userActions opening the verbs the object legitimately takes from a user context. better-auth keeps plugin-auth's identity write guard unchanged; the row-level managed_by provenance vocabulary (ADR-0066) is a different axis and is untouched.

  • 62a2117: Split the overloaded managedBy: 'system' bucket with an explicit engine-owned value (ADR-0103 addendum, #3343). ADR-0103 deferred the enum split ("revisitable later as a rename") because a new managedBy value would fall through to the fully-editable platform default on deployed Console clients. Both reasons against it are now retired — the server-side write guard / apiMethods reconciliation / /me/permissions clamp 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-owned with the same all-locked default affordance row as system (create/import/edit/delete: false, exportCsv: true). It joins ENGINE_OWNED_BUCKETS (the engine write guard) and GUARDED_WRITE_BUCKETS (the /me/permissions clamp); the guard, reconcileManagedApiMethods, and the clamp mechanisms are unchanged — engine-owned is 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-opening userActions (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) — system now reads as "engine-managed schema, writable via userActions".

    Behaviour-, enforcement- and wire-identical: resolved affordances, the guard verdict, the 405 apiMethods reconciliation, and the permissions clamp are the same before and after — this is a self-documenting relabel, not a policy change. No data migration (managedBy is schema metadata) and no code branches on the 'system' literal. Retiring the overloaded system entirely (moving the 8 writable objects to a dedicated bucket) is a breaking rename deferred to v17.

Patch Changes

  • 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 adversarial KNOWN DIVERGENCE pins (scoped admin_full_access grant; 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 pure hasPlatformAdminCapability (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.posture rung (#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/organizations only): a principal whose carried rung is not PLATFORM_ADMIN no 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 scoped admin_full_access grant (sys_user_permission_set.organization_id non-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_set for admin_full_access rows with a non-null organization_id, and custom permission sets whose systemPermissions intersect {manage_metadata, manage_platform_settings, studio.access, manage_users}. To restore cross-tenant operator access for such a principal, grant the unscoped admin_full_access instead. 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/modifyAllRecords short-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_OBJECTS list — exactly the drift ADR-0092 forbids, and it had already drifted (the list carried 17 names while 22 schemas declare managedBy: 'better-auth', leaving sys_scim_provider, sys_sso_provider, and three sys_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 registered managedBy: 'better-auth' object into the four write-granting default sets (organization_admin, member_default, viewer_readonly, MCP write) at kernel:ready, mutating the shared in-memory bootstrapPermissionSets in place (the array the evaluator resolves and the seeder serializes — a DB-row-only fix would be dead code). Never touches admin_full_access, never overrides an existing explicit entry, ignores userActions (the better-auth bucket is hard-denied — sys_user's userActions.edit opens only a field-level whitelist the identity guard enforces).
    • The static BETTER_AUTH_MANAGED_OBJECTS list is completed to 22 and kept as a compile-time baseline (covers the pre-kernel:ready window), now pinned bidirectionally against the @objectstack/platform-objects schemas by a test so it cannot silently rot again.
    • Engine-owned system/append-only objects are deliberately NOT given deny entries — a per-object entry overrides the wildcard and would drop viewAllRecords; 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.

  • 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

16.0.0-rc.1

Minor Changes

  • 62a2117: Split the overloaded managedBy: 'system' bucket with an explicit engine-owned value (ADR-0103 addendum, #3343). ADR-0103 deferred the enum split ("revisitable later as a rename") because a new managedBy value would fall through to the fully-editable platform default on deployed Console clients. Both reasons against it are now retired — the server-side write guard / apiMethods reconciliation / /me/permissions clamp 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-owned with the same all-locked default affordance row as system (create/import/edit/delete: false, exportCsv: true). It joins ENGINE_OWNED_BUCKETS (the engine write guard) and GUARDED_WRITE_BUCKETS (the /me/permissions clamp); the guard, reconcileManagedApiMethods, and the clamp mechanisms are unchanged — engine-owned is 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-opening userActions (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) — system now reads as "engine-managed schema, writable via userActions".

    Behaviour-, enforcement- and wire-identical: resolved affordances, the guard verdict, the 405 apiMethods reconciliation, and the permissions clamp are the same before and after — this is a self-documenting relabel, not a policy change. No data migration (managedBy is schema metadata) and no code branches on the 'system' literal. Retiring the overloaded system entirely (moving the 8 writable objects to a dedicated bucket) is a breaking rename deferred to v17.

Patch Changes

  • 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_OBJECTS list — exactly the drift ADR-0092 forbids, and it had already drifted (the list carried 17 names while 22 schemas declare managedBy: 'better-auth', leaving sys_scim_provider, sys_sso_provider, and three sys_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 registered managedBy: 'better-auth' object into the four write-granting default sets (organization_admin, member_default, viewer_readonly, MCP write) at kernel:ready, mutating the shared in-memory bootstrapPermissionSets in place (the array the evaluator resolves and the seeder serializes — a DB-row-only fix would be dead code). Never touches admin_full_access, never overrides an existing explicit entry, ignores userActions (the better-auth bucket is hard-denied — sys_user's userActions.edit opens only a field-level whitelist the identity guard enforces).
    • The static BETTER_AUTH_MANAGED_OBJECTS list is completed to 22 and kept as a compile-time baseline (covers the pre-kernel:ready window), now pinned bidirectionally against the @objectstack/platform-objects schemas by a test so it cannot silently rot again.
    • Engine-owned system/append-only objects are deliberately NOT given deny entries — a per-object entry overrides the wildcard and would drop viewAllRecords; 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.

  • 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

16.0.0-rc.0

Minor Changes

  • 22013aa: Split the overloaded managedBy: 'system' bucket into engine-owned vs. admin-writable, and enforce engine-owned writes (ADR-0103, #3220). The system bucket 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 as better-auth/append-only but, unlike better-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 managedBy enum value (which would fall through to fully-editable platform defaults on already-deployed Console clients), the write policy is now the resolved affordance (resolveCrudAffordances = bucket default + userActions), and engine-owned is defined as a system/append-only object 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 declare userActions: { create, edit, delete: true }. The affordance is a declaration only — the DelegatedAdminGate / RLS / permission sets remain the authz.
    • Engine-owned objects locked to readsapiMethods: ['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_secret is explicitly read-locked (an empty apiMethods array fails open).
    • sys_import_job stays engine-owned: the REST import route now writes its job rows isSystem-elevated (attribution preserved via the explicit created_by stamp) and the object is locked to ['get','list'].
    • New engine write guard (assertEngineOwnedWriteAllowed, plugin-security) fail-closed rejects user-context generic writes to engine-owned system/append-only objects, keyed off the resolved affordance; isSystem and 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 just better-auth: any advertised write verb an object's resolved affordances forbid is stripped at registration with a warning (the drift backstop, ADR-0049).
    • /me/permissions clamp (plugin-hono-server) now clamps system/append-only as well as better-auth, so the client hint reflects permission ∩ guard.

    Potentially breaking: a downstream/third-party system object 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. Declare userActions opening the verbs the object legitimately takes from a user context. better-auth keeps plugin-auth's identity write guard unchanged; the row-level managed_by provenance vocabulary (ADR-0066) is a different axis and is untouched.

Patch Changes

  • 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 adversarial KNOWN DIVERGENCE pins (scoped admin_full_access grant; 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 pure hasPlatformAdminCapability (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.posture rung (#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/organizations only): a principal whose carried rung is not PLATFORM_ADMIN no 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 scoped admin_full_access grant (sys_user_permission_set.organization_id non-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_set for admin_full_access rows with a non-null organization_id, and custom permission sets whose systemPermissions intersect {manage_metadata, manage_platform_settings, studio.access, manage_users}. To restore cross-tenant operator access for such a principal, grant the unscoped admin_full_access instead. 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/modifyAllRecords short-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

15.1.1

Patch Changes

  • @objectstack/spec@15.1.1
  • @objectstack/core@15.1.1
  • @objectstack/formula@15.1.1
  • @objectstack/platform-objects@15.1.1

15.1.0

Minor Changes

  • f531a26: OWD posture is now enforced on the runtime write path (#3050). metadata-protocol gains the ADR-0094-addendum registerAuthoringGate(type, gate) seam — an awaited, throwing pre-persistence hook inside saveMetaItem (draft and publish-mode saves; environment writes only). plugin-security registers the object posture gate on it: an environment overlay of a packaged object may only TIGHTEN sharingModel/externalSharingModel (ADR-0086 D1 — closes the OS_METADATA_WRITABLE=object unvalidated-widening hole), and externalSharingModel ≤ sharingModel (ADR-0090 D11) is now rejected at save time instead of only by CLI lint. Write-path only — stored metadata keeps loading unchanged.

Patch Changes

  • 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), getReadFilter now denies fail-closed (deny sentinel + error log) when context.onBehalfOf.userId is 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_records tool — GROUP BY aggregation over the engine read path

    New MCP tool aggregate_records (count/sum/avg/min/max/count_distinct, optional groupBy incl. date bucketing, where filter, IANA timezone) in the data:read family. 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: aggregate maps to the list ApiMethod in the object exposure gate (an object whose apiMethods whitelist excludes list cannot 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.aggregate simply does not register the tool (graceful degradation, same contract as the action tools).

  • f531a26: fix(security): exempt engine referential FK clears from the owner_id transfer guard (#3023)

    Follow-up to the #3004 ownership-anchor guard. owner_id is a lookup to sys_user with the default deleteBehavior: 'set_null', so deleting a sys_user makes cascadeDeleteRelations null owner_id on every dependent row. That cascade write re-entered the write middleware under the deleter's context, where the #3004 guard read the owner_id = null as 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 a public_read_write child 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. cascadeDeleteRelations now tags the set_null write with a server-derived __referentialFieldClear context 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_id ownership 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 not readonly (ownership is transferable), so the static-readonly strip never covered it and FLS doesn't gate it by default. A non-privileged writer could therefore insert a record under someone else's name (forge) or update one to a new owner (transfer / disown), evading the owner gate that governs update/delete. The security middleware (plugin-security step 3.5) now treats owner_id as 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 supplied owner_id is 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 carrying owner_id fails closed. A non-scalar owner_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, or modifyAllRecords which 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 (not isSystem), so a non-privileged user importing a CSV whose owner_id column names other users is correctly denied unless they hold the transfer grant — administrators (who carry modifyAllRecords) are unaffected.

    • #2982 — bulk writes skipped owner scoping on OWD-private objects. A update({ multi: true }) / bulk delete rebuilt the driver AST from options.where AFTER the middleware chain, discarding the owner/RLS write filter that plugin-sharing (buildWriteFilter) and plugin-security compose onto opCtx.ast — so a member's bulk write hit every matching row, including peers'. The engine now seeds opCtx.ast from the caller's predicate BEFORE the chain (the same seam reads use) and hands the middleware-composed AST to driver.updateMany / driver.deleteMany, so bulk writes are constrained to the rows the caller may edit — matching single-id write behavior. delete now applies the same scalar-id guard update already 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/delete carry an opCtx.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 inspected opCtx.ast.where, which a sibling middleware (plugin-sharing) may have already had an owner_id owner-match composed into — and the two middlewares' registration order is not contractually guaranteed. On an object whose owner_id is FLS-hidden, that could 403 a legitimate bulk write purely because the injected filter named the field. The guard now inspects opCtx.options.where (the caller's untouched predicate) for update/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 readRowById helper (fail-closed: missing engine / null id / thrown read → null, which always denies) now backs the provenance gates, and a memoized getCallerPreImage collapses the owner-anchor echo check (3.5) and the RLS check post-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-derived publicFormGrant, 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 could POST owner_id=<victim> (or organization_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 publicFormGrant branch in @objectstack/plugin-security strips id / owner_id / organization_id / tenant_id / audit columns / soft-delete state / __search from 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_id section 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, and GET /forms/:slug/lookup/:field refuses a publicPicker declared on a managed anchor (which would have opened anonymous sys_user search through owner_id).

    Authenticated writes are unaffected — this is the anonymous-surface rule only; owner_id transfer semantics for signed-in callers stay governed by the transfer grant (#3004 / PR #3018).

  • f531a26: fix(security): enforce referenced-object RLS/FLS on $expand (#2850)

    expandRelatedRecords resolved 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 the id $in [...] batch (one query per level, no N+1). The sub-read carries a server-set __expandRead marker: 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 scope on boot (#2909 T3). scope is 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/config values while rows are now stamped (and boot-normalized to) platform/package, so platform/package-managed positions — including the everyone/guest audience 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 everyone anchor AFTER the anchor is seeded. The baseline auto-bind (ADR-0090 D5) ran earlier in runBootstrap than bootstrapBuiltinRoles, which creates the everyone position — so the everyone lookup returned nothing and the app's isDefault set was never bound, leaving a fresh deploy's everyone empty (personas silently degraded) and a redundant sys_audience_binding_suggestion filed for the same set. The auto-bind now runs after bootstrapBuiltinRoles and before syncAudienceBindingSuggestions, 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

15.0.0

Major Changes

  • 0fcef9b: ADR-0095 D1: tenant isolation is now Layer 0 — an independent, always-first, AND-composed filter (tenant-layer.ts), no longer a wildcard tenant_isolation RLS policy OR-merged with business RLS. The effective row filter is Layer0(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; single mode 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 baseline member_default no longer permits it.
    • (c) Global-catalog objects visible to members. On a tenancy.enabled:false object, members were scoped by a phantom organization_id filter (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_isolation is retired from the seeded organization_admin / member_default / viewer_readonly sets; the _self / _org identity-table carve-outs and owner_only_writes/deletes are unchanged. Customized seeded sets keep their overlays (ADR-0094). The driver-level applyTenantScope seam is untouched. See ADR-0095 and framework#2936 (the extractTargetField == blind spot this exposes, tracked separately).

  • 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 决策 (computeInsertTenantCheckFiltercomputeLayeredRlsFilterlayer0)做校验 —— 一个显式提供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 或 bulk update{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 决策(computeWriteTenantCheckFiltercomputeLayeredRlsFilterlayer0)。一个显式提供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_ADMIN posture 落进 resolve-authz-context.tsctx.posture,但该字段未被 plumb 进 enforcement 中间件收到的 ExecutionContext(rest-server 与 runtime dispatcher 都丢弃了它),直接消费会静默 no-op。改用平台专属能力探针,读的是 enforcement 已用的同一套 permission sets,覆盖所有入口,且天然 fail-safe。

    矩阵门:authz-matrix-gate.test.ts 更新 private_obj.org_admin 格(read null{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

Minor Changes

  • 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_by and sys_position.managed_by converted from free text to a constrained select matching sys_capability (options platform / 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 as managed_by: 'admin' (was 'user'). Declared package sets ('package') and platform capabilities ('platform') were already canonical.
    • sys_position list views (active / default_positions / custom / all_positions) now surface the managed_by column, 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 idempotent kernel:ready reconciler (normalizeManagedByVocab) rewrites the residual legacy values (systemplatform, configpackage, useradmin) on existing sys_position / sys_permission_set rows.
    • i18n: managed_by field + option labels (platform / package / admin) added for sys_capability / sys_permission_set / sys_position across 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 defineCapability factory and a stack's capabilities array, instead of relying on the implicit "derive an untitled capability from whatever a permission set references in systemPermissions[]" back-door.

    • @objectstack/spec: new defineCapability / CapabilityDeclarationSchema ({ name, label?, description?, scope, packageId? }) and a capabilities field on the stack definition.
    • @objectstack/plugin-security: new bootstrapDeclaredCapabilities seeds declared capabilities into sys_capability with managed_by:'package' + package_id provenance (new package_id field 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-systemPermissions path still runs for back-compat but now skips any explicitly-declared name so it can't clobber authored metadata.
    • @objectstack/runtime: stack-declared capabilities are registered into the metadata registry (type capability) so the boot seeder can read them.
    • @objectstack/lint: validateCapabilityReferences treats stack.capabilities names as a known capability source.

    A capability is not a contract: DEFINE it (defineCapability), GRANT it (systemPermissions), REQUIRE it (requiredPermissions) — no inputs. Aligns with ADR-0094 D5 (retire implicit managed_by-guessing back-doors).

  • a581a65: feat(plugin-security): C2-β — explain 引擎 record 粒度行级归因 (#2920)

    explain(principal, object, operation, recordId?) 现支持记录级解释。透传 recordId 时,引擎在对象级流水线之上叠加行级归因,全部复用 enforcement 同一批函数(explained-by-construction):

    • tenant_isolation Layer 0:作为永远最先的层被 prepend;每层打上 kernelTierlayer_0_tenant vs layer_1_business),可区分「租户墙挡的」还是「业务 RLS 挡的」。
    • 每层 record 归因(tenant / owd_baseline / sharing / rls):outcome(admitted/excluded/not_evaluated)、有效 rowFiltermatchesRecord(用 @objectstack/formulamatchesFilterCondition 对同一条 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

Patch Changes

  • 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 in computeLayeredRlsFilter) only matched the legacy single-= / IN shape. It returned null for canonical CEL == (field == …), which is how real seeds and business policies author equality. A null target 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 ==-form organization_id policy on a tenancy.enabled:false object 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 return null (conservative keep), matching prior behavior for any unmatched shape.

    Behavior delta (fail-closed strengthening, same effective visibility): the wildcard owner_only_writes / owner_only_deletes seed policies (created_by == current_user.id) now correctly fail closed on an object that lacks a created_by column (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 carry created_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 targets organization_id on 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_id visibility load-bearing — it is the readScope depth anchor, so a unit/unit_and_below holder sees the owner set rooted at that BU (and, for unit_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_id to 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.tshasPlatformAdminGrant:要求非作用域 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_accessorganization_id == null 的 active user grant)计算并挂出 hasPlatformAdminGrantderivePosture 以此 + 投影出的 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)。

    新增 assertSystemRowWriteGatepackages/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_byuser/∅(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 链接表而非定义对象。

  • 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

14.8.0

Minor Changes

  • f0acf25: Surface a customized flag on sys_permission_set so Setup can tell — at a glance — which packaged permission sets have an environment overlay (ADR-0094).

    • The env projector stamps customized: true on a managed_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_set and to the "All" Setup list view (alongside managed_by), so a packaged-but-customized set is visible without opening the Studio layered diff.
  • 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 the sys_permission_set record while preserving its managed_by:'package' + package_id provenance, 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 permission the one type whose declared allowOrgOverride: true is 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.

  • 1dede32: Make the sys_permission_set data 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: new registerMutationProjector(type, fn) — an awaited, best-effort per-type hook invoked after persistence inside saveMetaItem / publishMetaItem / deleteMetaItem, so a derived data-plane read-model is already consistent when the write returns (outcome surfaced as projectionApplied on the response). Complements the fire-and-forget onMetadataMutation listeners.
    • @objectstack/plugin-security: every non-system data-door write on sys_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-memory permission entry, 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): upsertEnvPermissionSet now lives in permission-set-projection.js (still re-exported from the package root) and creates missing records; projectEnvPermissionOnMutation / subscribeEnvPermissionProjection are replaced by projectPermissionMutation / registerPermissionSetProjection.

Patch Changes

  • a199626: claimSeedOwnership now skips external (federated) objects — those with an external remote-table binding (ADR-0015) — the same way it already skips managedBy and sys_* objects.

    The seed-ownership backfill walks every registered object that exposes an owner_id column and re-owns its unowned rows to the first admin. Federated objects get owner_id auto-injected into their schema, so they passed the filter and the backfill issued select id from <remote_table> where owner_id is null against a read-only remote datasource whose table may not be provisioned yet at boot — producing startup errors like Find 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

14.7.0

Patch Changes

  • 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 a tenancy service — the single source of truth for tenancy mode (mode, isolationActive, requested, degraded, defaultOrgId()). It derives isolationActive from the presence of the org-scoping service, so the enterprise @objectstack/organizations package lights it up with no change. SecurityPlugin's RLS-strip gate and /auth/config (features.multiOrgEnabled, new features.degradedTenancy) now consume it instead of re-deriving the fact.

    Fail-fast on degraded tenancy (@objectstack/cli, ADR-0093 D5). OS_MULTI_ORG_ENABLED=true without a working @objectstack/organizations now 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=1 boots 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's user.create.after hook 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 new membershipPolicy: 'auto' | 'invite-only' auth option (default auto), and binds only to an unambiguous target org (single-org default; multi-org binds nothing). A bounded, idempotent kernel:ready backfill covers pre-existing member-less users in single-org/auto deployments (OS_SKIP_MEMBERSHIP_BACKFILL=1 to 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

14.6.0

Patch Changes

  • 6e2b8ae: Project env-scope permission-set edits onto the sys_permission_set record (#2857).

    A sys_permission_set has 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-scope save('permission', …) updated the sys_metadata overlay (and the layered read) but left the sys_permission_set record stale (split-brain). Enforcement reads the authoritative metadata so access stayed correct, but the admin surface showed old values.

    Adds the environment door: subscribeEnvPermissionProjection hooks the protocol's post-persistence onMetadataMutation choke point; on an active (non-draft) permission save it re-reads the fresh effective body via the layered read (the boot-cached metadata registry would return a stale declared body) and upsertEnvPermissionSet projects the six facets onto the record. Ownership is decided by the record's managed_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 existing authored-translation-sync mutation-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

14.5.0

Minor Changes

  • f70eb2c: ADR-0090 D10 — agent/service intersection runtime. When a request's principal acts onBehalfOf a 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. Previously principalKind:'agent' / onBehalfOf was 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 using pre-image on by-id writes, the check post-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 onBehalfOf is 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 onBehalfOf shape 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.

  • 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 human sub, 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 to principalKind:'agent' with onBehalfOf:{ 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. userId stays the human so owner-stamping and current_user.* RLS resolve to them; the user-derived systemPermissions are cleared so a cap-gated action can't ride the user's capabilities. A token without a client stays a human principal.
    • 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). An agent principal 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:read token 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:read agent acting for a member who owns a record can read it but cannot edit or create; a data:write agent for the same user can. Producer mapping unit-tested in @objectstack/runtime; enforcement dogfooded against the served engine (showcase-agent-scope-ceiling).

Patch Changes

  • 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 from x-user-id / x-tenant-id / x-permissions request headers (the route default), and setMany performed no permission check — so on a standard os serve --server deployment (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. SettingsServicePlugin now 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 / writePermission are 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 new enforced flag 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_FORBIDDEN is returned when the capability is missing.

    setup.write capability now real. Enforcing the manifests' declared writePermission surfaced a modeling gap: setup.write (the write counterpart to setup.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 by admin_full_access and organization_admin, alongside setup.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.

  • 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

14.4.0

Minor Changes

  • 82e745e: ADR-0091 L1 — grant validity windows: effective-dated assignments, resolution-time filtering, explain expired state, authoring lint.

    • plugin-security (objects): sys_user_position and sys_user_permission_set gain 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), and resolveAuthzContext now filters BOTH grant tables through it (D2, fail-closed — an expired unscoped admin_full_access grant no longer derives platform_admin). Present-but-unparseable bounds fail closed.
    • plugin-security (explain): buildContextForUser applies the same filter and returns expiredGrants; the principal layer reports the dedicated "held until … — expired" contributor state so "why did access disappear" is self-answering. Spec ExplainLayerSchema contributors gain an optional state: 'active' | 'expired'.
    • plugin-sharing: PositionGraphService.expandPositionUsers filters 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 (a valid_until in the past, or unparseable, is a grant that can never resolve) and security-delegation-missing-reason (a delegated_from row without reason breaks the D3 dual audit). Also re-exported the missing SECURITY_MASTER_DETAIL_UNGRANTED constant.

    No background job is involved anywhere — per ADR-0049, an expired grant simply stops resolving, in every edition.

  • f3035bd: ADR-0091 L2 — delegation of duty (职务代理): self-service, time-boxed position delegation without administration.

    • spec: PositionSchema.delegatable (default false) + the sys_position.delegatable field. A position opts in to being self-service delegated.
    • plugin-security (D12 gate): a new self-service branch — a non-admin holder of a delegatable position may insert a sys_user_position row assigning it to a delegate, WITHOUT any adminScope, iff the row is a well-formed delegation: delegated_from = the writer (you delegate your OWN authority), a mandatory valid_until in the future and within the 30-day ceiling, a mandatory reason, 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. A delegatable position that distributes an adminScope-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): buildContextForUser surfaces delegation provenance; the principal layer attributes a delegated position "via delegation from X, until Y".
    • liveness / proof (ADR-0054): position.delegatable is 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 at valid_until via the real resolver.

    Break-glass activation and recertification campaigns stay enterprise (D7); their community shapes are the L1 substrate.

Patch Changes

  • 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

14.3.0

Minor Changes

  • 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/tree now ENFORCE the ADR-0046 §6.7 audience model (ADR-0049 — no unenforced security properties): anonymous callers see only public books/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 to org; orphan rendering never inherits public). Gated evaluation fails CLOSED when holdings cannot be resolved. doc/book single-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/AudienceBook types). BREAKING but ships as a minor per 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_REGISTRY keys role/profile are gone — position is the registered form (the position type had LOST its form layout in the P1 rename); EnvironmentArtifactMetadataSchema declares positions instead of retired roles/profiles.
    • plugin-security: the security service exposes resolvePermissionSetNames(ctx) — the same resolution as data-plane enforcement, for the docs gate.
    • metadata: artifact ingestion maps positions → 'position' (the stale roles → 'role' mapping matched nothing since the P1 rename, silently dropping compiled positions from metadata registration).
    • lint: books join the D3 role-word scan (their audience is a permission-model reference now), and a new advisory rule security-book-audience-unknown-set flags 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 retired role/profile groups, with a vocabulary regression test.
  • 8f0b9df: fix(cli,plugin-security): os meta resync to 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: bootstrapPlatformAdmin skipped any row that already existed and never wrote the shipped declaration back. So editing a default set's source, recompiling, and restarting os dev without --fresh left the runtime serving the OLD value — silently, because the runtime authz resolver hydrates permission sets from the sys_permission_set row (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-admin admin_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 bootstrapDeclaredPermissions relies on), so this is not switched to a blind upsert. Instead:

    • bootstrapPlatformAdmin gains a resync option. Default boot behavior is unchanged (insert-once). Under resync, an existing row is reconciled to the shipped dist only when the platform still owns it (managed_by absent 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 resync command 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 --fresh wipe. Gated behind a confirmation prompt (--yes to skip; --json for scripting).

    Prod boot is unaffected; the fix is entirely opt-in via the new command.

Patch Changes

  • 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

14.2.0

Minor Changes

  • ac8f029: Two ADR-0090 D5 closures (#2752, #2753):

    GET /me/apps sources the engine registry. Stack apps are registered into the engine registry (runtime AppPlugin), not the metadata service — metadata.list('app') returned [] for every principal, leaving tabPermissions and AppSchema.requiredPermissions with no enforced consumer. The endpoint now reads registry.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 everyone anchor. member_default carried allowDelete on its '*' grant — an anchor-forbidden bit — so bootstrap refused the everyone binding 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_default drops allowDelete from 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 new me-apps-and-everyone-baseline dogfood).

  • 4ab9958: Position assignment panels as pure SDUI (ADR-0090 follow-through).

    • RecordRelatedListProps gains relationshipValueField (default 'id'): which parent-record field the junction's relationshipField stores — the generic affordance for name-keyed junctions (sys_user_position.position stores sys_position.name). Used for both the list filter and the Add-picker's parent-side value.
    • sys_user detail page gains a Positions tab (assign positions to a user; Add picker stores the position machine name via valueField: 'name'; the D12 delegated-admin gate's denials surface in the dialog).
    • New sys_position detail page (shipped by plugin-security): Holders (name-keyed via relationshipValueField: '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_list Add-picker and relationshipValueField support land in objectui alongside the ^14 alignment; with older renderers these tabs degrade to read-only lists.

Patch Changes

  • 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

14.1.0

Patch Changes

  • 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

14.0.0

Minor Changes

  • ac08698: ADR-0090 D6 — the explain engine gets its REST face (#2696).

    @objectstack/rest: new GET/POST /api/v1/security/explain (object/operation/userId, validated against the spec's ExplainRequestSchema) delegating to the security service's explain(request, callerContext) — the same code paths the enforcement middleware runs, so the returned ExplainDecision is explained by construction. The route is authenticated-only (401 even on requireAuth=false deployments), returns 501 when no security service exposes explain, and maps the service's PermissionDeniedError to 403. Registered on scoped (/environments/:environmentId) and unscoped base paths; the env kernel's own security service is preferred, with a new host-kernel securityServiceProvider fallback wired by the REST plugin.

    @objectstack/plugin-security: explainAccessForCaller now honors delegated administration (D12) — explaining ANOTHER user is authorized by manage_users or a delegated adminScope whose business-unit subtree covers the target user (new DelegatedAdminGate.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: true is an install-time SUGGESTION to bind the set to the built-in everyone position — 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: new sys_audience_binding_suggestion system object (read-only over the data API; unique per package × set × anchor) plus a convergent reconciler (syncAudienceBindingSuggestions) that reads every declared isDefault set — boot-declared stack metadata AND installed package manifests, so a runtime POST /api/v1/packages install is visible immediately — and keeps the table honest: undeclared → pending row pruned, bound out-of-band → marked confirmed (observed). The security service gains listAudienceBindingSuggestions / confirmAudienceBindingSuggestion / dismissAudienceBindingSuggestion, all pre-gated on tenant-level admin (ADR-0066 superuser wildcard — anchors stay tenant-level only per D12). Confirm writes the sys_position_permission_set row with the caller's execution context, so the D5/D9 audience-anchor gate (no high-privilege set on everyone/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/rest and @objectstack/runtime: the HTTP surface, registered on both API layers (the RestServer that objectstack 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: deletePackage gains an uninstall-cleanup seam — the exact mirror of the publish materializer: domain plugins register named cleanups via registerUninstallCleanup(name, fn) and every cleanup runs with the uninstalled package id, its outcome reported on the new cleanups array of the response (a failed revocation is visible, never silent). deletePackage also 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 the security.package-permissions cleanup — deletes the package's own sys_permission_set rows (managed_by: 'package' + matching package_id only; env-authored and foreign-package rows are never touched, ADR-0086 D4), their sys_position_permission_set / sys_user_permission_set bindings (bindings first, so no dangling grants), and the package's sys_audience_binding_suggestion rows (a reinstall re-prompts fresh). Also fixes the engine-call signature in the suggestion module: find/delete read context from 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 through protocol.deletePackage — one uninstall semantic instead of a bare sys_packages row delete — removing the package's metadata, durable record, registry entry, and running the cleanups; the response carries deletedCount + cleanups. A version-scoped delete keeps the narrow durable-registry semantics.

Patch Changes

  • 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): typed definePosition/ definePermissionSet/defineSharingRule factories; six flat positions (the stale pre-D3 parent fields 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), the isDefault everyone-suggestion (incl. personal-data grants on the private-OWD note object), a guest-safe set for the guest anchor (D9), and a delegated-administration adminScope bounded to a seeded sys_business_unit subtree (D12). Objects gain externalSharingModel dials (D11). A committed access-matrix.json opts 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 enforced unit_and_subordinates sharing-rule recipient instead.

    @objectstack/spec: defineStack strict 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 missing security/explain.mdx) are regenerated.

    Docs: protocol/objectql/security.mdx rewritten to the v2 model (no profiles, positions, canonical OWD four + D1 private default + externalSharingModel, position-scoped RLS, enforced sharing recipients); isProfile scrubbed from every authoring example; the dead /docs/references/identity/role link fixed; implementation-status and plugin READMEs aligned. Remaining rename misses are tracked in #2722 (RLSUserContext.role), #2723 (portal profiles), #2724 (sys_record_share role enum).

  • d0531c4: Setup → Access Control nav: the sys_position entry is renamed nav_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-sha pin 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_position is now labeled "Positions" (was still "Roles" after the ADR-0090 D3 rename) — nav_rolesnav_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

13.0.0

Major Changes

  • 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_rolesys_position, sys_user_rolesys_user_position (field roleposition), sys_role_permission_setsys_position_permission_set (field role_idposition_id); RoleSchema/defineRolePositionSchema/definePosition with no parent (positions are flat; hierarchy lives on the business-unit tree).
    • ExecutionContext.roles[]positions[]; the EvalUser/CEL contract current_user.rolescurrent_user.positions (formula validators updated); stack property roles:positions:; metadata kinds role/profileposition (profile kind removed).
    • isProfile removed from PermissionSetSchema (ADR-0090 D2); isDefault narrows to an install-time suggestion; appDefaultProfileNameappDefaultPermissionSetName (isDefault-only).
    • OWD enum drops legacy aliases read/read_write/full; new optional externalSharingModel (external dial, private default) lands as P1 spec shape (ADR-0090 D11).
    • Secure default (D1): a custom object with an owner field and NO sharingModel now resolves private (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: roleposition (expanded via sys_user_position ∪ the better-auth membership transition source); role_and_subordinates removed — unit_and_subordinates now 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 into os compile (errors gate the build) and os lint. Rules, each with a failing fixture: security-owd-unset (custom object with no sharingModel — the objectui#2348 leave_request shape), security-owd-alias (retired D4 alias values, with fix-it), security-external-wider-than-internal (D11 external ≤ internal), security-wildcard-vama ('*' + View/Modify All outside the platform admin set, ADR-0066), security-anchor-high-privilege (an isDefault/everyone-suggested set carrying anchor-forbidden bits), security-role-word (D3 vocabulary freeze in security identifiers/labels; ARIA/page roles exempt), and advisory security-private-no-readscope.

    D12 — delegated administration (@objectstack/plugin-security DelegatedAdminGate). PermissionSetSchema.adminScope (new in spec, persisted as sys_permission_set.admin_scope) declares WHERE (a sys_business_unit subtree), WHAT (manageAssignments / manageBindings / authorEnvironmentSets), and WHICH sets a delegate may hand out (assignablePermissionSets allowlist). Writes to sys_user_position, sys_position_permission_set, sys_user_permission_set, and sys_permission_set are 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_by audit-stamped; everyone else (including holders of plain CRUD on RBAC tables) is denied. Granting or authoring a set that itself carries an adminScope requires a held scope that STRICTLY contains it. The everyone/guest anchors 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_id lands with its three consumers scoped: D12 delegation boundary (enforced here), audit fact, and the depth-anchor contract for enterprise hierarchy-scope-resolver implementations (documented on IHierarchyScopeResolver).

    D9 tier tightening. describeHighPrivilegeBits moved to @objectstack/spec/security (re-exported from plugin-security) alongside new describeAnchorForbiddenBits: guest bindings 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 — SysRoleSysPosition, SysUserRoleSysUserPosition, SysRolePermissionSetSysPositionPermissionSet (no aliases, pre-launch one-step rename). sys_position row actions/list views renamed (activate_position, …), labels relabeled Role→Position. Non-tenant-admin writes to the RBAC link tables without an adminScope are now denied (previously any CRUD grant on those tables sufficed).

    BREAKING (@objectstack/platform-objects): sys_business_unit_member.role_in_business_unitfunction_in_business_unit (D3 reserved-word sweep; values member/lead/deputy unchanged).

Minor Changes

  • 01917c2: ADR-0090 P2 — audience anchors: everyone/guest builtin positions.

    • EVERYONE_POSITION / GUEST_POSITION constants in @objectstack/spec; both anchors seeded (system-managed) alongside the builtin identity names.
    • Every authenticated principal implicitly holds everyone in ctx.positions, so sets bound to it resolve as ordinary position-bound grants — ADDITIVE. The fallback CLIFF is abolished: the configured baseline (fallbackPermissionSet, default member_default) now applies in addition to explicit grants instead of only when the user had none, and is also seeded as an everyone binding (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_set writes that would bind a high-privilege set (VAMA, delete/purge/transfer, system permissions, '*' wildcard) to everyone/guest are rejected at the data layer, unconditionally (describeHighPrivilegeBits predicate 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). explainAccess is "explained by construction": it calls the SAME permission-set resolution, evaluator, FLS mask, and RLS composition the enforcement middleware calls (injected from SecurityPlugin), so the report cannot drift from enforcement. Exposed on the security kernel service as explain(request, callerContext); explaining another user requires manage_users (the target's context is reconstructed from sys_user_position / sys_user_permission_set with everyone-anchor semantics via buildContextForUser).

    Access-matrix snapshot gate (@objectstack/lint + os compile). buildAccessMatrix(stack) derives the (permission set × object) capability matrix purely from metadata; diffAccessMatrix renders semantic review lines ("'crm_admin' gains delete on 'crm_lead'", depth changes, OWD swings, entry add/remove). os compile gains an opt-in gate: with access-matrix.json committed 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 for examples/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.requiredPermissions accepts either the original string[] (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 folds transfer/restore into update and purge into delete, 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 an all bucket 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.

Patch Changes

  • 466adf6: Author-time capability-reference lint (ADR-0066 ⑨) — os validate / os lint now warn when a requiredPermissions names a capability that is registered nowhere.

    requiredPermissions (on objects, fields, apps, actions) is a free string, so a typo like mange_users is schema-valid and fails closed at runtime (the caller is denied) — safe, but silent. The new validateCapabilityReferences rule (@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's bootstrapSystemCapabilities also 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_capability row 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. systemPermissions itself is never flagged — it is the declaration side, and a package legitimately introduces new capabilities there. The object case also understands the per-operation requiredPermissions map form (ADR-0066 ⑤) and points a finding at the exact operation slice.

  • 799b285: Fix field-level-security read leak on mutation responses. The security middleware only masked read-protected fields on find/findOne results, so a caller with edit-but-not-field-read could insert/update a 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 covers insert/update results, 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

12.6.0

Patch Changes

  • 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

12.5.0

Patch Changes

  • Updated dependencies [8b3d363]
    • @objectstack/spec@12.5.0
    • @objectstack/core@12.5.0
    • @objectstack/formula@12.5.0
    • @objectstack/platform-objects@12.5.0

12.4.0

Patch Changes

  • Updated dependencies [60dc3ba]
    • @objectstack/spec@12.4.0
    • @objectstack/core@12.4.0
    • @objectstack/formula@12.4.0
    • @objectstack/platform-objects@12.4.0

12.3.0

Patch Changes

  • Updated dependencies [e7eceec]
    • @objectstack/spec@12.3.0
    • @objectstack/core@12.3.0
    • @objectstack/formula@12.3.0
    • @objectstack/platform-objects@12.3.0

12.2.0

Minor Changes

  • 3962023: feat(spec,security): make ambiguous nav landings unrepresentable + close the field-permission filter oracle (objectui#2251, objectui ADR-0055).

    spec — ObjectNavItem target exclusivity. NavigationItemSchema now rejects an object nav item that combines filters with recordId or viewName (custom issue on filters with the fix in the message). Runtime precedence would silently ignore the extras — a stale recordId hijacking a configured filters slice — so the ambiguous combination is now unwritable (ADR-0053 correct-by-construction). FROM { filters, viewName } / { filters, recordId } TO exactly one landing field; the legacy recordId + viewName combination stays tolerated (documented: viewName is ignored). filters shipped in the same unreleased minor, so no released metadata is affected.

    plugin-security — field-level predicate guard. FieldMasker strips 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 — probe salary >= X even though the column is masked). The security middleware now rejects (403 PermissionDeniedError, reason: 'field_predicate_denied') any caller query whose where / orderBy / groupBy / having / aggregations / windowFunctions reference 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 $and branch widens results and re-opens the oracle. New exports: assertReadableQueryFields, collectQueryFields, collectConditionFields.

Patch Changes

  • 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

12.1.0

Patch Changes

  • Updated dependencies [93e6d02]
    • @objectstack/spec@12.1.0
    • @objectstack/core@12.1.0
    • @objectstack/formula@12.1.0
    • @objectstack/platform-objects@12.1.0

12.0.0

Minor Changes

  • 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). ObjectStackProtocolImplementation gains 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 as materializeApplied (best-effort, never thrown — same contract as seedApplied). promoteDraft now returns the draft's packageId so the materializer can stamp the owning package. plugin-security registers a permission materializer that upserts the published set into sys_permission_set with managed_by:'package' + package_id — so a set authored through the studio package door (saved as a permission draft, 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 with bootstrapDeclaredPermissions (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 a sys_permission_set row with managed_by:'package', and refuses an insert that forges managed_by:'package'. The gate fails closed regardless of the caller's grants (a platform admin with modifyAllRecords is blocked just the same), so it is a real data-layer boundary rather than a UI hint. System/boot writes carry isSystem and bypass the whole middleware, so the boot seeder and the publish materializer are unaffected. Env-authored sets (managed_by user/platform or absent) stay freely editable through the admin door — the two doors never overwrite each other.

  • 7c09621: feat(security): pre-map transfer/restore/purge to their RBAC bits (#1883)

    The permission evaluator now maps the destructive record-lifecycle operations to their spec permission bits (transferallowTransfer, restoreallowRestore, purgeallowPurge) and extends the modifyAllRecords super-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 provenance managedBy: 'package' | 'platform' | 'user' on the existing metadata-persistence axis. Persisted on sys_permission_set as package_id / managed_by (new columns + package_id index).
    • Seeding (ADR-0086 D5): new bootstrapDeclaredPermissions — the sibling of bootstrapDeclaredRoles — materializes stack.permissions into sys_permission_set at boot with managed_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-authored platform/user/legacy rows are never clobbered. Closes the ADR-0078 inert-metadata violation for stack.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.

Patch Changes

  • 48ad533: fix(security): surface swallowed permission-set resolution failures (#2565)

    PermissionEvaluator.resolvePermissionSets swallowed metadata list() and sys_permission_set dbLoader 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 one warn per 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

11.10.0

Patch Changes

  • 6a9397e: Retire the deprecated compactLayout alias for highlightFields (framework#2536, closes the ADR-0085 deprecation window).

    • ObjectSchema no longer declares compactLayout: create() rejects it like any unknown key; lenient parse() strips it (no silent aliasing).
    • The parse-time alias AND the highlightFields → compactLayout back-fill transition mirror are removed from normalizeSemanticRoleAliases. 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: undefined in 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 compactLayout will now fail create() 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

11.9.0

Patch Changes

  • Updated dependencies [d3595d9]
    • @objectstack/spec@11.9.0
    • @objectstack/core@11.9.0
    • @objectstack/formula@11.9.0
    • @objectstack/platform-objects@11.9.0

11.8.0

Patch Changes

  • 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

11.7.0

Patch Changes

  • Updated dependencies [5178906]
    • @objectstack/spec@11.7.0
    • @objectstack/platform-objects@11.7.0
    • @objectstack/core@11.7.0
    • @objectstack/formula@11.7.0

11.6.0

Patch Changes

  • @objectstack/spec@11.6.0
  • @objectstack/core@11.6.0
  • @objectstack/formula@11.6.0
  • @objectstack/platform-objects@11.6.0

11.5.0

Patch Changes

  • 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

11.4.0

Patch Changes

  • 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

11.3.0

Patch Changes

  • 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

11.2.0

Patch Changes

  • 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

11.1.0

Patch Changes

  • 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, so organization_id is never stamped and member_default's wildcard tenant_isolation RLS 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:false objects, so a platform admin's viewAllRecords sees all identity rows env-wide. This is admin-only: non-admins never trigger the bypass — their _self carve-outs / tenant_isolation still 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 tenancy changes (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

11.0.0

Patch Changes

  • 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

10.3.0

Patch Changes

  • @objectstack/spec@10.3.0
  • @objectstack/core@10.3.0
  • @objectstack/formula@10.3.0
  • @objectstack/platform-objects@10.3.0

10.2.0

Patch Changes

  • Updated dependencies [b496498]
    • @objectstack/spec@10.2.0
    • @objectstack/core@10.2.0
    • @objectstack/formula@10.2.0
    • @objectstack/platform-objects@10.2.0

10.1.0

Patch Changes

  • 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

10.0.0

Minor Changes

  • e16f2a8: BREAKING: the system object sys_department is renamed to sys_business_unit — object + member table (sys_department_membersys_business_unit_member), fields, and i18n — with no compatibility alias. Any deployment holding sys_department rows, or metadata that references the object by name (lookups, list views, queries, sharing/approval scopes), must migrate to sys_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), renames sys_departmentsys_business_unit (no aliases — see BREAKING above), introduces the platform-owned sys_user_position assignment, and seeds stack-declared roles/sharingRules into sys_position/sys_sharing_rule at boot (closes #2077). Hierarchy-relative scopes are delegated to a pluggable IHierarchyScopeResolver (open edition fails closed to owner-only; defineStack errors without requires: ['hierarchy-security']). Also fixes a latent over-grant where engine.find({ filter }) was ignored (driver reads where) — normalized filterwhere in 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-memory matchesFilterCondition backend (one AST, three backends). plugin-security (RLS using, via a SQL bridge) and plugin-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 RLS check clause 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 the RLS.ownerPolicy/tenantPolicy/allowAllPolicy helper factories) from the legacy SQL-ish form (=, IN (...)) to pure CEL (==, in), so authors and AI learn ONE expression language. The sqlPredicateToCel bridge 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.

Patch Changes

  • 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

9.11.0

Minor Changes

  • fa8964d: feat(security): RLS predicates that won't compile are surfaced, not silently dropped (ADR-0056 D4)

    The RLS compiler previously dropped any using/check it 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 exports isSupportedRlsExpression(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: true on a permission set — instead of always inheriting the built-in member_default. The SecurityPlugin resolves the fallback from the isDefault profile when no explicit fallbackPermissionSet is configured (falling back to member_default when none is declared — non-breaking). This is the foundation for SSO/JIT provisioning (mapping IdP claims → a declared default profile). Proven by the showcase-default-profile dogfood test: a sign-up governed by a custom default that grants only showcase_announcement can read it but is denied showcase_private_note (which the member_default wildcard 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_portal profile. The form-submit route derives a narrow publicFormGrant: { 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-configured guest_portal, scoped to exactly the declared object (the field allow-list is still enforced at the route; guest_portal/anonymous are kept on the context for back-compat with guest-detection hooks). It is the prerequisite that unblocks the eventual requireAuth default flip, and generalizes the platform principle "public access = declared + runtime-derived scoped grant" (the same shape share-links already use). Proven by form-self-auth dogfood (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-security exports a new appDefaultProfileName(permissions) helper that extracts the first isProfile && isDefault profile name from a stack; @objectstack/cli (serve.ts) passes it as the SecurityPlugin fallbackPermissionSet (undefined → built-in member_default preserved, so apps that declare no default are unaffected).

    The showcase gains a working web-to-lead public form (showcase_inquiry + an allowAnonymous FormView authorized by the declaration-derived publicFormGrant, no guest_portal profile) 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_parent added to the authorable object.sharingModel enum.
    • @objectstack/plugin-security: reads inject masterFK 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 synthesisderiveCrudCases no 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. Honest blocked verdicts 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.email in RLS owner policies

    RLS using predicates can now reference current_user.email — a unique, human-readable, seedable owner anchor (owner = current_user.email). Previously the RLS compiler resolved only current_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 bounded sys_user fallback for the API-key path) and threaded onto the ExecutionContext in both identity resolvers — the REST data path (rest-server) and the dispatcher path (resolve-execution-context).

    Display name is 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_invoice carries an owner email + 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 == and current_user.name, neither of which the compiler accepts) to = current_user.email.

Patch Changes

  • 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

9.10.0

Minor Changes

  • 1f88fd9: Converge the RLS contract with the reference compiler, and wire §7.3.1 dynamic membership.

    • spec (docs): narrow rls.zod.ts to the four expression forms the compiler actually implements — field = current_user.<prop>, field = 'literal', field IN (current_user.<array>), and 1 = 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 @example policies, and documented the fail-closed behaviour explicitly.
    • spec (schema): ExecutionContext gains rlsMembership?: 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 via field IN (current_user.<key>) without subquery support. Generalizes the previously hard-coded org_user_ids.
    • plugin-security: RLSCompiler.compileFilter merges rlsMembership keys into the user context (arrays only, never clobbering the named id/organization_id/roles/org_user_ids fields), so §7.3.1 hierarchy- and sharing-based policies compile. compileExpression now recognizes 1 = 1 as always-true (empty filter), making RLS.allowAllPolicy grant access instead of silently failing closed. Missing/empty membership sets still fail closed.
  • e2b5324: feat(ownership): auto-provision a canonical owner_id and hand seeded records to the first admin

    Ownership 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, reassignable owner_id lookup (→ sys_user) on user-authored business objects, alongside the existing tenant/audit fields. Unlike the audit *_by lookups it is NOT readonly — ownership transfers. Withheld for managedBy / sys_* tables and for objects that opt out via ownership: '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 from bootstrapPlatformAdmin right after the first human is promoted to platform admin, transfers ownership of seeded rows (owner_id NULL or usr_system) to that admin. The ownership twin of org-scoping's claimOrphanOrgRows. Idempotent; skips managedBy / sys_*. Authors write plain seed records (no owner_id) and the platform — not the author — performs the handoff, so there is nothing to remember or mistype.

    • usr_system is never minted (runtime + objectql). The seed loader binds os.user to a NULL identity, so celos.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 bootstrapPlatformAdmin against a latent dts typecheck error (defensive read of the untyped description on seed permission sets).

Patch Changes

  • 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

9.9.1

Patch Changes

  • @objectstack/spec@9.9.1
  • @objectstack/core@9.9.1
  • @objectstack/platform-objects@9.9.1

9.9.0

Minor Changes

  • 92d75ca: fix(security): enforce row-level security on by-id writes — close the member-can-edit-others'-records hole (#1985).

    A single-id update/delete goes straight to driver.update(object, id, …) / driver.delete(object, id) and builds no query ast, so the RLS where filter the middleware injects on the read path was never applied to by-id writes. Combined with member_default granting *: { edit, delete } (scoped, by design, via the owner_only_writes/deletes RLS), 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 throws PermissionDeniedError (403). Reuses the existing RLS/tenant machinery, is recursion-safe (a find doesn'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_deletes keyed on owner_id, which author-defined objects almost never declare — so the policy referenced a missing column and computeRlsFilter dropped it (the no-op that made the bypass invisible). Now keyed on created_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 modifyAllRecords or no RLS) are unrestricted. Objects that opt out of audit fields (systemFields.audit: false) have no created_by and now fail closed for member writes (grant modifyAllRecords or 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).

Patch Changes

  • 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

9.8.0

Patch Changes

  • Updated dependencies [97c55b3]
  • Updated dependencies [1b1f490]
    • @objectstack/spec@9.8.0
    • @objectstack/core@9.8.0
    • @objectstack/platform-objects@9.8.0

9.7.0

Patch Changes

  • @objectstack/spec@9.7.0
  • @objectstack/core@9.7.0
  • @objectstack/platform-objects@9.7.0

9.6.0

Patch Changes

  • 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

9.5.1

Patch Changes

  • Updated dependencies [ee72aae]
    • @objectstack/spec@9.5.1
    • @objectstack/core@9.5.1
    • @objectstack/platform-objects@9.5.1

9.5.0

Patch Changes

  • 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

9.4.0

Patch Changes

  • 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

9.3.0

Patch Changes

  • 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

9.2.0

Patch Changes

  • Updated dependencies [2f57b75]
  • Updated dependencies [2f57b75]
    • @objectstack/spec@9.2.0
    • @objectstack/core@9.2.0
    • @objectstack/platform-objects@9.2.0

9.1.0

Patch Changes

  • Updated dependencies [b9062c9]
    • @objectstack/spec@9.1.0
    • @objectstack/core@9.1.0
    • @objectstack/platform-objects@9.1.0

9.0.1

Patch Changes

  • Updated dependencies [1817845]
    • @objectstack/spec@9.0.1
    • @objectstack/core@9.0.1
    • @objectstack/platform-objects@9.0.1

9.0.0

Patch Changes

  • 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

8.0.1

Patch Changes

  • @objectstack/spec@8.0.1
  • @objectstack/core@8.0.1
  • @objectstack/platform-objects@8.0.1

8.0.0

Patch Changes

  • 1e8b680: fix(security): close four P0 launch-readiness findings

    • plugin-auth (P0-1): generateSecret() now throws (fails boot) when no OS_AUTH_SECRET is set and NODE_ENV==='production', instead of silently falling back to a predictable dev-secret-<timestamp> (session forgery). The dev/test fallback is unchanged.
    • plugin-security (P0-2): the permission-resolution catch now fails closed — it logs at ERROR and throws PermissionDeniedError rather than return 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 / $contains operator now escapes LIKE metacharacters (% / _ / \) in the user value and binds an explicit ESCAPE '\', 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.

  • 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

7.9.0

Patch Changes

  • @objectstack/spec@7.9.0
  • @objectstack/core@7.9.0
  • @objectstack/platform-objects@7.9.0

7.8.0

Patch Changes

  • 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

7.7.0

Patch Changes

  • 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

7.6.0

Patch Changes

  • 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

7.5.0

Patch Changes

  • @objectstack/spec@7.5.0
  • @objectstack/core@7.5.0
  • @objectstack/platform-objects@7.5.0

7.4.1

Patch Changes

  • @objectstack/spec@7.4.1
  • @objectstack/core@7.4.1
  • @objectstack/platform-objects@7.4.1

7.4.0

Minor Changes

  • e478e0c: ADR-0029 K2 — security domain ownership (RBAC + sharing) + Setup nav contributions.

    Moves the security objects out of the @objectstack/platform-objects monolith 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) gains sys_position, sys_permission_set, sys_user_permission_set, sys_position_permission_set, and the defaultPermissionSets seed (which its bootstrap-platform-admin already consumes). The RBAC + default-permission-set tests move with them.
    • @objectstack/plugin-sharing gains sys_record_share, sys_sharing_rule, sys_share_link.
    • @objectstack/platform-objects no longer defines/exports any security objects; the /security subpath is now an empty barrel. Runtime is unchanged (both plugins already registered these objects at runtime).

    D7 navigation — the Setup app's group_access_control is now assembled from three sources: plugin-security contributes Roles / Permission Sets (priority 100), plugin-sharing contributes Sharing Rules / Record Shares (priority 200), and platform-objects keeps 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-objects i18n 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.

Patch Changes

  • 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-objects generated 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.ts and a src/translations/ bundle ({locale}.objects.generated.ts + an index.ts barrel), generated with os i18n extract and self-baselined so re-runs preserve translations.
    • Each plugin loads its bundle at runtime on kernel:ready via i18n.loadTranslations (the i18n service is optional — load is best-effort).
      • plugin-webhookssys_webhook, sys_webhook_delivery
      • plugin-approvalssys_approval_request, sys_approval_action
      • plugin-securitysys_position, sys_permission_set, sys_user_permission_set, sys_position_permission_set
      • plugin-sharingsys_record_share, sys_sharing_rule, sys_share_link
    • @objectstack/platform-objects translation 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.

  • 08fbbb4: Fix: the first-boot platform-admin promotion no longer gets stolen by the usr_system seed identity, and the dev seed admin uses fixed, well-known credentials.

    @objectstack/plugin-securitybootstrapPlatformAdmin skips the system user

    5e831dea3 (#1392) added ensureSeedIdentity to the runtime SeedLoader, which upserts a non-loginable system identity (usr_system, role system, system@objectstack.local) to own seeded records — created before the first human sign-up. Because bootstrapPlatformAdmin promoted the earliest-created sys_user, on any app that ships seed data usr_system won the promotion and the real admin login stayed at role: user. Login succeeded but Setup and Studio (gated by setup.access / studio.access on admin_full_access) were invisible — a silent, confusing regression.

    bootstrap-platform-admin.ts now 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 any admin_full_access grant held by usr_system — so a database where it was wrongly promoted self-heals on the next boot.

    @objectstack/clios dev seeds admin@objectos.ai / admin123

    The --admin-email / --admin-password defaults changed from admin@dev.local / admin12345 to the fixed, well-known admin@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

7.3.0

Patch Changes

  • Updated dependencies [5e7c554]
    • @objectstack/spec@7.3.0
    • @objectstack/core@7.3.0
    • @objectstack/platform-objects@7.3.0

7.2.1

Patch Changes

  • @objectstack/spec@7.2.1
  • @objectstack/core@7.2.1
  • @objectstack/platform-objects@7.2.1

7.2.0

Patch Changes

  • @objectstack/spec@7.2.0
  • @objectstack/core@7.2.0
  • @objectstack/platform-objects@7.2.0

7.1.0

Patch Changes

  • Updated dependencies [6228609]
  • Updated dependencies [47a92f4]
    • @objectstack/platform-objects@7.1.0
    • @objectstack/spec@7.1.0
    • @objectstack/core@7.1.0

7.0.0

Major Changes

  • 3a630b6: Split organization-scoping from @objectstack/plugin-security into a new @objectstack/plugin-org-scoping package.

    Per ADR-0002, "tenant" in ObjectStack means physical isolation (one Environment = one database, handled by @objectstack/driver-turso's multi-tenant router). The row-level organization_id scoping that previously lived inside SecurityPlugin is a different concept — logical scoping inside a single DB — and now ships as its own plugin.

    Breaking changes — @objectstack/plugin-security

    • Removed the multiTenant constructor option. SecurityPlugin no longer touches organization_id on insert and no longer registers the sys_organization post-create seed pipeline.
    • Wildcard current_user.organization_id RLS policies in the default permission sets are now stripped UNLESS the new org-scoping service is registered (i.e. unless OrgScopingPlugin is also installed).
    • Removed export cloneTenantSeedData (now exposed as cloneOrgSeedData from @objectstack/plugin-org-scoping).
    • bootstrapPlatformAdmin() no longer accepts a multiTenant flag and no longer auto-creates a default organization — that behavior moved to ensureDefaultOrganization() in the new plugin.

    Migration

    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_TENANT env switch — read by @objectstack/runtime/cloud/ArtifactKernelFactory, @objectstack/plugin-dev, and the objectstack CLI's serve / dev / start commands — automatically registers OrgScopingPlugin when set to true, so projects driven by the CLI need no code changes.

Patch Changes

  • 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

6.9.0

Patch Changes

  • @objectstack/spec@6.9.0
  • @objectstack/core@6.9.0
  • @objectstack/platform-objects@6.9.0

6.8.1

Patch Changes

  • @objectstack/spec@6.8.1
  • @objectstack/core@6.8.1
  • @objectstack/platform-objects@6.8.1

6.8.0

Patch Changes

  • 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

6.7.1

Patch Changes

  • @objectstack/spec@6.7.1
  • @objectstack/core@6.7.1
  • @objectstack/platform-objects@6.7.1

6.7.0

Patch Changes

  • 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

6.6.0

Patch Changes

  • Updated dependencies [a49cfc2]
    • @objectstack/spec@6.6.0
    • @objectstack/core@6.6.0
    • @objectstack/platform-objects@6.6.0

6.5.1

Patch Changes

  • @objectstack/spec@6.5.1
  • @objectstack/core@6.5.1
  • @objectstack/platform-objects@6.5.1

6.5.0

Patch Changes

  • @objectstack/spec@6.5.0
  • @objectstack/core@6.5.0
  • @objectstack/platform-objects@6.5.0

6.4.0

Patch Changes

  • 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

6.3.0

Patch Changes

  • @objectstack/spec@6.3.0
  • @objectstack/core@6.3.0
  • @objectstack/platform-objects@6.3.0

6.2.0

Patch Changes

  • Updated dependencies [b4c74a9]
    • @objectstack/spec@6.2.0
    • @objectstack/core@6.2.0
    • @objectstack/platform-objects@6.2.0

6.1.1

Patch Changes

  • @objectstack/spec@6.1.1
  • @objectstack/core@6.1.1
  • @objectstack/platform-objects@6.1.1

6.1.0

Patch Changes

  • Updated dependencies [93c0589]
    • @objectstack/spec@6.1.0
    • @objectstack/core@6.1.0
    • @objectstack/platform-objects@6.1.0

6.0.0

Patch Changes

  • 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

5.2.0

Patch Changes

  • b806f58: Scope sys_user visibility to fellow organization members.

    The default RLS policy on sys_user was id = 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 a id IN (SELECT user_id FROM sys_member ...) policy isn't expressible.

    This change:

    1. Pre-resolves org_user_ids (the IDs of all users in the active org) into ExecutionContext in all three REST entry-point resolvers (@objectstack/rest, @objectstack/runtime, @objectstack/plugin-hono-server).
    2. Adds the field to ExecutionContextSchema so it survives Zod parsing.
    3. Adds an org_user_ids field to the RLS compiler's user context.
    4. Adds a new sys_user_org_members policy (id IN (current_user.org_user_ids)) to both member_default and viewer_readonly permission sets, alongside the existing sys_user_self policy. 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.

  • 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

5.1.0

Patch Changes

  • Updated dependencies [75f4ee6]
  • Updated dependencies [823d559]
    • @objectstack/spec@5.1.0
    • @objectstack/platform-objects@5.1.0
    • @objectstack/core@5.1.0

5.0.0

Patch Changes

  • Updated dependencies [888a5c1]
  • Updated dependencies [2f9073a]
    • @objectstack/platform-objects@5.0.0
    • @objectstack/spec@5.0.0
    • @objectstack/core@5.0.0

4.2.0

Patch Changes

  • Updated dependencies [2869891]
    • @objectstack/spec@4.2.0
    • @objectstack/core@4.2.0
    • @objectstack/platform-objects@4.2.0

4.1.1

Patch Changes

  • @objectstack/spec@4.1.1
  • @objectstack/core@4.1.1
  • @objectstack/platform-objects@4.1.1

4.1.0

Minor Changes

  • 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 (maskResults on find/findOne). Insert and update operations could target any field — a hand-crafted POST bypassed FLS entirely.

    The middleware now runs FieldMasker.detectForbiddenWrites on every insert / update payload (single record or bulk array) and throws PermissionDeniedError (HTTP 403) when the payload references a field the caller is not permitted to edit. The offending field list is exposed via details.forbiddenFields for actionable client error UI.

    Allow-list semantics: only fields explicitly enumerated in a permission set's fields map 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).

Patch Changes

  • Updated dependencies [2108c30]
  • Updated dependencies [23db640]
    • @objectstack/spec@4.1.0
    • @objectstack/core@4.1.0
    • @objectstack/platform-objects@4.1.0

4.0.5

Patch Changes

  • 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

4.0.4

Patch Changes

  • Updated dependencies [326b66b]
    • @objectstack/spec@4.0.4
    • @objectstack/core@4.0.4

4.0.3

Patch Changes

  • @objectstack/spec@4.0.3
  • @objectstack/core@4.0.3

4.0.2

Patch Changes

  • Updated dependencies [5f659e9]
    • @objectstack/spec@4.0.2
    • @objectstack/core@4.0.2

4.0.0

Patch Changes

  • Updated dependencies [f08ffc3]
  • Updated dependencies [e0b0a78]
    • @objectstack/spec@4.0.0
    • @objectstack/core@4.0.0

3.3.1

Patch Changes

  • @objectstack/spec@3.3.1
  • @objectstack/core@3.3.1

3.3.0

Patch Changes

  • @objectstack/spec@3.3.0
  • @objectstack/core@3.3.0

3.2.9

Patch Changes

  • @objectstack/spec@3.2.9
  • @objectstack/core@3.2.9

3.2.8

Patch Changes

  • @objectstack/spec@3.2.8
  • @objectstack/core@3.2.8

3.2.7

Patch Changes

  • @objectstack/spec@3.2.7
  • @objectstack/core@3.2.7

3.2.6

Patch Changes

  • @objectstack/spec@3.2.6
  • @objectstack/core@3.2.6

3.2.5

Patch Changes

  • @objectstack/spec@3.2.5
  • @objectstack/core@3.2.5

3.2.4

Patch Changes

  • @objectstack/spec@3.2.4
  • @objectstack/core@3.2.4

3.2.3

Patch Changes

  • @objectstack/spec@3.2.3
  • @objectstack/core@3.2.3

3.2.2

Patch Changes

  • Updated dependencies [46defbb]
    • @objectstack/spec@3.2.2
    • @objectstack/core@3.2.2

3.2.1

Patch Changes

  • Updated dependencies [850b546]
    • @objectstack/spec@3.2.1
    • @objectstack/core@3.2.1

3.2.0

Patch Changes

  • Updated dependencies [5901c29]
    • @objectstack/spec@3.2.0
    • @objectstack/core@3.2.0

3.1.1

Patch Changes

  • Updated dependencies [953d667]
    • @objectstack/spec@3.1.1
    • @objectstack/core@3.1.1

3.1.0

Patch Changes

  • Updated dependencies [0088830]
    • @objectstack/spec@3.1.0
    • @objectstack/core@3.1.0

3.0.11

Patch Changes

  • Updated dependencies [92d9d99]
    • @objectstack/spec@3.0.11
    • @objectstack/core@3.0.11

3.0.10

Patch Changes

  • Updated dependencies [d1e5d31]
    • @objectstack/spec@3.0.10
    • @objectstack/core@3.0.10

3.0.9

Patch Changes

  • Updated dependencies [15e0df6]
    • @objectstack/spec@3.0.9
    • @objectstack/core@3.0.9

3.0.8

Patch Changes

  • Updated dependencies [5a968a2]
    • @objectstack/spec@3.0.8
    • @objectstack/core@3.0.8

3.0.7

Patch Changes

  • Updated dependencies [0119bd7]
  • Updated dependencies [5426bdf]
    • @objectstack/spec@3.0.7
    • @objectstack/core@3.0.7

3.0.6

Patch Changes

  • Updated dependencies [5df254c]
    • @objectstack/spec@3.0.6
    • @objectstack/core@3.0.6

3.0.5

Patch Changes

  • Updated dependencies [23a4a68]
    • @objectstack/spec@3.0.5
    • @objectstack/core@3.0.5

3.0.4

Patch Changes

  • Updated dependencies [d738987]
    • @objectstack/spec@3.0.4
    • @objectstack/core@3.0.4

3.0.3

Patch Changes

  • c7267f6: Patch release for maintenance updates and improvements.
  • Updated dependencies [c7267f6]
    • @objectstack/spec@3.0.3
    • @objectstack/core@3.0.3

3.0.2

Patch Changes

  • Updated dependencies [28985f5]
    • @objectstack/spec@3.0.2
    • @objectstack/core@3.0.2

3.0.1

Patch Changes

  • Updated dependencies [389725a]
    • @objectstack/spec@3.0.1
    • @objectstack/core@3.0.1

3.0.0

Major Changes

  • Release v3.0.0 — unified version bump for all ObjectStack packages.

Patch Changes

  • Updated dependencies
    • @objectstack/spec@3.0.0
    • @objectstack/core@3.0.0

2.0.7

Patch Changes

  • Updated dependencies
    • @objectstack/spec@2.0.7
    • @objectstack/core@2.0.7

2.0.6

Patch Changes

  • Patch release for maintenance and stability improvements
  • Updated dependencies
    • @objectstack/spec@2.0.6
    • @objectstack/core@2.0.6

2.0.5

Patch Changes

  • Unify all package versions with a patch release
  • Updated dependencies
    • @objectstack/spec@2.0.5
    • @objectstack/core@2.0.5