@objectstack/plugin-approvals 12.6.0 → 14.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
 
2
- > @objectstack/plugin-approvals@12.6.0 build /home/runner/work/framework/framework/packages/plugins/plugin-approvals
2
+ > @objectstack/plugin-approvals@14.3.0 build /home/runner/work/framework/framework/packages/plugins/plugin-approvals
3
3
  > tsup --config ../../../tsup.config.ts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -10,13 +10,13 @@
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
12
  CJS Build start
13
- ESM dist/index.mjs 120.29 KB
14
- ESM dist/index.mjs.map 215.92 KB
15
- ESM ⚡️ Build success in 284ms
16
- CJS dist/index.js 121.96 KB
17
- CJS dist/index.js.map 217.12 KB
18
- CJS ⚡️ Build success in 285ms
13
+ CJS dist/index.js 123.99 KB
14
+ CJS dist/index.js.map 220.47 KB
15
+ CJS ⚡️ Build success in 241ms
16
+ ESM dist/index.mjs 122.33 KB
17
+ ESM dist/index.mjs.map 219.27 KB
18
+ ESM ⚡️ Build success in 244ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 24925ms
21
- DTS dist/index.d.mts 381.04 KB
22
- DTS dist/index.d.ts 381.04 KB
20
+ DTS ⚡️ Build success in 28424ms
21
+ DTS dist/index.d.mts 382.64 KB
22
+ DTS dist/index.d.ts 382.64 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,122 @@
1
1
  # @objectstack/plugin-approvals
2
2
 
3
+ ## 14.3.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [2a71f48]
8
+ - Updated dependencies [02f6af4]
9
+ - Updated dependencies [c1064f1]
10
+ - @objectstack/platform-objects@14.3.0
11
+ - @objectstack/spec@14.3.0
12
+ - @objectstack/core@14.3.0
13
+ - @objectstack/formula@14.3.0
14
+ - @objectstack/metadata-core@14.3.0
15
+
16
+ ## 14.2.0
17
+
18
+ ### Patch Changes
19
+
20
+ - Updated dependencies [ac8f029]
21
+ - Updated dependencies [4ab9958]
22
+ - @objectstack/spec@14.2.0
23
+ - @objectstack/platform-objects@14.2.0
24
+ - @objectstack/core@14.2.0
25
+ - @objectstack/formula@14.2.0
26
+ - @objectstack/metadata-core@14.2.0
27
+
28
+ ## 14.1.0
29
+
30
+ ### Minor Changes
31
+
32
+ - 5a8465f: SLA escalation `escalateTo` is position-first (ADR-0090 D3 follow-up to the `position` approver type).
33
+
34
+ - **spec**: `ApprovalEscalationSchema.escalateTo` is documented as a position machine name or a
35
+ specific user id (was "User id, role, or manager level" — the same pre-D3 'role' trap the
36
+ `position` approver type fixed); the Studio xRef picker kind moves `role` → `position`.
37
+ - **plugin-approvals**: on escalation, `escalateTo` now expands position holders via
38
+ `sys_user_position` ∪ the `sys_member.role` transition source (ADR-0057 D4) for both the
39
+ `reassign` approver hand-off and the `notify` audience. An empty expansion falls back to
40
+ treating the value as a literal user id, so configs naming a specific user keep working
41
+ unchanged. The audit trail keeps the authored target.
42
+ - **lint**: new `approval-escalation-reassign-no-target` warning — `escalation.action: 'reassign'`
43
+ with no `escalateTo` silently degrades to a notify at runtime; the fix-it prescribes a position
44
+ or user id target (or `action: 'notify'`).
45
+
46
+ ### Patch Changes
47
+
48
+ - Updated dependencies [5a8465f]
49
+ - Updated dependencies [7f8620b]
50
+ - Updated dependencies [82ba3a6]
51
+ - @objectstack/spec@14.1.0
52
+ - @objectstack/core@14.1.0
53
+ - @objectstack/formula@14.1.0
54
+ - @objectstack/metadata-core@14.1.0
55
+ - @objectstack/platform-objects@14.1.0
56
+
57
+ ## 14.0.0
58
+
59
+ ### Minor Changes
60
+
61
+ - 216fa9a: Add a `position` approver type so approvals can route to org positions (ADR-0090 D3 fallout).
62
+
63
+ Post ADR-0090 D3 the `role` approver type resolves against the better-auth org-membership
64
+ tier (`sys_member.role`: `owner`/`admin`/`member`) — it was never a position. Downstream
65
+ apps that authored `{ type: 'role', value: 'sales_manager' }` silently routed approvals to
66
+ nobody. Now:
67
+
68
+ - **spec**: `ApproverType` gains `'position'` — `value` is the position machine name; the
69
+ approver expands to its holders via `sys_user_position`. Authoring guidance: keep
70
+ `type: 'role'` ONLY for membership tiers; for org positions use
71
+ `{ type: 'position', value: '<position_name>' }` (one-line fix for the mismatch above).
72
+ - **plugin-approvals**: the engine resolves `position` approvers via `sys_user_position` ∪
73
+ the `sys_member.role` transition source (same semantics as `PositionGraphService` in
74
+ plugin-sharing). The `department` approver type is now honored by its spec spelling
75
+ (previously only the off-spec `business_unit`/`bu` dialect matched).
76
+ - **lint**: new `validateApprovalApprovers` rule — `approval-role-not-membership-tier`
77
+ warns when a `role` approver's value is not a membership tier and prescribes the
78
+ `position` rewrite; `approval-approver-type-unknown` flags off-spec approver types
79
+ (with a `business_unit` → `department` fix-it). Wired into `os lint`.
80
+
81
+ ### Patch Changes
82
+
83
+ - Updated dependencies [0a8e685]
84
+ - Updated dependencies [afa8115]
85
+ - Updated dependencies [80f12ca]
86
+ - Updated dependencies [332b711]
87
+ - Updated dependencies [e2fa074]
88
+ - Updated dependencies [23c8668]
89
+ - Updated dependencies [29f017d]
90
+ - Updated dependencies [216fa9a]
91
+ - Updated dependencies [6c22b12]
92
+ - Updated dependencies [d0531c4]
93
+ - Updated dependencies [cff5aac]
94
+ - @objectstack/spec@14.0.0
95
+ - @objectstack/platform-objects@14.0.0
96
+ - @objectstack/core@14.0.0
97
+ - @objectstack/formula@14.0.0
98
+ - @objectstack/metadata-core@14.0.0
99
+
100
+ ## 13.0.0
101
+
102
+ ### Patch Changes
103
+
104
+ - Updated dependencies [6d83431]
105
+ - Updated dependencies [01917c2]
106
+ - Updated dependencies [b271691]
107
+ - Updated dependencies [a5a1e41]
108
+ - Updated dependencies [466adf6]
109
+ - Updated dependencies [5be00c3]
110
+ - Updated dependencies [466adf6]
111
+ - Updated dependencies [2bee609]
112
+ - Updated dependencies [9fa84f9]
113
+ - Updated dependencies [fc7e7f7]
114
+ - @objectstack/spec@13.0.0
115
+ - @objectstack/core@13.0.0
116
+ - @objectstack/formula@13.0.0
117
+ - @objectstack/platform-objects@13.0.0
118
+ - @objectstack/metadata-core@13.0.0
119
+
3
120
  ## 12.6.0
4
121
 
5
122
  ### Patch Changes
@@ -355,8 +472,8 @@
355
472
  ADR-0057 — ERP authorization core. Adds permission-grant access DEPTH
356
473
  (`own`/`own_and_reports`/`unit`/`unit_and_below`/`org`), renames `sys_department`
357
474
  → `sys_business_unit` (no aliases — see BREAKING above), introduces the platform-owned
358
- `sys_user_role` assignment, and seeds stack-declared `roles`/`sharingRules` into
359
- `sys_role`/`sys_sharing_rule` at boot (closes #2077). Hierarchy-relative scopes are
475
+ `sys_user_position` assignment, and seeds stack-declared `roles`/`sharingRules` into
476
+ `sys_position`/`sys_sharing_rule` at boot (closes #2077). Hierarchy-relative scopes are
360
477
  delegated to a pluggable `IHierarchyScopeResolver` (open edition fails closed to
361
478
  owner-only; `defineStack` errors without `requires: ['hierarchy-security']`). Also
362
479
  fixes a latent over-grant where `engine.find({ filter })` was ignored (driver reads
@@ -765,8 +882,8 @@
765
882
  `i18n.loadTranslations` (the i18n service is optional — load is best-effort).
766
883
  - `plugin-webhooks` ← `sys_webhook`, `sys_webhook_delivery`
767
884
  - `plugin-approvals` ← `sys_approval_request`, `sys_approval_action`
768
- - `plugin-security` ← `sys_role`, `sys_permission_set`,
769
- `sys_user_permission_set`, `sys_role_permission_set`
885
+ - `plugin-security` ← `sys_position`, `sys_permission_set`,
886
+ `sys_user_permission_set`, `sys_position_permission_set`
770
887
  - `plugin-sharing` ← `sys_record_share`, `sys_sharing_rule`, `sys_share_link`
771
888
  - `@objectstack/platform-objects` translation bundles are regenerated to drop
772
889
  those objects' keys (its extract config already excluded them); all other
package/dist/index.d.mts CHANGED
@@ -320,7 +320,12 @@ declare const SysApprovalRequest: Omit<{
320
320
  access?: {
321
321
  default: "public" | "private";
322
322
  } | undefined;
323
- requiredPermissions?: string[] | undefined;
323
+ requiredPermissions?: string[] | {
324
+ read?: string[] | undefined;
325
+ create?: string[] | undefined;
326
+ update?: string[] | undefined;
327
+ delete?: string[] | undefined;
328
+ } | undefined;
324
329
  softDelete?: {
325
330
  enabled: boolean;
326
331
  field: string;
@@ -666,9 +671,10 @@ declare const SysApprovalRequest: Omit<{
666
671
  trash: boolean;
667
672
  mru: boolean;
668
673
  clone: boolean;
669
- apiMethods?: ("search" | "create" | "import" | "delete" | "get" | "update" | "upsert" | "list" | "bulk" | "aggregate" | "history" | "restore" | "purge" | "export")[] | undefined;
674
+ apiMethods?: ("search" | "create" | "import" | "delete" | "update" | "get" | "upsert" | "list" | "bulk" | "aggregate" | "history" | "restore" | "purge" | "export")[] | undefined;
670
675
  } | undefined;
671
- sharingModel?: "full" | "private" | "read" | "public_read" | "public_read_write" | "controlled_by_parent" | "read_write" | undefined;
676
+ sharingModel?: "private" | "public_read" | "public_read_write" | "controlled_by_parent" | undefined;
677
+ externalSharingModel?: "private" | "public_read" | "public_read_write" | "controlled_by_parent" | undefined;
672
678
  publicSharing?: {
673
679
  enabled: boolean;
674
680
  allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
@@ -4507,7 +4513,12 @@ declare const SysApprovalAction: Omit<{
4507
4513
  access?: {
4508
4514
  default: "public" | "private";
4509
4515
  } | undefined;
4510
- requiredPermissions?: string[] | undefined;
4516
+ requiredPermissions?: string[] | {
4517
+ read?: string[] | undefined;
4518
+ create?: string[] | undefined;
4519
+ update?: string[] | undefined;
4520
+ delete?: string[] | undefined;
4521
+ } | undefined;
4511
4522
  softDelete?: {
4512
4523
  enabled: boolean;
4513
4524
  field: string;
@@ -4853,9 +4864,10 @@ declare const SysApprovalAction: Omit<{
4853
4864
  trash: boolean;
4854
4865
  mru: boolean;
4855
4866
  clone: boolean;
4856
- apiMethods?: ("search" | "create" | "import" | "delete" | "get" | "update" | "upsert" | "list" | "bulk" | "aggregate" | "history" | "restore" | "purge" | "export")[] | undefined;
4867
+ apiMethods?: ("search" | "create" | "import" | "delete" | "update" | "get" | "upsert" | "list" | "bulk" | "aggregate" | "history" | "restore" | "purge" | "export")[] | undefined;
4857
4868
  } | undefined;
4858
- sharingModel?: "full" | "private" | "read" | "public_read" | "public_read_write" | "controlled_by_parent" | "read_write" | undefined;
4869
+ sharingModel?: "private" | "public_read" | "public_read_write" | "controlled_by_parent" | undefined;
4870
+ externalSharingModel?: "private" | "public_read" | "public_read_write" | "controlled_by_parent" | undefined;
4859
4871
  publicSharing?: {
4860
4872
  enabled: boolean;
4861
4873
  allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
@@ -7028,7 +7040,12 @@ declare const SysApprovalApprover: Omit<{
7028
7040
  access?: {
7029
7041
  default: "public" | "private";
7030
7042
  } | undefined;
7031
- requiredPermissions?: string[] | undefined;
7043
+ requiredPermissions?: string[] | {
7044
+ read?: string[] | undefined;
7045
+ create?: string[] | undefined;
7046
+ update?: string[] | undefined;
7047
+ delete?: string[] | undefined;
7048
+ } | undefined;
7032
7049
  softDelete?: {
7033
7050
  enabled: boolean;
7034
7051
  field: string;
@@ -7374,9 +7391,10 @@ declare const SysApprovalApprover: Omit<{
7374
7391
  trash: boolean;
7375
7392
  mru: boolean;
7376
7393
  clone: boolean;
7377
- apiMethods?: ("search" | "create" | "import" | "delete" | "get" | "update" | "upsert" | "list" | "bulk" | "aggregate" | "history" | "restore" | "purge" | "export")[] | undefined;
7394
+ apiMethods?: ("search" | "create" | "import" | "delete" | "update" | "get" | "upsert" | "list" | "bulk" | "aggregate" | "history" | "restore" | "purge" | "export")[] | undefined;
7378
7395
  } | undefined;
7379
- sharingModel?: "full" | "private" | "read" | "public_read" | "public_read_write" | "controlled_by_parent" | "read_write" | undefined;
7396
+ sharingModel?: "private" | "public_read" | "public_read_write" | "controlled_by_parent" | undefined;
7397
+ externalSharingModel?: "private" | "public_read" | "public_read_write" | "controlled_by_parent" | undefined;
7380
7398
  publicSharing?: {
7381
7399
  enabled: boolean;
7382
7400
  allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
@@ -8442,8 +8460,8 @@ declare const SysApprovalApprover: Omit<{
8442
8460
  * `approve` / `reject` edge.
8443
8461
  *
8444
8462
  * This service owns the durable approval *state* — `sys_approval_request` /
8445
- * `sys_approval_action`, approver resolution (team / department / role /
8446
- * manager graph), and the optional status-field mirror — plus the decision
8463
+ * `sys_approval_action`, approver resolution (team / department / position /
8464
+ * role / manager graph), and the optional status-field mirror — plus the decision
8447
8465
  * API. It does not author processes, submit, or walk multi-step machinery
8448
8466
  * anymore; that orchestration lives on the one automation engine.
8449
8467
  */
@@ -8548,16 +8566,20 @@ declare class ApprovalService implements IApprovalService {
8548
8566
  private loadPendingRow;
8549
8567
  /**
8550
8568
  * Expand the approvers on an Approval node into user IDs by querying the
8551
- * graph tables for `team:` / `department:` / `role:` / `manager:` approver
8552
- * types. Falls back to a prefixed literal (`type:value`) when graph lookups
8553
- * produce nothing — so existing fixtures and flows that rely on substring
8554
- * matching keep working.
8569
+ * graph tables for `team:` / `department:` / `position:` / `role:` /
8570
+ * `manager:` approver types. Falls back to a prefixed literal
8571
+ * (`type:value`) when graph lookups produce nothing — so existing fixtures
8572
+ * and flows that rely on substring matching keep working.
8555
8573
  *
8556
8574
  * **Graph semantics:**
8557
8575
  * - `team` → flat members of `sys_team` (better-auth; no BFS)
8558
8576
  * - `department` → recursive BFS of `sys_business_unit.parent_business_unit_id`
8559
8577
  * → members of every descendant via `sys_business_unit_member`
8560
- * - `role` users with `sys_member.role = value` in tenant
8578
+ * - `position` holders via `sys_user_position` ∪ `sys_member.role`
8579
+ * transition source (ADR-0090 D3 / ADR-0057 D4)
8580
+ * - `role` → users with `sys_member.role = value` in tenant — the
8581
+ * better-auth MEMBERSHIP TIER (owner/admin/member), not a
8582
+ * position; author `position` for org positions
8561
8583
  * - `manager` → `sys_user.manager_id` of `record[value] ?? record.owner_id`
8562
8584
  * - `field` → literal user id stored in `record[value]`
8563
8585
  * - `user` → literal value
@@ -8567,6 +8589,16 @@ declare class ApprovalService implements IApprovalService {
8567
8589
  private expandTeamUsers;
8568
8590
  /** Recursive department — walks `sys_business_unit.parent_business_unit_id`. */
8569
8591
  private expandBusinessUnitUsers;
8592
+ /**
8593
+ * Position holders (ADR-0090 D3): `sys_user_position` is the platform-owned
8594
+ * assignment table, keyed by the position's machine name (ADR-0057 D4),
8595
+ * unioned with the better-auth membership string (`sys_member.role`) as a
8596
+ * transition source — the same semantics as `PositionGraphService` in
8597
+ * `plugin-sharing`, so an approval routes to exactly the users the sharing
8598
+ * engine would expand for the same position.
8599
+ */
8600
+ private expandPositionUsers;
8601
+ /** better-auth org-membership tier (`sys_member.role`) — NOT positions. */
8570
8602
  private expandRoleUsers;
8571
8603
  private lookupManager;
8572
8604
  /** Mirror a request status onto a business-object field, if configured. */
package/dist/index.d.ts CHANGED
@@ -320,7 +320,12 @@ declare const SysApprovalRequest: Omit<{
320
320
  access?: {
321
321
  default: "public" | "private";
322
322
  } | undefined;
323
- requiredPermissions?: string[] | undefined;
323
+ requiredPermissions?: string[] | {
324
+ read?: string[] | undefined;
325
+ create?: string[] | undefined;
326
+ update?: string[] | undefined;
327
+ delete?: string[] | undefined;
328
+ } | undefined;
324
329
  softDelete?: {
325
330
  enabled: boolean;
326
331
  field: string;
@@ -666,9 +671,10 @@ declare const SysApprovalRequest: Omit<{
666
671
  trash: boolean;
667
672
  mru: boolean;
668
673
  clone: boolean;
669
- apiMethods?: ("search" | "create" | "import" | "delete" | "get" | "update" | "upsert" | "list" | "bulk" | "aggregate" | "history" | "restore" | "purge" | "export")[] | undefined;
674
+ apiMethods?: ("search" | "create" | "import" | "delete" | "update" | "get" | "upsert" | "list" | "bulk" | "aggregate" | "history" | "restore" | "purge" | "export")[] | undefined;
670
675
  } | undefined;
671
- sharingModel?: "full" | "private" | "read" | "public_read" | "public_read_write" | "controlled_by_parent" | "read_write" | undefined;
676
+ sharingModel?: "private" | "public_read" | "public_read_write" | "controlled_by_parent" | undefined;
677
+ externalSharingModel?: "private" | "public_read" | "public_read_write" | "controlled_by_parent" | undefined;
672
678
  publicSharing?: {
673
679
  enabled: boolean;
674
680
  allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
@@ -4507,7 +4513,12 @@ declare const SysApprovalAction: Omit<{
4507
4513
  access?: {
4508
4514
  default: "public" | "private";
4509
4515
  } | undefined;
4510
- requiredPermissions?: string[] | undefined;
4516
+ requiredPermissions?: string[] | {
4517
+ read?: string[] | undefined;
4518
+ create?: string[] | undefined;
4519
+ update?: string[] | undefined;
4520
+ delete?: string[] | undefined;
4521
+ } | undefined;
4511
4522
  softDelete?: {
4512
4523
  enabled: boolean;
4513
4524
  field: string;
@@ -4853,9 +4864,10 @@ declare const SysApprovalAction: Omit<{
4853
4864
  trash: boolean;
4854
4865
  mru: boolean;
4855
4866
  clone: boolean;
4856
- apiMethods?: ("search" | "create" | "import" | "delete" | "get" | "update" | "upsert" | "list" | "bulk" | "aggregate" | "history" | "restore" | "purge" | "export")[] | undefined;
4867
+ apiMethods?: ("search" | "create" | "import" | "delete" | "update" | "get" | "upsert" | "list" | "bulk" | "aggregate" | "history" | "restore" | "purge" | "export")[] | undefined;
4857
4868
  } | undefined;
4858
- sharingModel?: "full" | "private" | "read" | "public_read" | "public_read_write" | "controlled_by_parent" | "read_write" | undefined;
4869
+ sharingModel?: "private" | "public_read" | "public_read_write" | "controlled_by_parent" | undefined;
4870
+ externalSharingModel?: "private" | "public_read" | "public_read_write" | "controlled_by_parent" | undefined;
4859
4871
  publicSharing?: {
4860
4872
  enabled: boolean;
4861
4873
  allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
@@ -7028,7 +7040,12 @@ declare const SysApprovalApprover: Omit<{
7028
7040
  access?: {
7029
7041
  default: "public" | "private";
7030
7042
  } | undefined;
7031
- requiredPermissions?: string[] | undefined;
7043
+ requiredPermissions?: string[] | {
7044
+ read?: string[] | undefined;
7045
+ create?: string[] | undefined;
7046
+ update?: string[] | undefined;
7047
+ delete?: string[] | undefined;
7048
+ } | undefined;
7032
7049
  softDelete?: {
7033
7050
  enabled: boolean;
7034
7051
  field: string;
@@ -7374,9 +7391,10 @@ declare const SysApprovalApprover: Omit<{
7374
7391
  trash: boolean;
7375
7392
  mru: boolean;
7376
7393
  clone: boolean;
7377
- apiMethods?: ("search" | "create" | "import" | "delete" | "get" | "update" | "upsert" | "list" | "bulk" | "aggregate" | "history" | "restore" | "purge" | "export")[] | undefined;
7394
+ apiMethods?: ("search" | "create" | "import" | "delete" | "update" | "get" | "upsert" | "list" | "bulk" | "aggregate" | "history" | "restore" | "purge" | "export")[] | undefined;
7378
7395
  } | undefined;
7379
- sharingModel?: "full" | "private" | "read" | "public_read" | "public_read_write" | "controlled_by_parent" | "read_write" | undefined;
7396
+ sharingModel?: "private" | "public_read" | "public_read_write" | "controlled_by_parent" | undefined;
7397
+ externalSharingModel?: "private" | "public_read" | "public_read_write" | "controlled_by_parent" | undefined;
7380
7398
  publicSharing?: {
7381
7399
  enabled: boolean;
7382
7400
  allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
@@ -8442,8 +8460,8 @@ declare const SysApprovalApprover: Omit<{
8442
8460
  * `approve` / `reject` edge.
8443
8461
  *
8444
8462
  * This service owns the durable approval *state* — `sys_approval_request` /
8445
- * `sys_approval_action`, approver resolution (team / department / role /
8446
- * manager graph), and the optional status-field mirror — plus the decision
8463
+ * `sys_approval_action`, approver resolution (team / department / position /
8464
+ * role / manager graph), and the optional status-field mirror — plus the decision
8447
8465
  * API. It does not author processes, submit, or walk multi-step machinery
8448
8466
  * anymore; that orchestration lives on the one automation engine.
8449
8467
  */
@@ -8548,16 +8566,20 @@ declare class ApprovalService implements IApprovalService {
8548
8566
  private loadPendingRow;
8549
8567
  /**
8550
8568
  * Expand the approvers on an Approval node into user IDs by querying the
8551
- * graph tables for `team:` / `department:` / `role:` / `manager:` approver
8552
- * types. Falls back to a prefixed literal (`type:value`) when graph lookups
8553
- * produce nothing — so existing fixtures and flows that rely on substring
8554
- * matching keep working.
8569
+ * graph tables for `team:` / `department:` / `position:` / `role:` /
8570
+ * `manager:` approver types. Falls back to a prefixed literal
8571
+ * (`type:value`) when graph lookups produce nothing — so existing fixtures
8572
+ * and flows that rely on substring matching keep working.
8555
8573
  *
8556
8574
  * **Graph semantics:**
8557
8575
  * - `team` → flat members of `sys_team` (better-auth; no BFS)
8558
8576
  * - `department` → recursive BFS of `sys_business_unit.parent_business_unit_id`
8559
8577
  * → members of every descendant via `sys_business_unit_member`
8560
- * - `role` users with `sys_member.role = value` in tenant
8578
+ * - `position` holders via `sys_user_position` ∪ `sys_member.role`
8579
+ * transition source (ADR-0090 D3 / ADR-0057 D4)
8580
+ * - `role` → users with `sys_member.role = value` in tenant — the
8581
+ * better-auth MEMBERSHIP TIER (owner/admin/member), not a
8582
+ * position; author `position` for org positions
8561
8583
  * - `manager` → `sys_user.manager_id` of `record[value] ?? record.owner_id`
8562
8584
  * - `field` → literal user id stored in `record[value]`
8563
8585
  * - `user` → literal value
@@ -8567,6 +8589,16 @@ declare class ApprovalService implements IApprovalService {
8567
8589
  private expandTeamUsers;
8568
8590
  /** Recursive department — walks `sys_business_unit.parent_business_unit_id`. */
8569
8591
  private expandBusinessUnitUsers;
8592
+ /**
8593
+ * Position holders (ADR-0090 D3): `sys_user_position` is the platform-owned
8594
+ * assignment table, keyed by the position's machine name (ADR-0057 D4),
8595
+ * unioned with the better-auth membership string (`sys_member.role`) as a
8596
+ * transition source — the same semantics as `PositionGraphService` in
8597
+ * `plugin-sharing`, so an approval routes to exactly the users the sharing
8598
+ * engine would expand for the same position.
8599
+ */
8600
+ private expandPositionUsers;
8601
+ /** better-auth org-membership tier (`sys_member.role`) — NOT positions. */
8570
8602
  private expandRoleUsers;
8571
8603
  private lookupManager;
8572
8604
  /** Mirror a request status onto a business-object field, if configured. */
package/dist/index.js CHANGED
@@ -1029,7 +1029,7 @@ var ESCALATION_JOB_NAME = "approvals-sla-escalation";
1029
1029
  var ESCALATION_SCAN_INTERVAL_MS = 5 * 60 * 1e3;
1030
1030
  var SLA_ACTOR_ID = "system:sla";
1031
1031
  var ACTION_TOKEN_TTL_MS = 72 * 60 * 60 * 1e3;
1032
- var SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] };
1032
+ var SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] };
1033
1033
  function uid(prefix) {
1034
1034
  const g = globalThis;
1035
1035
  if (g.crypto?.randomUUID) return `${prefix}_${g.crypto.randomUUID()}`;
@@ -1152,16 +1152,20 @@ var _ApprovalService = class _ApprovalService {
1152
1152
  }
1153
1153
  /**
1154
1154
  * Expand the approvers on an Approval node into user IDs by querying the
1155
- * graph tables for `team:` / `department:` / `role:` / `manager:` approver
1156
- * types. Falls back to a prefixed literal (`type:value`) when graph lookups
1157
- * produce nothing — so existing fixtures and flows that rely on substring
1158
- * matching keep working.
1155
+ * graph tables for `team:` / `department:` / `position:` / `role:` /
1156
+ * `manager:` approver types. Falls back to a prefixed literal
1157
+ * (`type:value`) when graph lookups produce nothing — so existing fixtures
1158
+ * and flows that rely on substring matching keep working.
1159
1159
  *
1160
1160
  * **Graph semantics:**
1161
1161
  * - `team` → flat members of `sys_team` (better-auth; no BFS)
1162
1162
  * - `department` → recursive BFS of `sys_business_unit.parent_business_unit_id`
1163
1163
  * → members of every descendant via `sys_business_unit_member`
1164
- * - `role` users with `sys_member.role = value` in tenant
1164
+ * - `position` holders via `sys_user_position` ∪ `sys_member.role`
1165
+ * transition source (ADR-0090 D3 / ADR-0057 D4)
1166
+ * - `role` → users with `sys_member.role = value` in tenant — the
1167
+ * better-auth MEMBERSHIP TIER (owner/admin/member), not a
1168
+ * position; author `position` for org positions
1165
1169
  * - `manager` → `sys_user.manager_id` of `record[value] ?? record.owner_id`
1166
1170
  * - `field` → literal user id stored in `record[value]`
1167
1171
  * - `user` → literal value
@@ -1186,12 +1190,18 @@ var _ApprovalService = class _ApprovalService {
1186
1190
  for (const u of users) out.push(u);
1187
1191
  continue;
1188
1192
  }
1189
- } else if (a.type === "business_unit" || a.type === "bu") {
1193
+ } else if (a.type === "department" || a.type === "business_unit" || a.type === "bu") {
1190
1194
  const users = await this.expandBusinessUnitUsers(String(a.value), organizationId);
1191
1195
  if (users.length) {
1192
1196
  for (const u of users) out.push(u);
1193
1197
  continue;
1194
1198
  }
1199
+ } else if (a.type === "position") {
1200
+ const users = await this.expandPositionUsers(String(a.value), organizationId);
1201
+ if (users.length) {
1202
+ for (const u of users) out.push(u);
1203
+ continue;
1204
+ }
1195
1205
  } else if (a.type === "role") {
1196
1206
  const users = await this.expandRoleUsers(String(a.value), organizationId);
1197
1207
  if (users.length) {
@@ -1278,6 +1288,36 @@ var _ApprovalService = class _ApprovalService {
1278
1288
  }
1279
1289
  return Array.from(new Set((rows ?? []).map((r) => String(r.user_id ?? "")).filter(Boolean)));
1280
1290
  }
1291
+ /**
1292
+ * Position holders (ADR-0090 D3): `sys_user_position` is the platform-owned
1293
+ * assignment table, keyed by the position's machine name (ADR-0057 D4),
1294
+ * unioned with the better-auth membership string (`sys_member.role`) as a
1295
+ * transition source — the same semantics as `PositionGraphService` in
1296
+ * `plugin-sharing`, so an approval routes to exactly the users the sharing
1297
+ * engine would expand for the same position.
1298
+ */
1299
+ async expandPositionUsers(positionName, organizationId) {
1300
+ if (!positionName) return [];
1301
+ const users = /* @__PURE__ */ new Set();
1302
+ const filter = { position: positionName };
1303
+ if (organizationId) filter.organization_id = organizationId;
1304
+ try {
1305
+ const rows = await this.engine.find("sys_user_position", {
1306
+ filter,
1307
+ fields: ["user_id"],
1308
+ limit: 1e4,
1309
+ context: SYSTEM_CTX
1310
+ });
1311
+ for (const r of rows ?? []) {
1312
+ const uid2 = String(r.user_id ?? "");
1313
+ if (uid2) users.add(uid2);
1314
+ }
1315
+ } catch {
1316
+ }
1317
+ for (const uid2 of await this.expandRoleUsers(positionName, organizationId)) users.add(uid2);
1318
+ return Array.from(users);
1319
+ }
1320
+ /** better-auth org-membership tier (`sys_member.role`) — NOT positions. */
1281
1321
  async expandRoleUsers(roleName, organizationId) {
1282
1322
  if (!roleName) return [];
1283
1323
  const filter = { role: roleName };
@@ -2199,6 +2239,15 @@ var _ApprovalService = class _ApprovalService {
2199
2239
  const escalateTo = typeof esc2.escalateTo === "string" && esc2.escalateTo.trim() ? esc2.escalateTo.trim() : void 0;
2200
2240
  const now = this.clock.now().toISOString();
2201
2241
  const pending = csvSplit(raw.pending_approvers);
2242
+ let escalatees = [];
2243
+ if (escalateTo) {
2244
+ try {
2245
+ escalatees = await this.expandPositionUsers(escalateTo, raw.organization_id ?? null);
2246
+ } catch {
2247
+ escalatees = [];
2248
+ }
2249
+ if (!escalatees.length) escalatees = [escalateTo];
2250
+ }
2202
2251
  await this.engine.insert("sys_approval_action", {
2203
2252
  id: uid("aact"),
2204
2253
  request_id: raw.id,
@@ -2210,16 +2259,16 @@ var _ApprovalService = class _ApprovalService {
2210
2259
  comment: `${action}${escalateTo ? ` \u2192 ${escalateTo}` : ""}`,
2211
2260
  created_at: now
2212
2261
  }, { context: SYSTEM_CTX });
2213
- if (action === "reassign" && escalateTo) {
2262
+ if (action === "reassign" && escalatees.length) {
2214
2263
  await this.engine.update("sys_approval_request", {
2215
2264
  id: raw.id,
2216
- pending_approvers: escalateTo,
2265
+ pending_approvers: escalatees.join(","),
2217
2266
  updated_at: now
2218
2267
  }, { context: SYSTEM_CTX });
2219
- await this.syncApproverIndex(raw.id, [escalateTo], raw.organization_id ?? null, now);
2268
+ await this.syncApproverIndex(raw.id, escalatees, raw.organization_id ?? null, now);
2220
2269
  await this.notify({
2221
2270
  topic: "approval.escalated",
2222
- audience: [escalateTo],
2271
+ audience: escalatees,
2223
2272
  actorId: SLA_ACTOR_ID,
2224
2273
  source: { object: "sys_approval_request", id: raw.id },
2225
2274
  payload: {
@@ -2237,7 +2286,7 @@ var _ApprovalService = class _ApprovalService {
2237
2286
  } else {
2238
2287
  await this.notify({
2239
2288
  topic: "approval.sla_breached",
2240
- audience: [...pending, ...escalateTo ? [escalateTo] : []],
2289
+ audience: [...pending, ...escalatees],
2241
2290
  actorId: SLA_ACTOR_ID,
2242
2291
  source: { object: "sys_approval_request", id: raw.id },
2243
2292
  payload: {
@@ -2937,7 +2986,7 @@ function unbindAllHooks(engine) {
2937
2986
 
2938
2987
  // src/approval-node.ts
2939
2988
  var import_automation2 = require("@objectstack/spec/automation");
2940
- var SYSTEM_CTX2 = { isSystem: true, roles: [], permissions: [] };
2989
+ var SYSTEM_CTX2 = { isSystem: true, positions: [], permissions: [] };
2941
2990
  function registerApprovalNode(automation, service, logger) {
2942
2991
  automation.registerNodeExecutor({
2943
2992
  type: import_automation2.APPROVAL_NODE_TYPE,