Skip to content

@objectstack/plugin-approvals@16.0.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 06:26
f55be04

Minor Changes

  • e412fb6: feat(approvals): declare file attachments on approve/reject decisions

    The declared approval_approve / approval_reject actions on
    sys_approval_request gain an optional multi-file attachments param
    (type: 'file', multiple). The console renders type:'file' action params
    through the shared upload widget (objectui ADR-0059) and POSTs the resolved
    attachments: string[], so a reviewer can attach supporting files to a
    decision through the generic declared-action dialog — letting the approvals
    inbox retire its hand-wired attachment composer (objectui#2698).

    Purely additive metadata: the decision route already forwards
    body.attachments to ApprovalService.decide, and the
    sys_approval_action.attachments column (file, multiple) already persists them
    (#3266/#3274). No service or route change.

  • 8efa395: feat(approvals): server-computed viewer capability for precise decision-action gating

    getRequest / listRequests now attach a per-viewer block —
    viewer: { can_act, is_submitter } — computed from the caller's context
    (ApprovalRequestRow.viewer):

    • can_act — the caller is a current pending approver (their user id is in the
      request's resolved pending_approvers while it is still pending). This is
      the same check the decision methods authorize with, so it already reflects
      position/team/manager resolution — strictly more accurate than a client-side
      identity guess.
    • is_submitter — the caller submitted the request.

    The declared decision actions on sys_approval_request now gate on it: approver
    actions (approve/reject/reassign/send-back/request-info) use
    record.viewer.can_act; submitter levers (remind/recall/resubmit) use
    record.viewer.is_submitter. Previously approver actions only trimmed the
    non-pending case, so a submitter viewing their own pending request saw buttons
    they couldn't use (the backend 403'd); a position-addressed approver could be
    wrongly hidden by the old client heuristic. Where viewer is absent (a row
    surfaced outside a service read with a user context), the predicate fails closed.

  • 3a18b60: feat(approvals): rename the role approver type to org_membership_level (#3133)

    ApproverType.role was the last platform surface projecting the reserved word
    "role" (ADR-0090 D3). It is not covered by D3's better-auth exception: that
    exception protects better-auth's own sys_member.role column, which we do
    not own — ApproverType is our own enum, an authoring surface, and D3 mandates
    that the projection of that concept is spelled org_membership_level and
    labelled "organization membership", never "role".

    The sentence licensing the leak was also false: ADR-0090 D3 claims
    sys_member.role is "already relabelled org_membership_level in the platform
    projection", but org_membership_level existed nowhere in the codebase and
    ADR-0057 D7 lists that relabel under "Deferred (evidence-gated, P4)". The
    projection never landed, so the word reached authors.

    The name manufactured a real, silent failure — "hotcrm class": every other
    surface renamed to position (sys_role, ShareRecipientType.role,
    ctx.roles[]), so { type: 'role', value: 'sales_manager' } reads as the
    legacy spelling of a position. It resolves against the membership tier, finds
    no member row, falls back to an inert role:sales_manager literal, and the
    request waits forever on an approver that cannot exist.

    • spec: ApproverType gains org_membership_level; role stays as a
      deprecated alias for one window (a published 15.x flow keeps loading) with
      DEPRECATED_APPROVER_TYPES + canonicalApproverType() as the single source
      for the mapping. Removed in the next major.
    • plugin-approvals: resolves on the canonical type and warns on the
      deprecated spelling. The type:value fallback literal keeps the authored
      spelling — stored sys_approval_approver rows and pending_approvers slots
      from 15.x carry role:<v>, and rewriting it would orphan them.
    • lint: approval-role-not-membership-tierapproval-approver-not-membership-tier
      (the rule id carried the reserved word too), plus a new
      approval-approver-type-deprecated. The two are mutually exclusive: a bad
      value wins, because prescribing org_membership_level for a position name
      would be wrong advice — the fix there is position.

    Authoring type: 'role' keeps working and now says so out loud. Rewrite it as
    org_membership_level; if the value is an org position, the fix is position.

Patch 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.

  • 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 [06cb319]

  • 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
    • @objectstack/metadata-core@16.0.0