@objectstack/plugin-approvals 17.1.0 → 17.2.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.
- package/CHANGELOG.md +397 -0
- package/dist/index.d.mts +365 -3
- package/dist/index.d.ts +365 -3
- package/dist/index.js +525 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +525 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,402 @@
|
|
|
1
1
|
# @objectstack/plugin-approvals
|
|
2
2
|
|
|
3
|
+
## 17.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b47ba2c: **BREAKING** (compile-time only): `ApprovalServiceOptions['logger']` now
|
|
8
|
+
declares a **non-optional** `warn`, so a durability report always has
|
|
9
|
+
somewhere to land (#9754, #10556). This is the thirteenth of the thirteen
|
|
10
|
+
mechanical repairs the card names — held out of #10691 to serialize against
|
|
11
|
+
PR #10547, which owned `approval-service.ts` while it was open; that fence
|
|
12
|
+
has since cleared.
|
|
13
|
+
|
|
14
|
+
`minor`, not `major`: during the launch window this stack ships breaking
|
|
15
|
+
changes as `minor` — every publishable package versions in lockstep, so a
|
|
16
|
+
`major` would promote the whole release. `patch` would be wrong in the other
|
|
17
|
+
direction, because this *can* break a consumer's build. This is the same
|
|
18
|
+
reasoning #10691 used for the twelve sibling repairs; no exemption for a
|
|
19
|
+
types-only break was found there either, and none applies here.
|
|
20
|
+
|
|
21
|
+
`error` stays optional — hosts legitimately inject reduced sinks, and
|
|
22
|
+
requiring `error` was measured and rejected as #9754 option C. What changes
|
|
23
|
+
is that its *absence* now has a declared, guaranteed destination. Call sites
|
|
24
|
+
keep the `logger?.warn?.(…)` spelling as the backstop for hosts the type
|
|
25
|
+
cannot reach, so **no runtime behaviour changes**: nothing that printed
|
|
26
|
+
before stops printing, and nothing silent starts printing.
|
|
27
|
+
|
|
28
|
+
### Who has to change, and what to do
|
|
29
|
+
|
|
30
|
+
Only a caller that constructs `ApprovalService` (or an `ApprovalServiceOptions`
|
|
31
|
+
value) with a `logger` object that has **no `warn` method** — for example
|
|
32
|
+
`{ error }` alone. Add a `warn` member; there is no rename, no removal, and no
|
|
33
|
+
stored value or metadata key to rewrite. The only non-test construction site
|
|
34
|
+
in this repo (`ApprovalsServicePlugin.start`, in this same package) passes the
|
|
35
|
+
kernel `ctx.logger`, whose `warn` is already required, so the in-repo cost is
|
|
36
|
+
zero.
|
|
37
|
+
|
|
38
|
+
<!-- adr-0087: not-required (runtime-interface-only packages/plugins/plugin-approvals/src/approval-service.ts#ApprovalServiceOptions) the tightened type is a plain TypeScript logger interface -- no Zod projection, no metadata surface, and it is referenced by none -- so `objectstack migrate meta` has nothing to rewrite. Nothing is removed or renamed and no stored value moves; the only consumer action is adding a `warn` member at a construction site the compiler names. -->
|
|
39
|
+
- 13f533a: fix(approvals): screen the `manager` approver to the request's organization (#10153)
|
|
40
|
+
|
|
41
|
+
`expandApprovers` hands the directory organization to every graph-shaped
|
|
42
|
+
approver expansion — `department`, `position`, `org_membership_level`. The
|
|
43
|
+
`manager` branch did not: `lookupManager` read `sys_user.manager_id` under a
|
|
44
|
+
system context and took no organization argument at all. `sys_user` is a global
|
|
45
|
+
identity table with no `organization_id`, so nothing else on that path supplied
|
|
46
|
+
the tenancy fact either. A `manager_id` crossing an organization boundary
|
|
47
|
+
therefore routed the submission to an approver **in another organization** — an
|
|
48
|
+
out-of-tenant person granted approval authority over the record.
|
|
49
|
+
|
|
50
|
+
The same column has been screened on the hierarchy side since cloud#1195. This
|
|
51
|
+
brings the approvals consumer into line for the `manager` branch.
|
|
52
|
+
|
|
53
|
+
## What the screen is
|
|
54
|
+
|
|
55
|
+
`lookupManager(userId, organizationId)` now resolves the manager and then asks
|
|
56
|
+
whether he is **provably outside** the request's organization:
|
|
57
|
+
|
|
58
|
+
| membership rows for the manager | result |
|
|
59
|
+
|---|---|
|
|
60
|
+
| some exist, none in the request's org | **screened out** — the slot falls through to the `manager:<value>` literal |
|
|
61
|
+
| one is in the request's org | resolves, unchanged |
|
|
62
|
+
| none exist at all | resolves, unchanged — the tenancy fact is absent, not negative |
|
|
63
|
+
| the `sys_member` read failed | resolves, unchanged |
|
|
64
|
+
| the request carries no organization | resolves, unchanged — and no read is performed |
|
|
65
|
+
|
|
66
|
+
The fail-open half is this file's ruled posture on addressing paths, stated
|
|
67
|
+
twice already: `filterApproversWhoCanRead` refuses to empty a live slate on an
|
|
68
|
+
infrastructure hiccup, and `expandPositionUsers` carries "a step routing to
|
|
69
|
+
nobody is worse than one routing to a lapsed holder". A drop is logged with the
|
|
70
|
+
manager's id, his organizations and the request's, so the fix ("repair the link"
|
|
71
|
+
/ "grant the membership" / "retarget the step") is legible without a debugger.
|
|
72
|
+
|
|
73
|
+
## ⚠️ This moves one input from accepted to refused
|
|
74
|
+
|
|
75
|
+
A node whose **sole** approver is a cross-org `manager` and which is authored
|
|
76
|
+
with the **non-default** `onEmptyApprovers: 'fail'` used to open successfully;
|
|
77
|
+
it now throws `NO_APPROVERS`. Nothing new is thrown — a screened-out manager
|
|
78
|
+
leaves only a `type:value` literal, which the pre-existing empty-slate test
|
|
79
|
+
already classifies as empty, and `'fail'` already throws on empty. Every
|
|
80
|
+
screened sibling has reached that same bucket since it was written.
|
|
81
|
+
|
|
82
|
+
**The default policy is unaffected**: `admin_rescue` still opens the request
|
|
83
|
+
(decidable by a privileged admin) and warns, and `auto_approve` still
|
|
84
|
+
auto-approves. Both directions and both policies are pinned in
|
|
85
|
+
`manager-approver-org-screen.test.ts`.
|
|
86
|
+
|
|
87
|
+
## What this does NOT decide
|
|
88
|
+
|
|
89
|
+
- **#7497** (does approver routing imply record read visibility?) stays open.
|
|
90
|
+
The screen reads `sys_member`, which looks like the D2 read filter beside it,
|
|
91
|
+
and the code says at length why it is the *sibling* treatment instead: two of
|
|
92
|
+
the three org-scoped expansions already screen on `sys_member.organization_id`,
|
|
93
|
+
and `sys_user` offers no other tenancy fact. No reads are granted and no read
|
|
94
|
+
screen is applied to any type that lacked one.
|
|
95
|
+
- **`team`** is still unscreened — it is a sibling graph expansion that is not
|
|
96
|
+
org-scoped either, tracked as #10230, and it touches this same file.
|
|
97
|
+
- `APPROVER_ORG_SCOPED` is untouched. It answers ADR-0105 D9 *retargetability*
|
|
98
|
+
(may an author write `organization:` on this type?), not screening, and
|
|
99
|
+
`manager: false` remains correct.
|
|
100
|
+
- e222a53: **BREAKING** (compile-time only): twelve logger sink types that declared an
|
|
101
|
+
optional `error` now declare a **non-optional** `warn`, so a durability report
|
|
102
|
+
always has somewhere to land (#9754, #10556).
|
|
103
|
+
|
|
104
|
+
`minor`, not `major`: during the launch window this stack ships breaking changes
|
|
105
|
+
as `minor` — every publishable package versions in lockstep, so a `major` would
|
|
106
|
+
promote the whole release. `patch` would be wrong in the other direction, because
|
|
107
|
+
this *can* break a consumer's build.
|
|
108
|
+
|
|
109
|
+
`error` stays optional on every one of these types — hosts legitimately inject
|
|
110
|
+
reduced sinks, and requiring `error` was measured and rejected as #9754 option C.
|
|
111
|
+
What changes is that its *absence* now has a declared, guaranteed destination.
|
|
112
|
+
Call sites keep the `logger?.warn?.(…)` spelling as the backstop for hosts the
|
|
113
|
+
type cannot reach, so **no runtime behaviour changes**: nothing that printed
|
|
114
|
+
before stops printing, and nothing silent starts printing.
|
|
115
|
+
|
|
116
|
+
### Who has to change, and what to do
|
|
117
|
+
|
|
118
|
+
Only a caller that hands one of these sinks an object with **no `warn` method** —
|
|
119
|
+
for example `{ info }` or `{ error }` alone. Add a `warn` member; there is no
|
|
120
|
+
rename, no removal, and no stored value or metadata key to rewrite. Every
|
|
121
|
+
construction site inside this repo already supplied one, so the in-repo cost was
|
|
122
|
+
zero; the compile error is reserved for the callers that were silently discarding
|
|
123
|
+
these reports.
|
|
124
|
+
|
|
125
|
+
The affected types, by package:
|
|
126
|
+
|
|
127
|
+
- `@objectstack/cloud-connection` — the internal `PluginContext['logger']`
|
|
128
|
+
- `@objectstack/metadata-protocol` — `IndexMigrationLogger`
|
|
129
|
+
- `@objectstack/plugin-approvals` — the internal `MinimalLogger` of `lifecycle-hooks`
|
|
130
|
+
- `@objectstack/plugin-audit` — `AuthEventAuditLogger`, `ReadAuditLogger`
|
|
131
|
+
- `@objectstack/plugin-auth` — `ReconcileMembershipDeps['logger']`, the internal
|
|
132
|
+
`LoggerLike` of `member-role-canonical`, and `AuthManagerOptions['logger']`
|
|
133
|
+
- `@objectstack/plugin-email` — `ReclaimLogger`, via `ReclaimAttachmentContentOptions`
|
|
134
|
+
- `@objectstack/plugin-reports` — `ReportServiceOptions['logger']`
|
|
135
|
+
- `@objectstack/plugin-sharing` — the internal `MinimalLogger` of `bulk-recompute`,
|
|
136
|
+
`rule-hooks` and `record-share-cascade`
|
|
137
|
+
- `@objectstack/plugin-webhooks` — `OptionalLogger`, via `AutoEnqueuerOptions`
|
|
138
|
+
- `@objectstack/service-knowledge` — `KnowledgeLogger`
|
|
139
|
+
|
|
140
|
+
`AuthManagerOptions['logger']` is the one most likely to be reached from outside:
|
|
141
|
+
`AuthManager` is public surface, its `logger` option stays optional, and a logger
|
|
142
|
+
that *is* supplied must now carry `warn`. The only non-test construction site in
|
|
143
|
+
this repo passes the kernel `Logger`, whose `warn` is already required.
|
|
144
|
+
|
|
145
|
+
`ReportService` and `AutoEnqueuer` additionally stopped defaulting their logger
|
|
146
|
+
field to `{}`. The field is now honestly optional rather than holding an empty
|
|
147
|
+
object that declared it could report and discarded everything. Behaviour is
|
|
148
|
+
unchanged in both directions.
|
|
149
|
+
|
|
150
|
+
<!-- adr-0087: not-required (runtime-interface-only packages/plugins/plugin-auth/src/auth-manager.ts#AuthManagerOptions, packages/plugins/plugin-auth/src/reconcile-membership.ts#ReconcileMembershipDeps, packages/metadata-protocol/src/migrations/partial-index-probe.ts#IndexMigrationLogger, packages/plugins/plugin-audit/src/auth-event-audit.ts#AuthEventAuditLogger, packages/plugins/plugin-audit/src/read-audit.ts#ReadAuditLogger, packages/plugins/plugin-reports/src/report-service.ts#ReportServiceOptions, packages/services/service-knowledge/src/knowledge-service.ts#KnowledgeLogger) every tightened type is a plain TypeScript logger interface -- no Zod projection, no metadata surface, and none is referenced by one -- so `objectstack migrate meta` has nothing to rewrite. Nothing is removed or renamed and no stored value moves; the only consumer action is adding a `warn` member at a construction site the compiler names. -->
|
|
151
|
+
|
|
152
|
+
### Patch Changes
|
|
153
|
+
|
|
154
|
+
- 07bd1ca: Apply the subject object's field-level read controls to the approval payload
|
|
155
|
+
snapshot at serve time (#10749), so an approver no longer receives fields the
|
|
156
|
+
app author declared they may not read.
|
|
157
|
+
|
|
158
|
+
`sys_approval_request.payload_json` stores the submitted record's raw row,
|
|
159
|
+
captured from the flow's `$record` variable — which the automation layer hands
|
|
160
|
+
over with the record's own FLS never applying. The column is a `textarea` on
|
|
161
|
+
`sys_approval_request`, so to every read door it is an opaque string: the
|
|
162
|
+
field-visibility machinery governs *columns of objects* and cannot see inside a
|
|
163
|
+
JSON column. Every field-level read control declared on the SUBJECT object —
|
|
164
|
+
`requiredPermissions` (ADR-0066 D3), a permission set marking a field
|
|
165
|
+
non-readable, a `maskingRule` — was therefore unenforceable on the approval
|
|
166
|
+
path, for every app.
|
|
167
|
+
|
|
168
|
+
Per the maintainer's ruling (2026-08-22, Option B) the full snapshot **stays at
|
|
169
|
+
rest**: the approval record remains audit evidence of what was actually
|
|
170
|
+
submitted, which write-time trimming would have given away. Redaction happens at
|
|
171
|
+
**serve** time, keyed on the reading caller, so the same row answers an admin
|
|
172
|
+
with the whole snapshot and a restricted approver with only the fields they may
|
|
173
|
+
read. The readable set is not recomputed — it comes from the security service's
|
|
174
|
+
`getReadableFields`, documented as the same field mask the read middleware
|
|
175
|
+
applies, so this seam cannot drift from data-plane FLS.
|
|
176
|
+
|
|
177
|
+
Two doors are covered, because `payload_json` has two independent readers and a
|
|
178
|
+
seam covering one manufactures the belief that the path is masked:
|
|
179
|
+
|
|
180
|
+
- the **service door** (`getRequest` / `listRequests`, behind
|
|
181
|
+
`GET /api/v1/approvals/requests[/:id]`), which serves the parsed `payload`.
|
|
182
|
+
Redaction runs BEFORE display enrichment, so `payload_display` and
|
|
183
|
+
`payload_labels` — both built by walking the snapshot's own keys — cannot ship
|
|
184
|
+
a restricted field's name, its authored label, or the title of the record it
|
|
185
|
+
points at;
|
|
186
|
+
- the **generic data door**: the object declares
|
|
187
|
+
`enable.apiMethods: ['get','list']`, so a plain `find`/`findOne` returns the
|
|
188
|
+
raw string without the service ever running. Covered by object-scoped engine
|
|
189
|
+
middleware, which reaches the whole family sharing that producer (REST data
|
|
190
|
+
routes, ObjectQL, CSV/XLSX export, MCP). Middleware rather than an `afterFind`
|
|
191
|
+
hook on purpose: a hook receives `buildSession`'s output, which carries no
|
|
192
|
+
`onBehalfOf`, so a hook-based seam would drop the ADR-0090 D10 delegator
|
|
193
|
+
intersection and answer a delegated read more permissively than the service.
|
|
194
|
+
|
|
195
|
+
**Behaviour change, argued rather than assumed.** A non-admin caller that was
|
|
196
|
+
reading restricted keys out of the snapshot now receives fewer keys, and that is
|
|
197
|
+
a real change for such a consumer. It is shipped as a fix rather than a breaking
|
|
198
|
+
change because those fields were never that caller's to read: the approval path
|
|
199
|
+
was a bypass of a declaration the platform enforces everywhere else, and the
|
|
200
|
+
served type is `payload?: unknown` — never a promised field set. This follows the
|
|
201
|
+
`__search` companion strip (#7642), which shipped the same way on the same
|
|
202
|
+
reasoning. Object-level access is deliberately untouched: an approver commonly
|
|
203
|
+
holds no read grant on the object under approval at all, and
|
|
204
|
+
`getReadableFields` answers a caller with no field-permission entries with the
|
|
205
|
+
full set, so every approval drawer shipping today keeps rendering.
|
|
206
|
+
|
|
207
|
+
`hidden: true` is deliberately NOT acted on. It is a UI contract ("Hidden from
|
|
208
|
+
default UI") which, in the spec's own words, "has never governed serialization"
|
|
209
|
+
— measurably: no read path in the repo strips a value on it. Enforcing it here
|
|
210
|
+
alone would make the approval path stricter than a direct read of the same row
|
|
211
|
+
(closing no leak, since the approver can simply read the record) while breaking
|
|
212
|
+
drawers that render a `hidden` business column. That is a `packages/spec`
|
|
213
|
+
semantics question and is left open.
|
|
214
|
+
- f0d7647: **Deliberate search-semantics change.** `ApprovalService.listRequests` /
|
|
215
|
+
`countRequests` no longer push a free-text predicate onto the payload snapshot
|
|
216
|
+
column for a caller whose view of that snapshot is masked (#11040).
|
|
217
|
+
|
|
218
|
+
Since #10749 the approval snapshot (`sys_approval_request.payload_json`, the
|
|
219
|
+
submitted record's row) is redacted **at serve time, per reader**: each row is
|
|
220
|
+
cut down to the fields that caller may read on that row's subject object. The
|
|
221
|
+
full row deliberately stays at rest, so the approval record remains audit
|
|
222
|
+
evidence of what was actually submitted.
|
|
223
|
+
|
|
224
|
+
The free-text filter, however, is evaluated by the driver against the stored
|
|
225
|
+
column — before anything is served, and therefore against the unmasked bytes. A
|
|
226
|
+
predicate over that column is a question about its contents whose answer is row
|
|
227
|
+
membership, so the field-level read controls #10749 enforces on the way out did
|
|
228
|
+
not hold on the way in. That is `declared ≠ enforced`, and the platform has
|
|
229
|
+
already settled the governing principle for it: under `maskingRule` (#8993) a
|
|
230
|
+
field a caller sees masked is non-filterable, refused loudly, because otherwise
|
|
231
|
+
equality probes reconstruct the hidden span. This extends that settled posture
|
|
232
|
+
to the snapshot column, which is reached through a different door.
|
|
233
|
+
|
|
234
|
+
**What changes.** For a caller whose view of the snapshot is masked, free-text
|
|
235
|
+
search matches on `process_name`, `object_name`, `record_id` and `submitter_id`
|
|
236
|
+
— the columns of `sys_approval_request` itself, which anyone who can see the row
|
|
237
|
+
reads whole — and no longer on snapshot contents. Such a caller can still find a
|
|
238
|
+
request by process, object, record id or submitter; they can no longer find one
|
|
239
|
+
by a value they may not read. Rows returned, their order and pagination are
|
|
240
|
+
otherwise untouched, and no query is refused: the change only ever removes one
|
|
241
|
+
disjunct, never denies.
|
|
242
|
+
|
|
243
|
+
**What does not change.** A caller the serve path hands the whole snapshot to
|
|
244
|
+
keeps today's behaviour exactly — same rows, same order. That includes every
|
|
245
|
+
deployment that has not wired a field-visibility authority (the seam is
|
|
246
|
+
late-bound, and absent it snapshots are served unredacted), and the case where a
|
|
247
|
+
wired authority declines to narrow. Consistency with the serve path is the rule
|
|
248
|
+
here rather than blanket fail-closed: where serve hands over the whole snapshot,
|
|
249
|
+
keeping the predicate discloses nothing serve does not already disclose.
|
|
250
|
+
|
|
251
|
+
The masked/unmasked verdict is read from **the same authority and the same
|
|
252
|
+
per-caller call the serve path uses**, asked as the caller. It is deliberately
|
|
253
|
+
not a second, independently derived notion of "redacted" — two derivations drift,
|
|
254
|
+
and the drift between a serve rule and a filter rule is exactly what this fixes.
|
|
255
|
+
|
|
256
|
+
Because redaction is decided per row while a filter is built before any row
|
|
257
|
+
exists, the predicate-time scope matters: with an `object` filter the subject
|
|
258
|
+
object is known and the seam is asked about it directly; without one the query
|
|
259
|
+
spans every object, nothing sound can be asked, and the disjunct is dropped.
|
|
260
|
+
|
|
261
|
+
Held by `approval-free-text-scope.test.ts`.
|
|
262
|
+
- 6d5c4fa: Release these plugins' resources from `destroy()`, the teardown hook the kernel
|
|
263
|
+
actually calls (#10371). `Plugin` declares `init()`, `start?(ctx)` and
|
|
264
|
+
`destroy?()` — and no `stop()` — so `ObjectKernel.performShutdown()` and
|
|
265
|
+
`LiteKernel.destroy()`, which walk the plugins in reverse calling
|
|
266
|
+
`plugin.destroy()`, walked straight past every plugin whose teardown was spelled
|
|
267
|
+
`stop()`. `await kernel.shutdown()` resolved with the reports dispatcher still
|
|
268
|
+
armed, the REST/OpenAPI/Slack connectors still registered on the automation
|
|
269
|
+
engine, the approvals SLA escalation job still scheduled, and the knowledge
|
|
270
|
+
event-sync subscription still open.
|
|
271
|
+
|
|
272
|
+
Each teardown body now lives in `destroy()`. `stop()` is retained as a
|
|
273
|
+
delegating alias with its parameter made optional, so an embedder that learned
|
|
274
|
+
to call it directly — precisely because the kernel never did — keeps working
|
|
275
|
+
unchanged. No export is removed and the `Plugin` interface is untouched.
|
|
276
|
+
|
|
277
|
+
Same defect as #9371 in `@objectstack/service-messaging`, which surfaced as
|
|
278
|
+
fully green test runs exiting 1 on `EnvironmentTeardownError` and being evicted
|
|
279
|
+
from the merge queue.
|
|
280
|
+
- aa765b9: **Who loses access:** members of a team belonging to a *different* organization
|
|
281
|
+
than the record being approved. Concretely — a request raised in `org_a` routed
|
|
282
|
+
to a `team` approver whose `sys_team.organization_id` is `org_b` used to place
|
|
283
|
+
every `sys_team_member` of that team into `pending_approvers`, giving them the
|
|
284
|
+
approve/reject buttons on a record they are not a tenant of. They no longer
|
|
285
|
+
enter the slate, and the step falls back to the dead `team:<id>` literal with
|
|
286
|
+
the existing `#3807` "expanded to nobody" warning — the same shape a cross-org
|
|
287
|
+
`position` approver has always produced (#10230).
|
|
288
|
+
|
|
289
|
+
`team` was the last approver expansion that resolved people without asking
|
|
290
|
+
which organization was asking; `department`, `position`, `org_membership_level`
|
|
291
|
+
and (since #10153) `manager` all do. The screen reads the team's own
|
|
292
|
+
`organization_id`, so it costs one row and a team that fails it never fans out.
|
|
293
|
+
|
|
294
|
+
**Who does not lose access**, deliberately: a team stamped with the request's
|
|
295
|
+
own organization; a team stamped with **no** organization (`organization_id:
|
|
296
|
+
null` on a platform object means "owned by no organization" — what a seed
|
|
297
|
+
writes, since a seed cannot know the id the runtime mints at boot); a team id
|
|
298
|
+
with no `sys_team` row at all; and any request that carries no organization —
|
|
299
|
+
all four leave routing exactly as it was, because the tenancy fact is absent
|
|
300
|
+
rather than negative.
|
|
301
|
+
|
|
302
|
+
⚠️ One externally observable accept→reject change beyond the routing itself:
|
|
303
|
+
under the non-default `onEmptyApprovers: 'fail'` policy, a node whose *sole*
|
|
304
|
+
approver was a cross-org team used to open a request and now throws
|
|
305
|
+
`NO_APPROVERS`. Under the default (`admin_rescue`) the node still opens.
|
|
306
|
+
- c5d0c2f: Screen expanded `team` approver members to the request's organization (#10547).
|
|
307
|
+
|
|
308
|
+
#10230 made a `team` approver prove the TEAM's tenancy, and deferred the
|
|
309
|
+
members on purpose. `sys_team_member` carries `team_id` and `user_id` and no
|
|
310
|
+
organization column, so a team that passed that screen still routed every user
|
|
311
|
+
id it listed — including a user whose only `sys_member` row is in another
|
|
312
|
+
organization. Measured on a fixture, not read off the schema: an `org_a`
|
|
313
|
+
request against an `org_a` team returned `["u_outsider","u_insider"]` with zero
|
|
314
|
+
`sys_member` reads.
|
|
315
|
+
|
|
316
|
+
The expansion now screens the members with the same provably-outside posture
|
|
317
|
+
the neighbouring screens pin, in ONE `$in` read for the whole slate:
|
|
318
|
+
|
|
319
|
+
- membership rows exist for the user and none is the request's organization
|
|
320
|
+
(present and NEGATIVE) — dropped, with a warning naming the users, the team
|
|
321
|
+
and both organizations;
|
|
322
|
+
- no membership rows, an unreadable `sys_member`, a possibly-truncated read, or
|
|
323
|
+
a request carrying no organization (ABSENT) — routing is left exactly as it
|
|
324
|
+
was, and the no-organization case performs no read at all.
|
|
325
|
+
|
|
326
|
+
Holding membership elsewhere is not disqualifying; holding none here is.
|
|
327
|
+
|
|
328
|
+
⚠️ Behaviour change, confined to one non-default policy: a node whose only
|
|
329
|
+
approver is a team staffed entirely by users provably outside the organization
|
|
330
|
+
now resolves to no one. Under the default `onEmptyApprovers: 'admin_rescue'` it
|
|
331
|
+
still opens, routed to the dead `team:<id>` literal as any unresolved slate is;
|
|
332
|
+
under `onEmptyApprovers: 'fail'` it now throws `NO_APPROVERS` where it
|
|
333
|
+
previously opened.
|
|
334
|
+
|
|
335
|
+
Residual condition on the security value: the screen can only act on tenancy
|
|
336
|
+
facts that exist. A deployment that stamps an organization on its approval
|
|
337
|
+
requests but does not materialize `sys_member` rows sees no change — by design,
|
|
338
|
+
since #3807 recorded what treating an absent fact as a negative one costs.
|
|
339
|
+
- Updated dependencies [8f04d9a]
|
|
340
|
+
- Updated dependencies [6936d07]
|
|
341
|
+
- Updated dependencies [59eb04d]
|
|
342
|
+
- Updated dependencies [9f05b7d]
|
|
343
|
+
- Updated dependencies [3b2af5e]
|
|
344
|
+
- Updated dependencies [7d2d112]
|
|
345
|
+
- Updated dependencies [5fa0d72]
|
|
346
|
+
- Updated dependencies [8cc8401]
|
|
347
|
+
- Updated dependencies [02b3b07]
|
|
348
|
+
- Updated dependencies [46d34ab]
|
|
349
|
+
- Updated dependencies [914c413]
|
|
350
|
+
- Updated dependencies [55809a0]
|
|
351
|
+
- Updated dependencies [ee2ff45]
|
|
352
|
+
- Updated dependencies [47cd3ec]
|
|
353
|
+
- Updated dependencies [52db1d1]
|
|
354
|
+
- Updated dependencies [5649efb]
|
|
355
|
+
- Updated dependencies [9d7d2de]
|
|
356
|
+
- Updated dependencies [c815c50]
|
|
357
|
+
- Updated dependencies [795ea05]
|
|
358
|
+
- Updated dependencies [2306a76]
|
|
359
|
+
- Updated dependencies [e5ea701]
|
|
360
|
+
- Updated dependencies [26f3588]
|
|
361
|
+
- Updated dependencies [a40dcc1]
|
|
362
|
+
- Updated dependencies [def0d3e]
|
|
363
|
+
- Updated dependencies [8d0bb79]
|
|
364
|
+
- Updated dependencies [5acb58d]
|
|
365
|
+
- Updated dependencies [2e3cf95]
|
|
366
|
+
- Updated dependencies [4c93387]
|
|
367
|
+
- Updated dependencies [504c8d5]
|
|
368
|
+
- Updated dependencies [a037f7c]
|
|
369
|
+
- Updated dependencies [3ee8ddf]
|
|
370
|
+
- Updated dependencies [16cef97]
|
|
371
|
+
- Updated dependencies [a79bd35]
|
|
372
|
+
- Updated dependencies [6ceaa4b]
|
|
373
|
+
- Updated dependencies [15ea214]
|
|
374
|
+
- Updated dependencies [de19489]
|
|
375
|
+
- Updated dependencies [c684d00]
|
|
376
|
+
- Updated dependencies [923c424]
|
|
377
|
+
- Updated dependencies [0ab81d1]
|
|
378
|
+
- Updated dependencies [1ec36b7]
|
|
379
|
+
- Updated dependencies [5f2e54c]
|
|
380
|
+
- Updated dependencies [189373b]
|
|
381
|
+
- Updated dependencies [35ad101]
|
|
382
|
+
- Updated dependencies [ceb33a9]
|
|
383
|
+
- Updated dependencies [dccbcec]
|
|
384
|
+
- Updated dependencies [05bc692]
|
|
385
|
+
- Updated dependencies [73d9795]
|
|
386
|
+
- Updated dependencies [8012960]
|
|
387
|
+
- Updated dependencies [266654d]
|
|
388
|
+
- Updated dependencies [f34f56b]
|
|
389
|
+
- Updated dependencies [f399618]
|
|
390
|
+
- Updated dependencies [75e9301]
|
|
391
|
+
- Updated dependencies [f334d66]
|
|
392
|
+
- Updated dependencies [2810695]
|
|
393
|
+
- @objectstack/platform-objects@17.2.0
|
|
394
|
+
- @objectstack/spec@17.2.0
|
|
395
|
+
- @objectstack/core@17.2.0
|
|
396
|
+
- @objectstack/metadata-core@17.2.0
|
|
397
|
+
- @objectstack/types@17.2.0
|
|
398
|
+
- @objectstack/formula@17.2.0
|
|
399
|
+
|
|
3
400
|
## 17.1.0
|
|
4
401
|
|
|
5
402
|
### Minor Changes
|