@objectstack/plugin-approvals 17.1.0 → 17.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.
- package/CHANGELOG.md +1288 -0
- package/dist/index.d.mts +1576 -587
- package/dist/index.d.ts +1576 -587
- package/dist/index.js +1145 -92
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1151 -93
- package/dist/index.mjs.map +1 -1
- package/package.json +18 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,1293 @@
|
|
|
1
1
|
# @objectstack/plugin-approvals
|
|
2
2
|
|
|
3
|
+
## 17.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- dda969c: fix(approvals): a deleted record's pending approvals auto-cancel instead of stranding in the inbox (#13568)
|
|
8
|
+
|
|
9
|
+
Deleting a record left every `pending` approval it had opened sitting in the
|
|
10
|
+
approvers' inbox — counted in the pending total, openable, and pointing at a
|
|
11
|
+
`record_id` that resolves to nothing. Nothing about it was module-specific:
|
|
12
|
+
an approval node that declares `lockRecord` blocks the EDIT, so "delete and
|
|
13
|
+
recreate" is the only route left to an author who needs to fix a submitted
|
|
14
|
+
record, and every such delete added another orphan. Maintainer ruling
|
|
15
|
+
2026-08-31 (`总监席第 5 场决裁批 #5`, verbatim 「同意」): pending requests
|
|
16
|
+
auto-cancel on record delete — status `cancelled` plus a machine-readable
|
|
17
|
+
reason, rows KEPT for audit, out of the pending count and the inbox's default
|
|
18
|
+
view.
|
|
19
|
+
|
|
20
|
+
**Graded `minor`, and deliberately not `patch`.** The repair itself is a
|
|
21
|
+
defect fix, but it lands by WIDENING two published vocabularies and adding a
|
|
22
|
+
declared column, and this repo's convention grades a shipped service's
|
|
23
|
+
accept-set/behaviour move as `minor`. **No `BREAKING` banner**: nothing is
|
|
24
|
+
narrowed and no metadata that used to be accepted is now refused — the one
|
|
25
|
+
consequence a consumer can feel is that `ApprovalStatus` and
|
|
26
|
+
`ApprovalActionKind` each gained a member, so an exhaustive `switch` with no
|
|
27
|
+
default, or a `satisfies Record<ApprovalStatus, …>` map outside this repo,
|
|
28
|
+
now has a case to add. That is the same shape `returned` had when ADR-0044
|
|
29
|
+
landed it.
|
|
30
|
+
|
|
31
|
+
**Spec (`@objectstack/spec/contracts/approval-service`)**
|
|
32
|
+
|
|
33
|
+
- `APPROVAL_STATUSES` gains `cancelled` (+ its `APPROVAL_STATUS_LABELS`
|
|
34
|
+
entry). Its own terminal state rather than a re-use of `recalled`: a recall
|
|
35
|
+
is an ACT by the submitter, and filing a platform-initiated void as one
|
|
36
|
+
attributes a withdrawal to a person who never performed it.
|
|
37
|
+
- New `APPROVAL_CANCEL_REASONS` / `ApprovalCancelReason` /
|
|
38
|
+
`APPROVAL_CANCEL_REASON_LABELS`, single entry `record_deleted`. A
|
|
39
|
+
VOCABULARY, not free text, because the reason has a non-human consumer (the
|
|
40
|
+
inbox and the tombstone presentation branch on it) — and a CLASS, per the
|
|
41
|
+
ruling's wording, so the next platform-initiated cancellation cause extends
|
|
42
|
+
this list instead of minting a second terminal status for itself.
|
|
43
|
+
- `APPROVAL_ACTION_KINDS` gains `cancel` — the only kind with no human actor,
|
|
44
|
+
by construction.
|
|
45
|
+
- `ApprovalRequestRow.cancel_reason` declared, optional-nullable.
|
|
46
|
+
|
|
47
|
+
**Plugin (`@objectstack/plugin-approvals`)**
|
|
48
|
+
|
|
49
|
+
- `sys_approval_request.cancel_reason`, a select derived from the contract
|
|
50
|
+
vocabulary and never re-typed (the #3786 rule the `status` column already
|
|
51
|
+
follows). On the row rather than on the audit entry, so a plain list view
|
|
52
|
+
can read WHY without joining the append-only action log.
|
|
53
|
+
- `bindRecordDeleteCancelHook` — a GLOBAL `afterDelete` registration beside
|
|
54
|
+
the existing global record-lock hook, so one platform-level linkage covers
|
|
55
|
+
every "approval + `lockRecord`" object at once. It needs no row-set
|
|
56
|
+
plumbing: the engine binds the deleted row's pre-image on the by-id path
|
|
57
|
+
and fans `afterDelete` out per matched row on a predicate delete, so a bulk
|
|
58
|
+
delete is covered by the same handler. The approvals tables are excluded at
|
|
59
|
+
registration, so they do not pay the delete-side pre-image read.
|
|
60
|
+
- `ApprovalService.cancelForDeletedRecord` writes the transition: one
|
|
61
|
+
append-only `sys_approval_action` row (`action: 'cancel'`, no actor),
|
|
62
|
+
`status: 'cancelled'` + `cancel_reason: 'record_deleted'` +
|
|
63
|
+
`completed_at`, and a `sys_approval_approver` index clear — that last one
|
|
64
|
+
is not optional garnish, it is what actually empties the inbox, because the
|
|
65
|
+
approver filter resolves through that index rather than through `status`.
|
|
66
|
+
- The `Completed` list view now includes `cancelled`, so a kept audit row is
|
|
67
|
+
visible in the one curated terminal view rather than only under `All`.
|
|
68
|
+
- ⛔ **No flow resume and no status mirror-back.** A cancellation is a status
|
|
69
|
+
write plus a reason, not a decision, so there is no branch to resume down.
|
|
70
|
+
The mirror-back is skipped by construction rather than by a swallowed
|
|
71
|
+
error: it is an `update_record` against the row that was just deleted — the
|
|
72
|
+
exact write this card's forensics caught failing elsewhere. The suspended
|
|
73
|
+
run the request gated is reported at `warn` with its id and otherwise left
|
|
74
|
+
alone; what becomes of it belongs to the automation service.
|
|
75
|
+
- ⛔ **The delete is never refused.** The "forbid delete while an approval is
|
|
76
|
+
pending" direction was vetoed in the same ruling — `lockRecord` already
|
|
77
|
+
blocks the edit, and blocking the delete too locks an author onto a record
|
|
78
|
+
they cannot fix. Nothing in the hook throws; a failure degrades to the
|
|
79
|
+
pre-existing state (the stale row) and is logged.
|
|
80
|
+
- Terminal rows are untouched. `approved` / `rejected` / `recalled` /
|
|
81
|
+
`returned` requests about the deleted record keep their recorded outcome —
|
|
82
|
+
history stays history, and rendering their now-dead record reference is a
|
|
83
|
+
separate console-side change.
|
|
84
|
+
|
|
85
|
+
zh-CN / ja-JP / es-ES bundles carry authored translations for the new leaves
|
|
86
|
+
(已作废 / 無効化済み / Anulada), not source fills.
|
|
87
|
+
- 277948f: feat(spec,plugin-approvals): `escalation.enabled` defaults to `true` and the SLA sweep finally reads it (#12278)
|
|
88
|
+
|
|
89
|
+
**BREAKING** semantic default flip on a published authorable key, shipped as
|
|
90
|
+
`minor` under the repo's launch-window convention for breaking changes.
|
|
91
|
+
Maintainer ruling 2026-08-27 (Option C), explicitly reversing the 2026-08-26
|
|
92
|
+
"spec stays as declared" ruling with fresh analysis.
|
|
93
|
+
|
|
94
|
+
`ApprovalEscalationSchema.enabled` declared `default(false)` while the
|
|
95
|
+
plugin-approvals escalation sweep never read the key: any escalation block
|
|
96
|
+
with a positive `timeoutHours` escalated, and with `action: 'auto_approve'`
|
|
97
|
+
that silently approved requests their author had declared off the clock —
|
|
98
|
+
the ADR-0049 declared-but-unenforced shape, failing open. Worse, the
|
|
99
|
+
approval-node executor parses node config through the schema before
|
|
100
|
+
snapshotting it onto the request row, so the old default **materialized**
|
|
101
|
+
`enabled: false` into storage for every author who omitted the key, making
|
|
102
|
+
"authored off" and "defaulted off" byte-identical at the sweep site.
|
|
103
|
+
|
|
104
|
+
One change, both halves:
|
|
105
|
+
|
|
106
|
+
- **spec**: `enabled` now defaults to `true` (stays `z.boolean()`; no
|
|
107
|
+
tri-state). The feature-level switch is whether an `escalation` block
|
|
108
|
+
exists at all; within a block carrying `timeoutHours`, escalation is on
|
|
109
|
+
unless explicitly turned off — which is what the runtime, its eleven
|
|
110
|
+
behaviour tests, and every teaching surface have always meant. Declared in
|
|
111
|
+
`DEFAULT_CHANGES_BY_MAJOR` (17) and registered as the
|
|
112
|
+
`approval-escalation-enabled-default-flip` semantic migration entry.
|
|
113
|
+
- **runtime**: `runEscalations` skips a request whose snapshot carries an
|
|
114
|
+
explicit `escalation.enabled === false` — the declared switch is enforced.
|
|
115
|
+
Request snapshots created **before** the flip cutoff
|
|
116
|
+
(`ESCALATION_ENABLED_FLIP_CUTOFF_MS`, 2026-08-28T00:00:00Z) ride a
|
|
117
|
+
read-side legacy window and keep escalating exactly as they do today: their
|
|
118
|
+
stored `false` is overwhelmingly the old schema default materialized onto an
|
|
119
|
+
author who never wrote the key, every such stored row is escalating today,
|
|
120
|
+
and the window retires itself as pending requests drain — zero tenant rows
|
|
121
|
+
rewritten.
|
|
122
|
+
|
|
123
|
+
Deployed metadata that omits `enabled` does not change behaviour (it
|
|
124
|
+
escalated before, it escalates after). What changes is that writing
|
|
125
|
+
`enabled: false` finally binds for newly opened requests — the declared
|
|
126
|
+
intent being honoured. To keep an SLA off, write `enabled: false`; to
|
|
127
|
+
escalate on timeout, an `escalation` block with `timeoutHours` is enough.
|
|
128
|
+
|
|
129
|
+
<!-- adr-0087: registered approval-escalation-enabled-default-flip -->
|
|
130
|
+
- 3690922: fix(plugin-approvals): the non-submitter recall refusal renders through the
|
|
131
|
+
Operation Message Catalog instead of a hardcoded English sentence (#11993, the
|
|
132
|
+
services-side half of the shape-A ruling)
|
|
133
|
+
|
|
134
|
+
A user who opened a record someone else had submitted for approval, clicked
|
|
135
|
+
Recall and was correctly refused read the reason in English regardless of their
|
|
136
|
+
own locale. `@objectstack/rest`'s `handleApprovalError` ships this service's
|
|
137
|
+
thrown reason as the 403 body's human-readable `error`, and Console splices it
|
|
138
|
+
under its own localized label — so an operator in a fully Chinese deployment
|
|
139
|
+
read a Chinese prefix glued onto an English sentence they could not act on
|
|
140
|
+
(`撤回审批失败: <English>`).
|
|
141
|
+
|
|
142
|
+
The refusal now renders through the shared Operation Message Catalog in
|
|
143
|
+
`@objectstack/spec/system` under the key `approval_recall_not_submitter` that
|
|
144
|
+
#12493 landed for it — the same mechanism `plugin-security`'s denial gates
|
|
145
|
+
already use, with the same resolution ladder (deployment override → the
|
|
146
|
+
caller's locale → `en` → the key) and the same guarantee that a misbehaving
|
|
147
|
+
i18n service cannot turn a 403 into a 500. All four platform locales (`en`,
|
|
148
|
+
`zh-CN`, `ja-JP`, `es-ES`) ship copy that names who *can* recall, rather than
|
|
149
|
+
dead-ending the reader.
|
|
150
|
+
|
|
151
|
+
`ApprovalServiceOptions` gains an optional `messageTranslator` — a lazily
|
|
152
|
+
resolved, `II18nService.t`-compatible lookup, wired by `ApprovalsServicePlugin`
|
|
153
|
+
the same way `tenancyPosture` and the field-visibility source are, because the
|
|
154
|
+
i18n service is contributed by another plugin and may start later. It is what
|
|
155
|
+
makes the override address the catalog documents,
|
|
156
|
+
`errors.approval_recall_not_submitter`, actually take effect for this emitter;
|
|
157
|
+
a stack without an i18n service still renders the built-in catalog in the
|
|
158
|
+
caller's locale.
|
|
159
|
+
|
|
160
|
+
**Not changed: who may recall an approval.** The gate is byte-identical — the
|
|
161
|
+
submitter, or a privileged admin releasing a stuck record (#3424). Only the
|
|
162
|
+
sentence the refusal carries is different, and the `FORBIDDEN:` code prefix
|
|
163
|
+
that the REST layer maps to 403 is untouched.
|
|
164
|
+
|
|
165
|
+
The button-visibility half of #11993 — a non-submitter seeing a live recall
|
|
166
|
+
button at all — is not addressed here; see the issue for the measurement.
|
|
167
|
+
- 599515d: Four more system objects declare their polymorphic pointer pair (#11386, ADR-0052 §5, adopting the carrier #11339 landed): `sys_audit_log.record_id`, `sys_approval_request.record_id`, `sys_record_share.record_id` and `sys_share_link.record_id` now carry `referenceVia: 'object_name'`. A seed row addressing one of these by the target's natural key resolves against the object its sibling column names, per row — so a packaged app can ship audit history, pending approvals, record grants and share links that actually attach to the records they are about, and the queries that give each row its meaning (the `{object_name, record_id}` index, the pending-request lock, the sharing middleware's grant lookup, the share link's fail-closed record-existence gate) match on the target's real id.
|
|
168
|
+
|
|
169
|
+
The accept/reject contract changes with it on those four objects, deliberately and in the already-ruled direction: an unresolvable pointer on a DECLARED pair is a loud, counted failure instead of the old silent verbatim store. On a grant table that is the sharper win — a share whose `record_id` stayed a natural key enforced nothing while displaying as a grant, and was then deleted by the orphan sweep for describing a record that does not exist. Internal-id-shaped values still pass through verbatim, so a demo row about an already-deleted record (an `action: 'delete'` audit row) stays authorable. Undeclared text columns are untouched.
|
|
170
|
+
|
|
171
|
+
The fifth object surveyed, `sys_automation_run` (`trigger_object` / `trigger_record_id`), deliberately STAYS UNDECLARED. Its pair has the same shape but its rows are not content about a record: a `paused` row is a live continuation the engine rehydrates on boot, terminal rows are telemetry under a 30-day sweep, and the object has no natural key to address rows by. The verdict, its reasons, and what would have to change to flip it are recorded on the field itself and pinned by a test.
|
|
172
|
+
|
|
173
|
+
### Patch Changes
|
|
174
|
+
|
|
175
|
+
- 1007134: fix(approvals): the #3424 override reaches a `pending` request only — override-recall of a `returned` request is now refused (#12775)
|
|
176
|
+
|
|
177
|
+
**Behaviour change, declared explicitly (maintainer ruling 2026-09-02) so that
|
|
178
|
+
one revert of this changeset's PR restores the previous behaviour.**
|
|
179
|
+
|
|
180
|
+
`ApprovalService.recall` admits two callers: the submitter, and a platform or
|
|
181
|
+
tenant admin releasing a stuck request (the #3424 privileged override). Recall
|
|
182
|
+
is also valid on the LATEST `returned` request of a run — the ADR-0044 revise
|
|
183
|
+
window, where the submitter abandons the revision instead of resubmitting.
|
|
184
|
+
Those two rules met above the state check: the override short-circuit carried
|
|
185
|
+
no status test of its own, so an override actor could recall a `returned`
|
|
186
|
+
request too. Nothing else on the platform said so — `isOverrideActor`'s doc
|
|
187
|
+
block names a PENDING request, `attachViewers` computes
|
|
188
|
+
`viewer.can_override` as `status === 'pending' && isOverrideActor(...)`, and
|
|
189
|
+
the `approval_recall` action's override arm reads that flag — so the reach was
|
|
190
|
+
API-only, never offered by any UI, and pinned by nothing.
|
|
191
|
+
|
|
192
|
+
What changes:
|
|
193
|
+
|
|
194
|
+
- **Override-recall of a `returned` request is refused.** The override
|
|
195
|
+
short-circuit in `recall` now applies only while the request is `pending`,
|
|
196
|
+
spelled exactly as the viewer flag is computed. On `returned` an override
|
|
197
|
+
actor is judged exactly as any other non-submitter and receives the existing
|
|
198
|
+
refusal: `403 FORBIDDEN` over REST, with the operation catalog's
|
|
199
|
+
`approval_recall_not_submitter` sentence. No new error code, no new envelope.
|
|
200
|
+
- **Only `pending` requests are override-recallable** — the same scope as the
|
|
201
|
+
other three override levers (approve / reject / reassign are pending-only at
|
|
202
|
+
their endpoints), and the same scope the viewer flag has always declared.
|
|
203
|
+
|
|
204
|
+
What does not change:
|
|
205
|
+
|
|
206
|
+
- The submitter's own recall of a `returned` request (the ADR-0044 revise
|
|
207
|
+
window) is untouched; so is the submitter's recall of a `pending` one.
|
|
208
|
+
- The override actor's recall of a `pending` request — the #3424 rescue of a
|
|
209
|
+
request routed to an unstaffed position — is untouched, lock release and all.
|
|
210
|
+
- The refusal's message, wire code and developer log line keep their shape; the
|
|
211
|
+
log line now also names the request status it refused on.
|
|
212
|
+
|
|
213
|
+
Why: the gate now agrees with the viewer flag and the documented contract at
|
|
214
|
+
one point instead of disagreeing with both. If a real operator workflow
|
|
215
|
+
depended on override-recalling a `returned` request, this is the change to
|
|
216
|
+
revert; the `returned` record lock is already released, so the stuck-record
|
|
217
|
+
rescue motive that justifies the override does not apply on that status.
|
|
218
|
+
- b579b03: fix(approvals): refuse loudly when a successful mutation's read-back is org-filtered out (#12769)
|
|
219
|
+
|
|
220
|
+
Ten ApprovalService result sites (decide/decideNode, recall, sendBack, resubmit,
|
|
221
|
+
reassign, remind, requestInfo, comment) read the row they just mutated back
|
|
222
|
+
through the caller's organization narrowing and asserted the result non-null
|
|
223
|
+
(`fresh!`). For an org-less request row — produced by construction on every
|
|
224
|
+
schedule / time-relative / api trigger run (#10131; pinned rather than repaired
|
|
225
|
+
by #9132) — an org-scoped caller's read-back matches nothing, so a call that
|
|
226
|
+
SUCCEEDED shipped a well-formed success envelope whose declared-non-null
|
|
227
|
+
`request` was `null` (HTTP 200 with `"request": null` through the REST
|
|
228
|
+
pass-through), and a client dereferencing `request.status` crashed.
|
|
229
|
+
|
|
230
|
+
The read-back now throws `READ_BACK_FAILED: …` instead: the write is recorded
|
|
231
|
+
and NOT rolled back; only the result echo is refused, loudly. Over REST the
|
|
232
|
+
error surfaces through each route's existing 500 arm (`APPROVAL_RECALL_FAILED`
|
|
233
|
+
and siblings — already-registered codes) with the `READ_BACK_FAILED:` message
|
|
234
|
+
in the body. No declared result type changed — the declared-non-null `request`
|
|
235
|
+
is now always true because a result that cannot be built is never returned. The
|
|
236
|
+
caller-org narrowing in `loadRequest` (tenancy wall) is deliberately untouched.
|
|
237
|
+
- 2a05851: fix(plugin-approvals): the `approval_recall` action shows for the #3424 admin override (#12716)
|
|
238
|
+
|
|
239
|
+
`ApprovalService.recall` has admitted two callers since #3424 — the submitter,
|
|
240
|
+
and a platform/tenant admin releasing a stuck request — and `isOverrideActor`'s
|
|
241
|
+
own doc block names recall as one of the four override levers in so many words.
|
|
242
|
+
The declared action that reaches that endpoint did not agree: `approval_recall`'s
|
|
243
|
+
`visible` predicate was submitter-only, while its three siblings
|
|
244
|
+
(`approval_approve` / `approval_reject` / `approval_reassign`) each OR in
|
|
245
|
+
`record.viewer.can_override`.
|
|
246
|
+
|
|
247
|
+
So recall was the one lever the override covers whose button never appeared. An
|
|
248
|
+
admin rescuing an approval routed to an unstaffed position could approve or
|
|
249
|
+
reject their way out — writing a decision nobody made — or reassign it, but
|
|
250
|
+
could not simply withdraw it. This is declared-vs-enforced drift in the less
|
|
251
|
+
usual direction: a capability the server grants that no UI entry exposed.
|
|
252
|
+
|
|
253
|
+
`approval_recall`'s `visible` now ORs in `record.viewer.can_override`, spelled
|
|
254
|
+
byte-identically to the three siblings.
|
|
255
|
+
|
|
256
|
+
Not a permission change: the service's authorisation set is untouched, and
|
|
257
|
+
`can_override` was already computed server-side for every viewer.
|
|
258
|
+
|
|
259
|
+
**Pending-only, and enforced rather than asserted.** The new arm carries no
|
|
260
|
+
status test of its own — neither do the siblings — because the flag is already
|
|
261
|
+
status-scoped where it is computed: `attachViewers` sets
|
|
262
|
+
`can_override: row.status === 'pending' && isOverrideActor(...)`, ANDed, so the
|
|
263
|
+
flag can never be true off `pending` and the arm is pending-only in effect
|
|
264
|
+
however CEL groups the expression. The submitter's own `returned` (revise
|
|
265
|
+
window) arm is unchanged. Pinned in both directions, with the flag's own scoping
|
|
266
|
+
pinned against the real service on a genuinely `returned` row.
|
|
267
|
+
- 9bc403e: Correct what `sys_approval_request.payload_json` is documented to be FOR — it is audit evidence served redacted per reader, not a notification source
|
|
268
|
+
|
|
269
|
+
The object's module docstring — which ships to consumers in the package's type
|
|
270
|
+
declarations — justified the snapshot column with: *"used by notifications so
|
|
271
|
+
they can render before the record is locked or changed."* That consumer does
|
|
272
|
+
not exist. Measured against every `this.notify(...)` call site in
|
|
273
|
+
`approval-service.ts`, all **12** of them, each passes a payload of
|
|
274
|
+
`{ title, message, actionUrl }` (two also carry `actions`), built from
|
|
275
|
+
`object_name` / `record_id` and the caller's own comment. **None** reads
|
|
276
|
+
`payload_json` or the parsed `payload`.
|
|
277
|
+
|
|
278
|
+
This is more than tidiness: that sentence was the only documented
|
|
279
|
+
justification for the column holding a *full* row, and it was cited as such
|
|
280
|
+
during the #10749 consumer inventory before anyone checked it. The docstring
|
|
281
|
+
now states the real reason — the snapshot is retained as **audit evidence of
|
|
282
|
+
what was actually submitted**, so the column stays whole at rest, and is served
|
|
283
|
+
**redacted per reader** by the subject object's field-level read controls via
|
|
284
|
+
`getReadableFields`, on the approvals-inbox door and the generic data door
|
|
285
|
+
alike (#11039).
|
|
286
|
+
|
|
287
|
+
The field's own `description` is deliberately unchanged: `Record snapshot at
|
|
288
|
+
submission time` is accurate, and it — unlike the JSDoc — is the string
|
|
289
|
+
extracted into the four generated i18n bundles, so no translation leaf moves
|
|
290
|
+
and no locale is left holding an English seed.
|
|
291
|
+
|
|
292
|
+
Also carried in the same pass, the residual documentation the #10749 closure
|
|
293
|
+
assigned to the next docs touch in this lane: `payload-redaction.ts` recorded
|
|
294
|
+
`hidden`-vs-serialization as an **open** `packages/spec` question, and it has
|
|
295
|
+
since been ruled (maintainer, 2026-08-24, applying the 2026-08-12 lineage).
|
|
296
|
+
That paragraph now states the ruling — **`hidden: true` stays UI-only;
|
|
297
|
+
`internal: true` is the serialization primitive** — so an author who needs a
|
|
298
|
+
field kept out of read results is pointed at `internal: true` (#7728,
|
|
299
|
+
ADR-0049) rather than at `hidden`, which never governed serialization.
|
|
300
|
+
|
|
301
|
+
Documentation only: no runtime behaviour, no schema field, and no public type
|
|
302
|
+
signature changes.
|
|
303
|
+
- 59c0891: fix(plugin-approvals): the stranded-request inspection now sees a run that FAILED mid-resume (#13909)
|
|
304
|
+
|
|
305
|
+
`ApprovalService.inspectStrandedRequests` was structurally blind to the shape
|
|
306
|
+
#13909 owns, and reported `0` for it — the one shape an operator most needs to
|
|
307
|
+
see.
|
|
308
|
+
|
|
309
|
+
**The mechanism.** `AutomationEngine.resumeInternal` consumes the suspension
|
|
310
|
+
*before* running the downstream nodes: `forgetSuspendedRun(run, 'resumed')`
|
|
311
|
+
precedes `traverseNext`. A downstream node that merely THREW therefore threw
|
|
312
|
+
with the pause already gone, the catch arm recorded the run `failed`, and
|
|
313
|
+
nothing can resume it again (`resume` answers `RUN_NOT_FOUND`, `cancelRun` is a
|
|
314
|
+
no-op). The decision is durable and the flow stopped half-way.
|
|
315
|
+
|
|
316
|
+
**Why the inspection could not see it.** Its second oracle was
|
|
317
|
+
`if (terminal) continue` — the existence of ANY run-history row ended the check,
|
|
318
|
+
on the reading "the run ran to a terminal state, it is not dangling". But the
|
|
319
|
+
terminal row here is written BY the failure that stranded the request, so the
|
|
320
|
+
evidence of the defect was read as evidence of health. `releaseDeadRunRequests`
|
|
321
|
+
cannot see it either: it scans `status: 'pending'`, and the decision is what
|
|
322
|
+
took the row out of `pending`.
|
|
323
|
+
|
|
324
|
+
**The widening, and its limits.** The second oracle now classifies the run
|
|
325
|
+
instead of merely detecting it. A `failed` run is reported; `completed`,
|
|
326
|
+
`cancelled` and `paused` are each still skipped, one named reason at a time, and
|
|
327
|
+
a status this code does not recognise is skipped too — the spec's
|
|
328
|
+
`ExecutionStatus` vocabulary is wider than the four statuses the engine writes,
|
|
329
|
+
and a future status must not become a silent false positive. `paused` in
|
|
330
|
+
particular stays skipped because "the suspension is gone but no terminal row is
|
|
331
|
+
written yet" is exactly what a resume IN FLIGHT looks like. The first oracle is
|
|
332
|
+
unchanged: a run the suspension store still holds is alive, and an unreadable
|
|
333
|
+
store is still counted `undetermined`, never condemned.
|
|
334
|
+
|
|
335
|
+
Reported rows now carry `runState: 'missing' | 'failed'` (new exported type
|
|
336
|
+
`StrandedRunState`), because the two shapes need different remedies: a `missing`
|
|
337
|
+
run has no history to read, a `failed` one has a step log and an error naming
|
|
338
|
+
the node that threw. The sweep's own warning splits its counts the same way.
|
|
339
|
+
`StrandedApprovalRequest` is an output-only reporting shape the service
|
|
340
|
+
produces; the added field is not constructed by any caller in this repo.
|
|
341
|
+
|
|
342
|
+
**Still read-only, and still not a census.** No status is changed and no run is
|
|
343
|
+
cancelled — the decision genuinely happened. This makes the condition *visible*
|
|
344
|
+
in a deployment; how many runs are already in it can only be answered against
|
|
345
|
+
that deployment's own tables. Nothing here changes the resume ordering, which
|
|
346
|
+
is #13909's own next slice.
|
|
347
|
+
- 4bb09e8: feat(service-automation): an operator can put back a suspension a failed resume consumed (#13909)
|
|
348
|
+
|
|
349
|
+
A run that was resumed and whose downstream node merely **threw** was
|
|
350
|
+
terminally unresumable, and nothing anywhere could move it. The engine consumes
|
|
351
|
+
the suspension *before* running downstream nodes, so such a node throws with
|
|
352
|
+
the pause already gone and the catch arm records the run `failed`: `resume`
|
|
353
|
+
then answers `RUN_NOT_FOUND`, `cancelRun` is a no-op, and none of the engine's
|
|
354
|
+
other public methods takes the run anywhere. A deployment could enter that
|
|
355
|
+
state and never leave it.
|
|
356
|
+
|
|
357
|
+
`AutomationEngine.restoreConsumedSuspension(runId, { requestedBy, reason })`
|
|
358
|
+
is the exit. It puts the consumed suspension back — verbatim, as it stood at
|
|
359
|
+
the pause — so the run is resumable again through an ordinary `resume`, with
|
|
360
|
+
the same authority gate, the same screen validation and the same idempotency
|
|
361
|
+
guard as any other.
|
|
362
|
+
|
|
363
|
+
- **Deliberate, never automatic.** Nothing calls it on its own: no retry, no
|
|
364
|
+
sweeper. An operator asks for one run, by id.
|
|
365
|
+
- **Safe to refuse, with the reason named.** A run that is still suspended, one
|
|
366
|
+
whose resume is *in flight*, one that completed, one that was cancelled, one
|
|
367
|
+
that never suspended, and an unknown id each get their own refusal — as does
|
|
368
|
+
an unreadable store, which is refused rather than guessed at.
|
|
369
|
+
- **Idempotent.** A suspension is keyed by run id, so however many operators
|
|
370
|
+
ask there is one resumable pause and no extra traversal — the verb re-arms
|
|
371
|
+
and stops. Two racing callers in one process get one restore and one refusal.
|
|
372
|
+
- **It leaves a trace.** The restore is logged with the run, flow, node, when
|
|
373
|
+
the suspension was consumed, who asked and why, and the run is recorded
|
|
374
|
+
`paused` again so the repair is not invisible. Across a restart the exit
|
|
375
|
+
still works: the consumed suspension rides the run's own terminal history row
|
|
376
|
+
(in `sys_automation_run` columns that already existed), and a run that is
|
|
377
|
+
restored and then finishes clears it.
|
|
378
|
+
|
|
379
|
+
⚠️ A repair, not a prevention. The failed attempt's side effects are **not**
|
|
380
|
+
undone and the original resume signal is **not** replayed — the continuation
|
|
381
|
+
must be re-issued. Whether the pause should survive a downstream throw at all
|
|
382
|
+
is a separate, unruled decision (#13937); this change leaves the resume
|
|
383
|
+
ordering, `forgetSuspendedRun` and `traverseNext` exactly as they are, mints no
|
|
384
|
+
new run status, and works whichever way that is ruled — the runs already stuck
|
|
385
|
+
today are not released by changing what future resumes do.
|
|
386
|
+
|
|
387
|
+
`plugin-approvals` carries a comment correction only: its organization backfill
|
|
388
|
+
documented `context_json` as never written on terminal rows, which this change
|
|
389
|
+
makes false for that one class of row. No behaviour change there.
|
|
390
|
+
- 5a916c4: **Ops:** a one-off, idempotent backfill for the platform rows the pre-#10101 writers stranded with no organization — dry run first (#11308).
|
|
391
|
+
|
|
392
|
+
#10101 fixed the WRITERS: a `sys_approval_request` and a `sys_automation_run` are now stamped from the SUBJECT record's organization, with the acting context as the ruled fallback. It wrote nothing to existing rows, so the population produced before it persists — a **pending** org-less approval request LOCKS the record it is about while being invisible in every organization-scoped inbox, its own owner's included, and automation-run history stays unattributed. This is the repair for those rows, on the maintainer's 2026-08-23 ruling (direction 3).
|
|
393
|
+
|
|
394
|
+
`packages/plugins/plugin-approvals/src/backfill-platform-row-organizations.ts` sweeps `sys_approval_request` (with its `sys_approval_action` / `sys_approval_approver` children, which move with their request) and `sys_automation_run`. It scans only rows whose organization column is unset, re-reads each row's subject at repair time — live record first, the write-time snapshot (`payload_json` / `context_json`'s `record`) second for a subject that has since been deleted — and stamps the platform row with the subject's own organization.
|
|
395
|
+
|
|
396
|
+
**Dry run first, and by default.** `planPlatformRowOrganizationBackfill(engine)` reads only and returns a per-object report naming every row it would touch; `runPlatformRowOrganizationBackfill(engine, { dryRun: false })` writes. Nothing runs at boot and nothing is scheduled: this is an operator-invoked module, run once against an affected install.
|
|
397
|
+
|
|
398
|
+
**Rows whose subject is equally org-less are counted and named, never written.** The acting-context fallback the writers apply is not available to a repair — the acting context is gone — and inventing one stays vetoed. Those ids are reported so the population is checkable and stays visible.
|
|
399
|
+
|
|
400
|
+
**`sys_api_key`'s divergence is preserved, not flattened.** Both the column read on a subject and the column written on a platform row are resolved from the registered schema through the shared `createRecordOrganizationResolver` (`@objectstack/metadata-core`), so a platform row ABOUT an API key is repaired from that object's stamp-only `active_organization_id` (limb 0, #8778) and the credential table itself is never written to.
|
|
401
|
+
|
|
402
|
+
**Idempotent, and asserted rather than claimed.** Every scan is `WHERE <organization column> IS NULL` and every write fills that column, so a repaired row cannot match again; the test suite runs the sweep twice and pins the second run at zero writes.
|
|
403
|
+
|
|
404
|
+
Publishes no runtime code: the module is not exported from the package index and not bundled into `dist` (`tsup` builds `src/index.ts`). It is graded rather than skipped because the release notes are where an operator of an affected install learns the repair exists, what it will and will not touch, and that the dry run comes first.
|
|
405
|
+
- 30928a6: fix(i18n): read the provenance companion at serving time, not only record it (#12642)
|
|
406
|
+
|
|
407
|
+
Maintainer ruling #12069 Option A (#11671) landed translation provenance as
|
|
408
|
+
**two** halves: `os i18n extract --source-hashes` RECORDS which source revision
|
|
409
|
+
a generated leaf is still a byte copy of, and `withSourceFallback` READS those
|
|
410
|
+
records at serving time and substitutes the current source for a leaf whose
|
|
411
|
+
source has moved underneath it. The recording half was then rolled out to every
|
|
412
|
+
bundle set. The reading half was not — measured on `main`: provenance
|
|
413
|
+
**recorded in 9 of 9** bundle sets and **read at serving time in 1**.
|
|
414
|
+
|
|
415
|
+
The other eight assembled their `TranslationBundle` straight from the raw
|
|
416
|
+
generated modules and never consulted the companion sitting beside them, so
|
|
417
|
+
they recorded the drift and went on serving the superseded draft. Nothing said
|
|
418
|
+
so: `check:i18n` compares key sets and they still matched, `check:i18n-coverage`
|
|
419
|
+
counts a present leaf as translated, and `check:i18n-stale-fill`'s cross-locale
|
|
420
|
+
rule needs a SECOND locale holding the same stale bytes before it can testify.
|
|
421
|
+
The measured case had one locale and no second witness.
|
|
422
|
+
|
|
423
|
+
All eight are wired here, in the shape `@objectstack/platform-objects`'s own
|
|
424
|
+
`metadata-translations/index.ts` uses — the committed
|
|
425
|
+
`<locale>.source-hashes.generated.ts` passed as the fourth argument, the third
|
|
426
|
+
left `undefined` because these sets have no hand-authored sections. Provenance
|
|
427
|
+
is now recorded in 9 of 9 sets and served in 9 of 9.
|
|
428
|
+
|
|
429
|
+
`@objectstack/plugin-webhooks` was the last of them and is the only one whose
|
|
430
|
+
manifest changed: `withSourceFallback` lives in `@objectstack/platform-objects`,
|
|
431
|
+
which that package did not declare. It was **already in that package's install
|
|
432
|
+
closure** through `@objectstack/service-messaging`, so the edge declares a
|
|
433
|
+
resolution that already resolved rather than adding a package to the graph —
|
|
434
|
+
and relying on it undeclared would have been a phantom dependency under this
|
|
435
|
+
repo's strict package manager.
|
|
436
|
+
|
|
437
|
+
`check:i18n-stale-fill` gains a second verdict, **UNSERVED PROVENANCE**, so this
|
|
438
|
+
cannot silently come apart again: a bundle set that commits a companion and
|
|
439
|
+
does not consult it at serving time now fails the build, including a tenth set
|
|
440
|
+
that lands tomorrow.
|
|
441
|
+
|
|
442
|
+
**Graded `patch`, and the grade is the interesting part.** No API changes, no
|
|
443
|
+
new exported surface, and no key set moves — substitution was chosen over
|
|
444
|
+
deletion precisely so key-set claims stay put (ruling #8765 Option B). What
|
|
445
|
+
changes is which STRING a stale leaf serves. On this tree that is **zero
|
|
446
|
+
leaves**: a record is only ever written for a leaf that IS a byte copy of the
|
|
447
|
+
current source, so the companions arrive 0-stale by construction. The change is
|
|
448
|
+
in what happens the next time a source string moves — the reader sees the
|
|
449
|
+
English source rather than a superseded draft of it, which is the same
|
|
450
|
+
degradation an untranslated key already produces and not a new state.
|
|
451
|
+
- de47336: chore(i18n): roll the generated-leaf provenance companion out to the remaining bundle sets (#12559)
|
|
452
|
+
|
|
453
|
+
`os i18n extract --source-hashes` (#11671, maintainer ruling #12069 Option A)
|
|
454
|
+
records, per generated translation leaf, the digest of the source revision that
|
|
455
|
+
leaf is **still a byte copy of** — the one signal that tells a stale fill from a
|
|
456
|
+
real translation once the source has moved and the two stopped being
|
|
457
|
+
distinguishable by value. It shipped opt-in, and exactly one of the nine i18n
|
|
458
|
+
bundle sets opted in. A landed detector, a changeset announcing it and a green
|
|
459
|
+
gate read together as *"generated translation staleness is now caught"*; for
|
|
460
|
+
eight of nine sets it was not, and the thing making it not caught was a single
|
|
461
|
+
absent flag in an extract config — invisible from all three of those surfaces.
|
|
462
|
+
|
|
463
|
+
**All eight remaining sets now opt in** — `plugin-approvals`, `plugin-audit`,
|
|
464
|
+
`plugin-security`, `plugin-sharing`, `plugin-webhooks`, `service-messaging`,
|
|
465
|
+
`service-realtime`, `service-storage`. Each documents `source-hashes` in its
|
|
466
|
+
extract config and commits three `<locale>.source-hashes.generated.ts`
|
|
467
|
+
companions, produced by the same extract run as the bundles they sit beside
|
|
468
|
+
(`check:i18n` compares them byte-for-byte, so they cannot be written by hand).
|
|
469
|
+
`check:i18n` now reports 7 bundles per set where it reported 4, and 11 for
|
|
470
|
+
`platform-objects` where it reported 8.
|
|
471
|
+
|
|
472
|
+
**Records count what is currently RECORDABLE, never what is covered.** A record
|
|
473
|
+
is written only for a leaf that *is* right now a byte copy of the current
|
|
474
|
+
source, so a fully translated locale starts with an empty table — which is the
|
|
475
|
+
instrument armed, not an instrument that measures nothing: the entry appears by
|
|
476
|
+
itself on the first extract after a leaf becomes a fill. Measured at this
|
|
477
|
+
commit, per set over its three translated locales: `service-messaging` 289,
|
|
478
|
+
`plugin-approvals` 61, `plugin-security` 33, `plugin-webhooks` 20,
|
|
479
|
+
`plugin-audit` 8, `service-storage` 7, `plugin-sharing` 1 (es-ES only; zh-CN and
|
|
480
|
+
ja-JP are fully translated and start empty), `service-realtime` 0 (all three
|
|
481
|
+
locales fully translated). **419 records written across the eight sets, 0
|
|
482
|
+
stale.**
|
|
483
|
+
|
|
484
|
+
**One extractor fix the rollout forced.** `--source-hashes` had one user, and
|
|
485
|
+
that user commits both generated sections, so the interaction with
|
|
486
|
+
`--no-metadata-forms` had never been exercised. The provenance table is computed
|
|
487
|
+
over every generated section the extractor builds; the eight sets here commit no
|
|
488
|
+
metadata-forms bundle, and their `metadataForms` subtree — absent from their
|
|
489
|
+
merge baseline — arrives as a fresh `--fill=default` copy of `en`, so every leaf
|
|
490
|
+
of it was recordable. First measured on `plugin-audit`: **763 records, of which
|
|
491
|
+
2 were its own objects and 761 were digests of the Studio metadata-form baseline
|
|
492
|
+
`@objectstack/platform-objects` owns.** Those records are unreadable in the
|
|
493
|
+
package holding them and would have rewritten all 24 companions on any unrelated
|
|
494
|
+
`*.form.ts` change in `packages/spec` — the cross-package coupling ADR-0029 D8
|
|
495
|
+
and every `bundle-ownership.test.ts` keep out of committed bundles. The
|
|
496
|
+
companion now covers exactly the sections a run commits, decided by the same two
|
|
497
|
+
predicates that decide the bundle files. `platform-objects` commits both, so its
|
|
498
|
+
three committed companions are byte-for-byte unchanged.
|
|
499
|
+
|
|
500
|
+
**Grade: `patch`, and behaviour on the day it lands is unchanged for every
|
|
501
|
+
leaf.** A record is written only where a leaf is currently a byte copy of the
|
|
502
|
+
**current** source, so every record written equals the current digest and none
|
|
503
|
+
of them can be stale; the mechanism cannot arrive red. No committed translation
|
|
504
|
+
bundle changed a byte, no public API moved, and no leaf's rendered text changed.
|
|
505
|
+
`narrowToCommittedSections` is new but internal to `@objectstack/cli` — the
|
|
506
|
+
package's entrypoint does not re-export the extractor utils.
|
|
507
|
+
|
|
508
|
+
**What this does not do**, stated so the boundary is not inferred wrongly a
|
|
509
|
+
second time: these eight sets now *record* provenance. Reading it at serving
|
|
510
|
+
time is `withSourceFallback`, and that is still wired in
|
|
511
|
+
`@objectstack/platform-objects` alone — so a stale fill in one of the eight is
|
|
512
|
+
now recorded and reportable, but not yet substituted at runtime. Tracked
|
|
513
|
+
separately.
|
|
514
|
+
- ad54eb3: feat(tooling): onboard all 14 `packages/plugins/**` packages into `check:test-typecheck` (#14062)
|
|
515
|
+
|
|
516
|
+
Every plugin package now has a `tsconfig.test.json` compiled by the shared
|
|
517
|
+
`check:test-typecheck` gate, and its `typecheck` script names it. Before this,
|
|
518
|
+
the shrink-only `test-typecheck-debt.json` ratchet said **nothing** about a
|
|
519
|
+
third of the repo's runtime surface: 14 packages, 1 `tsconfig.test.json`
|
|
520
|
+
(`plugin-security`, wired directly to `tsc` rather than to the instrument), and
|
|
521
|
+
0 `check:test-typecheck` scripts.
|
|
522
|
+
|
|
523
|
+
Onboarded as a family by the director ruling of 2026-09-01 on #14062
|
|
524
|
+
(maintainer verbatim: 「同意」), which also carries the #5286 maintainer
|
|
525
|
+
authority the starting ledgers need. The smaller branch triage recommended —
|
|
526
|
+
declare the instrument's scope and re-site the two compile-time pins — was
|
|
527
|
+
recorded as considered and not taken: an instrument silent over a third of the
|
|
528
|
+
runtime surface is a hole readers generalise across, and that costs more than
|
|
529
|
+
fourteen tsconfigs.
|
|
530
|
+
|
|
531
|
+
**Measured, not assumed** (at `e80889095`, workspace closure built first). Four
|
|
532
|
+
packages carry residue and therefore a starting ledger — plugin-approvals 324
|
|
533
|
+
over 8 files, plugin-auth 94 over 10, plugin-sharing 3 over 2,
|
|
534
|
+
knowledge-ragflow 3 over 1. The other ten measure **zero** and deliberately get
|
|
535
|
+
no ledger file at all: the gate reads a missing ledger as `{ entries: {} }`, so
|
|
536
|
+
any error there is red immediately with no entry to be added to — strictly
|
|
537
|
+
stronger than a ledger holding nothing, and the call `plugin-security` had
|
|
538
|
+
already recorded for itself.
|
|
539
|
+
|
|
540
|
+
⛔ **This does not repair 345 type errors.** Per ruling item 3 it makes the
|
|
541
|
+
ratchet able to *see* them; paydown follows the ratchet's own shrink-only
|
|
542
|
+
discipline on its own cards. No test file is edited here.
|
|
543
|
+
|
|
544
|
+
Two corrections to the finding's own prose, both measured: the exclusion is
|
|
545
|
+
narrower than "no plugin package compiles its tests" — 9 of the 14 already
|
|
546
|
+
compiled their tests inside the `typecheck`-invoked build config, at zero
|
|
547
|
+
errors — and `exec-context-annotation.pin.ts` is a `.pin.ts`, which
|
|
548
|
+
`**/*.test.ts` never excluded, so its directives were already live. The pin
|
|
549
|
+
this change genuinely makes real is
|
|
550
|
+
`plugin-approvals/src/manager-org-screen-parity.contract.test.ts`, which no tsc
|
|
551
|
+
program had ever read.
|
|
552
|
+
- 1272f0a: Promote `resolveRecordOrganizationField` to the shared platform-row organization resolver (the cloud#1395 Option A ruling): a platform row's organization is the SUBJECT record's organization; actor context is the fallback, never the primary.
|
|
553
|
+
|
|
554
|
+
- `@objectstack/metadata-core` now owns the resolver (`resolveRecordOrganizationField`, `createFieldPresenceProbe`, and the new memoized `createRecordOrganizationResolver` factory) so all three sanctioned writers share one precedence.
|
|
555
|
+
- `@objectstack/plugin-approvals`: `openNodeRequest` stamps `sys_approval_request`, `sys_approval_action` and the `sys_approval_approver` index from the subject record's organization (acting context as fallback). Fixes the measured defect where every schedule / time-relative / api triggered approval persisted `organization_id = NULL` — locking the record it was about while being invisible in every inbox, its owner's included.
|
|
556
|
+
- `@objectstack/service-automation`: `sys_automation_run` rows (paused and terminal) resolve their organization from the trigger-record snapshot, with the acting tenant as fallback. Terminal rows previously never carried an organization at all.
|
|
557
|
+
- `@objectstack/plugin-audit`: the resolver moved out; the package re-exports it from the original paths, behavior unchanged.
|
|
558
|
+
|
|
559
|
+
The `sys_api_key` divergence is preserved and pinned: `tenancy.organizationField` (who a row is ABOUT) still wins over the tenant wall answer, and the credential table stays unwalled.
|
|
560
|
+
- Updated dependencies [809d417]
|
|
561
|
+
- Updated dependencies [387e231]
|
|
562
|
+
- Updated dependencies [f794e4e]
|
|
563
|
+
- Updated dependencies [cae2169]
|
|
564
|
+
- Updated dependencies [b812a54]
|
|
565
|
+
- Updated dependencies [2d4fa75]
|
|
566
|
+
- Updated dependencies [0e4e51b]
|
|
567
|
+
- Updated dependencies [e84bbf6]
|
|
568
|
+
- Updated dependencies [effae80]
|
|
569
|
+
- Updated dependencies [efb3513]
|
|
570
|
+
- Updated dependencies [d62f990]
|
|
571
|
+
- Updated dependencies [c45d8e6]
|
|
572
|
+
- Updated dependencies [2e3e8c7]
|
|
573
|
+
- Updated dependencies [e621291]
|
|
574
|
+
- Updated dependencies [655b106]
|
|
575
|
+
- Updated dependencies [40a93b5]
|
|
576
|
+
- Updated dependencies [101ad2c]
|
|
577
|
+
- Updated dependencies [d5b330d]
|
|
578
|
+
- Updated dependencies [dda969c]
|
|
579
|
+
- Updated dependencies [1f45690]
|
|
580
|
+
- Updated dependencies [277948f]
|
|
581
|
+
- Updated dependencies [8bdd955]
|
|
582
|
+
- Updated dependencies [54e2d36]
|
|
583
|
+
- Updated dependencies [b745157]
|
|
584
|
+
- Updated dependencies [f3bbbef]
|
|
585
|
+
- Updated dependencies [4f24e9d]
|
|
586
|
+
- Updated dependencies [e27583e]
|
|
587
|
+
- Updated dependencies [4bd6faa]
|
|
588
|
+
- Updated dependencies [86cbe37]
|
|
589
|
+
- Updated dependencies [6a180e4]
|
|
590
|
+
- Updated dependencies [474242f]
|
|
591
|
+
- Updated dependencies [63cd487]
|
|
592
|
+
- Updated dependencies [bd4aa4e]
|
|
593
|
+
- Updated dependencies [803eaab]
|
|
594
|
+
- Updated dependencies [f8e8f03]
|
|
595
|
+
- Updated dependencies [983edf1]
|
|
596
|
+
- Updated dependencies [eae824e]
|
|
597
|
+
- Updated dependencies [f6fa22c]
|
|
598
|
+
- Updated dependencies [8a483b3]
|
|
599
|
+
- Updated dependencies [3bc2e38]
|
|
600
|
+
- Updated dependencies [97bcd99]
|
|
601
|
+
- Updated dependencies [df59de0]
|
|
602
|
+
- Updated dependencies [96e25a8]
|
|
603
|
+
- Updated dependencies [713f83f]
|
|
604
|
+
- Updated dependencies [77d4b3c]
|
|
605
|
+
- Updated dependencies [f75a38a]
|
|
606
|
+
- Updated dependencies [7a25e7d]
|
|
607
|
+
- Updated dependencies [1fa05a6]
|
|
608
|
+
- Updated dependencies [c85a265]
|
|
609
|
+
- Updated dependencies [dcb10a5]
|
|
610
|
+
- Updated dependencies [773a999]
|
|
611
|
+
- Updated dependencies [35dffea]
|
|
612
|
+
- Updated dependencies [d8024f0]
|
|
613
|
+
- Updated dependencies [8120808]
|
|
614
|
+
- Updated dependencies [776a098]
|
|
615
|
+
- Updated dependencies [5060877]
|
|
616
|
+
- Updated dependencies [4f6325d]
|
|
617
|
+
- Updated dependencies [52954c0]
|
|
618
|
+
- Updated dependencies [2aa8456]
|
|
619
|
+
- Updated dependencies [d23ebb9]
|
|
620
|
+
- Updated dependencies [93809a3]
|
|
621
|
+
- Updated dependencies [7c0d0c3]
|
|
622
|
+
- Updated dependencies [daae7aa]
|
|
623
|
+
- Updated dependencies [8dc22d6]
|
|
624
|
+
- Updated dependencies [fa5d137]
|
|
625
|
+
- Updated dependencies [a392dbf]
|
|
626
|
+
- Updated dependencies [279431e]
|
|
627
|
+
- Updated dependencies [948dd6b]
|
|
628
|
+
- Updated dependencies [3b4c56c]
|
|
629
|
+
- Updated dependencies [ae8edd2]
|
|
630
|
+
- Updated dependencies [e25403c]
|
|
631
|
+
- Updated dependencies [a81aa9d]
|
|
632
|
+
- Updated dependencies [64baa68]
|
|
633
|
+
- Updated dependencies [9fa70d7]
|
|
634
|
+
- Updated dependencies [09db64a]
|
|
635
|
+
- Updated dependencies [92916e7]
|
|
636
|
+
- Updated dependencies [a84f3ea]
|
|
637
|
+
- Updated dependencies [f2eaae8]
|
|
638
|
+
- Updated dependencies [56c093c]
|
|
639
|
+
- Updated dependencies [c09451b]
|
|
640
|
+
- Updated dependencies [ba64877]
|
|
641
|
+
- Updated dependencies [e7191ce]
|
|
642
|
+
- Updated dependencies [7345308]
|
|
643
|
+
- Updated dependencies [79b6a22]
|
|
644
|
+
- Updated dependencies [30d96ab]
|
|
645
|
+
- Updated dependencies [f658793]
|
|
646
|
+
- Updated dependencies [0fd4899]
|
|
647
|
+
- Updated dependencies [c95ad19]
|
|
648
|
+
- Updated dependencies [e58ea8b]
|
|
649
|
+
- Updated dependencies [4a17645]
|
|
650
|
+
- Updated dependencies [3795c5f]
|
|
651
|
+
- Updated dependencies [8ab926b]
|
|
652
|
+
- Updated dependencies [7317cf2]
|
|
653
|
+
- Updated dependencies [e25e839]
|
|
654
|
+
- Updated dependencies [5997207]
|
|
655
|
+
- Updated dependencies [8b13cc8]
|
|
656
|
+
- Updated dependencies [00d8f65]
|
|
657
|
+
- Updated dependencies [4a4a35d]
|
|
658
|
+
- Updated dependencies [4a4a35d]
|
|
659
|
+
- Updated dependencies [86e765a]
|
|
660
|
+
- Updated dependencies [1d7e76a]
|
|
661
|
+
- Updated dependencies [53dc739]
|
|
662
|
+
- Updated dependencies [fd289be]
|
|
663
|
+
- Updated dependencies [03bf7b1]
|
|
664
|
+
- Updated dependencies [f90e820]
|
|
665
|
+
- Updated dependencies [18d816a]
|
|
666
|
+
- Updated dependencies [e8bd715]
|
|
667
|
+
- Updated dependencies [b91c351]
|
|
668
|
+
- Updated dependencies [a28a3c0]
|
|
669
|
+
- Updated dependencies [200d255]
|
|
670
|
+
- Updated dependencies [2852acc]
|
|
671
|
+
- Updated dependencies [daeaaf9]
|
|
672
|
+
- Updated dependencies [c459da6]
|
|
673
|
+
- Updated dependencies [e914733]
|
|
674
|
+
- Updated dependencies [1d8ad0f]
|
|
675
|
+
- Updated dependencies [9738c35]
|
|
676
|
+
- Updated dependencies [f887e52]
|
|
677
|
+
- Updated dependencies [881f8d8]
|
|
678
|
+
- Updated dependencies [3bfa1e6]
|
|
679
|
+
- Updated dependencies [0a8ebf3]
|
|
680
|
+
- Updated dependencies [901355c]
|
|
681
|
+
- Updated dependencies [34ce8e7]
|
|
682
|
+
- Updated dependencies [33681ea]
|
|
683
|
+
- Updated dependencies [bfe13c8]
|
|
684
|
+
- Updated dependencies [0fb3044]
|
|
685
|
+
- Updated dependencies [4635f3e]
|
|
686
|
+
- Updated dependencies [fd289be]
|
|
687
|
+
- Updated dependencies [ee3595c]
|
|
688
|
+
- Updated dependencies [09b4f4e]
|
|
689
|
+
- Updated dependencies [b2eab95]
|
|
690
|
+
- Updated dependencies [93940d4]
|
|
691
|
+
- Updated dependencies [3a04b01]
|
|
692
|
+
- Updated dependencies [45b9051]
|
|
693
|
+
- Updated dependencies [3954fb7]
|
|
694
|
+
- Updated dependencies [4805b56]
|
|
695
|
+
- Updated dependencies [b9e9227]
|
|
696
|
+
- Updated dependencies [d395692]
|
|
697
|
+
- Updated dependencies [5894d30]
|
|
698
|
+
- Updated dependencies [a3765f6]
|
|
699
|
+
- Updated dependencies [2d5cee3]
|
|
700
|
+
- Updated dependencies [e22158f]
|
|
701
|
+
- Updated dependencies [7404925]
|
|
702
|
+
- Updated dependencies [0c2334f]
|
|
703
|
+
- Updated dependencies [778c59f]
|
|
704
|
+
- Updated dependencies [d2619fd]
|
|
705
|
+
- Updated dependencies [af56546]
|
|
706
|
+
- Updated dependencies [6acb11a]
|
|
707
|
+
- Updated dependencies [33c5fd3]
|
|
708
|
+
- Updated dependencies [20b0fdb]
|
|
709
|
+
- Updated dependencies [905019b]
|
|
710
|
+
- Updated dependencies [a286411]
|
|
711
|
+
- Updated dependencies [98c0d33]
|
|
712
|
+
- Updated dependencies [368a82e]
|
|
713
|
+
- Updated dependencies [a3d5724]
|
|
714
|
+
- Updated dependencies [93ea19b]
|
|
715
|
+
- Updated dependencies [9ee2dcf]
|
|
716
|
+
- Updated dependencies [8cb96ec]
|
|
717
|
+
- Updated dependencies [8f10a79]
|
|
718
|
+
- Updated dependencies [6269a55]
|
|
719
|
+
- Updated dependencies [a17da05]
|
|
720
|
+
- Updated dependencies [a8c00e2]
|
|
721
|
+
- Updated dependencies [22e5236]
|
|
722
|
+
- Updated dependencies [0fb8760]
|
|
723
|
+
- Updated dependencies [e5ce2ed]
|
|
724
|
+
- Updated dependencies [be21955]
|
|
725
|
+
- Updated dependencies [bc56e18]
|
|
726
|
+
- Updated dependencies [be21955]
|
|
727
|
+
- Updated dependencies [a9ee989]
|
|
728
|
+
- Updated dependencies [4d0d944]
|
|
729
|
+
- Updated dependencies [15d58db]
|
|
730
|
+
- Updated dependencies [d63b014]
|
|
731
|
+
- Updated dependencies [9abe4e4]
|
|
732
|
+
- Updated dependencies [2cc7122]
|
|
733
|
+
- Updated dependencies [50d6c92]
|
|
734
|
+
- Updated dependencies [15d55fb]
|
|
735
|
+
- Updated dependencies [9e0ba21]
|
|
736
|
+
- Updated dependencies [311433f]
|
|
737
|
+
- Updated dependencies [3e5ad08]
|
|
738
|
+
- Updated dependencies [9abe4e4]
|
|
739
|
+
- Updated dependencies [b7131f3]
|
|
740
|
+
- Updated dependencies [e5812fa]
|
|
741
|
+
- Updated dependencies [7085f90]
|
|
742
|
+
- Updated dependencies [dee4dd4]
|
|
743
|
+
- Updated dependencies [ce7e497]
|
|
744
|
+
- Updated dependencies [51ecb2f]
|
|
745
|
+
- Updated dependencies [9086761]
|
|
746
|
+
- Updated dependencies [f6344e7]
|
|
747
|
+
- Updated dependencies [42a117b]
|
|
748
|
+
- Updated dependencies [1401ae7]
|
|
749
|
+
- Updated dependencies [4297fe7]
|
|
750
|
+
- Updated dependencies [e398863]
|
|
751
|
+
- Updated dependencies [d16df74]
|
|
752
|
+
- Updated dependencies [d79c602]
|
|
753
|
+
- Updated dependencies [f11fc61]
|
|
754
|
+
- Updated dependencies [e808890]
|
|
755
|
+
- Updated dependencies [8f79379]
|
|
756
|
+
- Updated dependencies [e6ca40e]
|
|
757
|
+
- Updated dependencies [0c77ea4]
|
|
758
|
+
- Updated dependencies [52954c0]
|
|
759
|
+
- Updated dependencies [89eb997]
|
|
760
|
+
- Updated dependencies [7131f12]
|
|
761
|
+
- Updated dependencies [aa5994e]
|
|
762
|
+
- Updated dependencies [be93457]
|
|
763
|
+
- Updated dependencies [a65db76]
|
|
764
|
+
- Updated dependencies [2cf5a96]
|
|
765
|
+
- Updated dependencies [15eb2c9]
|
|
766
|
+
- Updated dependencies [5691b07]
|
|
767
|
+
- Updated dependencies [2a6122b]
|
|
768
|
+
- Updated dependencies [225e769]
|
|
769
|
+
- Updated dependencies [8af88dd]
|
|
770
|
+
- Updated dependencies [fb5fbb8]
|
|
771
|
+
- Updated dependencies [d7b3963]
|
|
772
|
+
- Updated dependencies [33184fd]
|
|
773
|
+
- Updated dependencies [7c41693]
|
|
774
|
+
- Updated dependencies [b72db01]
|
|
775
|
+
- Updated dependencies [dce5cd4]
|
|
776
|
+
- Updated dependencies [9688f58]
|
|
777
|
+
- Updated dependencies [556ebc1]
|
|
778
|
+
- Updated dependencies [177ebdc]
|
|
779
|
+
- Updated dependencies [8d237b4]
|
|
780
|
+
- Updated dependencies [2d2e6f0]
|
|
781
|
+
- Updated dependencies [2d8dd8d]
|
|
782
|
+
- Updated dependencies [22d573e]
|
|
783
|
+
- Updated dependencies [b5a2398]
|
|
784
|
+
- Updated dependencies [348860c]
|
|
785
|
+
- Updated dependencies [5383fa6]
|
|
786
|
+
- Updated dependencies [5b3ff63]
|
|
787
|
+
- Updated dependencies [1a6a19c]
|
|
788
|
+
- Updated dependencies [064d484]
|
|
789
|
+
- Updated dependencies [527e050]
|
|
790
|
+
- Updated dependencies [dd33bf9]
|
|
791
|
+
- Updated dependencies [4cb2a90]
|
|
792
|
+
- Updated dependencies [74a7804]
|
|
793
|
+
- Updated dependencies [53d3689]
|
|
794
|
+
- Updated dependencies [b3a63d3]
|
|
795
|
+
- Updated dependencies [49f0dcf]
|
|
796
|
+
- Updated dependencies [033a34c]
|
|
797
|
+
- Updated dependencies [4d25d22]
|
|
798
|
+
- Updated dependencies [1ffee51]
|
|
799
|
+
- Updated dependencies [5ae4303]
|
|
800
|
+
- Updated dependencies [ece4dad]
|
|
801
|
+
- Updated dependencies [e9b377e]
|
|
802
|
+
- Updated dependencies [146f448]
|
|
803
|
+
- Updated dependencies [735f5c7]
|
|
804
|
+
- Updated dependencies [a7e18de]
|
|
805
|
+
- Updated dependencies [366f895]
|
|
806
|
+
- Updated dependencies [dc75ba8]
|
|
807
|
+
- Updated dependencies [cce0aa9]
|
|
808
|
+
- Updated dependencies [e764507]
|
|
809
|
+
- Updated dependencies [cff17af]
|
|
810
|
+
- Updated dependencies [39404f3]
|
|
811
|
+
- Updated dependencies [ca1965f]
|
|
812
|
+
- Updated dependencies [8619f95]
|
|
813
|
+
- Updated dependencies [b706af9]
|
|
814
|
+
- Updated dependencies [db8c288]
|
|
815
|
+
- Updated dependencies [0e5fe7f]
|
|
816
|
+
- Updated dependencies [add4360]
|
|
817
|
+
- Updated dependencies [e0abc38]
|
|
818
|
+
- Updated dependencies [fc9ba76]
|
|
819
|
+
- Updated dependencies [1272f0a]
|
|
820
|
+
- Updated dependencies [0f94cc7]
|
|
821
|
+
- Updated dependencies [a11c1a5]
|
|
822
|
+
- Updated dependencies [71f9cd1]
|
|
823
|
+
- Updated dependencies [ee17d86]
|
|
824
|
+
- Updated dependencies [cdbd920]
|
|
825
|
+
- Updated dependencies [18c432e]
|
|
826
|
+
- Updated dependencies [3c418c4]
|
|
827
|
+
- Updated dependencies [fa8715a]
|
|
828
|
+
- Updated dependencies [a933ed7]
|
|
829
|
+
- Updated dependencies [b3ca463]
|
|
830
|
+
- Updated dependencies [a933ed7]
|
|
831
|
+
- Updated dependencies [0d4a6a8]
|
|
832
|
+
- Updated dependencies [518d5e5]
|
|
833
|
+
- Updated dependencies [6643ba1]
|
|
834
|
+
- Updated dependencies [eeba2ef]
|
|
835
|
+
- Updated dependencies [ec4c4d2]
|
|
836
|
+
- Updated dependencies [424f73c]
|
|
837
|
+
- Updated dependencies [cccbe51]
|
|
838
|
+
- Updated dependencies [a8d6b1d]
|
|
839
|
+
- Updated dependencies [e4a7695]
|
|
840
|
+
- Updated dependencies [87075b1]
|
|
841
|
+
- Updated dependencies [fc58a99]
|
|
842
|
+
- Updated dependencies [14cfc00]
|
|
843
|
+
- Updated dependencies [1c6f7b4]
|
|
844
|
+
- Updated dependencies [e854a53]
|
|
845
|
+
- Updated dependencies [dfebfc8]
|
|
846
|
+
- Updated dependencies [598b7ec]
|
|
847
|
+
- Updated dependencies [d028b37]
|
|
848
|
+
- Updated dependencies [f7b25c5]
|
|
849
|
+
- Updated dependencies [122ef38]
|
|
850
|
+
- Updated dependencies [4a37870]
|
|
851
|
+
- Updated dependencies [428f9b2]
|
|
852
|
+
- Updated dependencies [aa7ff56]
|
|
853
|
+
- Updated dependencies [811a3c2]
|
|
854
|
+
- Updated dependencies [1401ae7]
|
|
855
|
+
- Updated dependencies [2fd3f1c]
|
|
856
|
+
- Updated dependencies [c41b42e]
|
|
857
|
+
- Updated dependencies [d41d166]
|
|
858
|
+
- Updated dependencies [c4db311]
|
|
859
|
+
- Updated dependencies [750fff5]
|
|
860
|
+
- Updated dependencies [c19035e]
|
|
861
|
+
- Updated dependencies [ececf7a]
|
|
862
|
+
- Updated dependencies [d173125]
|
|
863
|
+
- Updated dependencies [8eeca27]
|
|
864
|
+
- Updated dependencies [8425c17]
|
|
865
|
+
- Updated dependencies [a5ef1d8]
|
|
866
|
+
- Updated dependencies [87ad30c]
|
|
867
|
+
- Updated dependencies [772d5de]
|
|
868
|
+
- Updated dependencies [ce80ec2]
|
|
869
|
+
- Updated dependencies [b372318]
|
|
870
|
+
- Updated dependencies [97a2263]
|
|
871
|
+
- Updated dependencies [29d0676]
|
|
872
|
+
- Updated dependencies [0169d49]
|
|
873
|
+
- Updated dependencies [6bd3231]
|
|
874
|
+
- Updated dependencies [d2b5ba8]
|
|
875
|
+
- Updated dependencies [b799ac5]
|
|
876
|
+
- Updated dependencies [8f74307]
|
|
877
|
+
- Updated dependencies [d23dc08]
|
|
878
|
+
- Updated dependencies [038f333]
|
|
879
|
+
- Updated dependencies [644ad50]
|
|
880
|
+
- Updated dependencies [9735662]
|
|
881
|
+
- Updated dependencies [4d5b4f8]
|
|
882
|
+
- Updated dependencies [5d16379]
|
|
883
|
+
- Updated dependencies [0da7cd2]
|
|
884
|
+
- Updated dependencies [28a5c3e]
|
|
885
|
+
- Updated dependencies [4bc18e5]
|
|
886
|
+
- Updated dependencies [9f57f1e]
|
|
887
|
+
- @objectstack/spec@17.3.0
|
|
888
|
+
- @objectstack/platform-objects@17.3.0
|
|
889
|
+
- @objectstack/core@17.3.0
|
|
890
|
+
- @objectstack/types@17.3.0
|
|
891
|
+
- @objectstack/metadata-core@17.3.0
|
|
892
|
+
- @objectstack/formula@17.3.0
|
|
893
|
+
|
|
894
|
+
## 17.2.0
|
|
895
|
+
|
|
896
|
+
### Minor Changes
|
|
897
|
+
|
|
898
|
+
- b47ba2c: **BREAKING** (compile-time only): `ApprovalServiceOptions['logger']` now
|
|
899
|
+
declares a **non-optional** `warn`, so a durability report always has
|
|
900
|
+
somewhere to land (#9754, #10556). This is the thirteenth of the thirteen
|
|
901
|
+
mechanical repairs the card names — held out of #10691 to serialize against
|
|
902
|
+
PR #10547, which owned `approval-service.ts` while it was open; that fence
|
|
903
|
+
has since cleared.
|
|
904
|
+
|
|
905
|
+
`minor`, not `major`: during the launch window this stack ships breaking
|
|
906
|
+
changes as `minor` — every publishable package versions in lockstep, so a
|
|
907
|
+
`major` would promote the whole release. `patch` would be wrong in the other
|
|
908
|
+
direction, because this *can* break a consumer's build. This is the same
|
|
909
|
+
reasoning #10691 used for the twelve sibling repairs; no exemption for a
|
|
910
|
+
types-only break was found there either, and none applies here.
|
|
911
|
+
|
|
912
|
+
`error` stays optional — hosts legitimately inject reduced sinks, and
|
|
913
|
+
requiring `error` was measured and rejected as #9754 option C. What changes
|
|
914
|
+
is that its *absence* now has a declared, guaranteed destination. Call sites
|
|
915
|
+
keep the `logger?.warn?.(…)` spelling as the backstop for hosts the type
|
|
916
|
+
cannot reach, so **no runtime behaviour changes**: nothing that printed
|
|
917
|
+
before stops printing, and nothing silent starts printing.
|
|
918
|
+
|
|
919
|
+
### Who has to change, and what to do
|
|
920
|
+
|
|
921
|
+
Only a caller that constructs `ApprovalService` (or an `ApprovalServiceOptions`
|
|
922
|
+
value) with a `logger` object that has **no `warn` method** — for example
|
|
923
|
+
`{ error }` alone. Add a `warn` member; there is no rename, no removal, and no
|
|
924
|
+
stored value or metadata key to rewrite. The only non-test construction site
|
|
925
|
+
in this repo (`ApprovalsServicePlugin.start`, in this same package) passes the
|
|
926
|
+
kernel `ctx.logger`, whose `warn` is already required, so the in-repo cost is
|
|
927
|
+
zero.
|
|
928
|
+
|
|
929
|
+
<!-- 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. -->
|
|
930
|
+
- 13f533a: fix(approvals): screen the `manager` approver to the request's organization (#10153)
|
|
931
|
+
|
|
932
|
+
`expandApprovers` hands the directory organization to every graph-shaped
|
|
933
|
+
approver expansion — `department`, `position`, `org_membership_level`. The
|
|
934
|
+
`manager` branch did not: `lookupManager` read `sys_user.manager_id` under a
|
|
935
|
+
system context and took no organization argument at all. `sys_user` is a global
|
|
936
|
+
identity table with no `organization_id`, so nothing else on that path supplied
|
|
937
|
+
the tenancy fact either. A `manager_id` crossing an organization boundary
|
|
938
|
+
therefore routed the submission to an approver **in another organization** — an
|
|
939
|
+
out-of-tenant person granted approval authority over the record.
|
|
940
|
+
|
|
941
|
+
The same column has been screened on the hierarchy side since cloud#1195. This
|
|
942
|
+
brings the approvals consumer into line for the `manager` branch.
|
|
943
|
+
|
|
944
|
+
## What the screen is
|
|
945
|
+
|
|
946
|
+
`lookupManager(userId, organizationId)` now resolves the manager and then asks
|
|
947
|
+
whether he is **provably outside** the request's organization:
|
|
948
|
+
|
|
949
|
+
| membership rows for the manager | result |
|
|
950
|
+
|---|---|
|
|
951
|
+
| some exist, none in the request's org | **screened out** — the slot falls through to the `manager:<value>` literal |
|
|
952
|
+
| one is in the request's org | resolves, unchanged |
|
|
953
|
+
| none exist at all | resolves, unchanged — the tenancy fact is absent, not negative |
|
|
954
|
+
| the `sys_member` read failed | resolves, unchanged |
|
|
955
|
+
| the request carries no organization | resolves, unchanged — and no read is performed |
|
|
956
|
+
|
|
957
|
+
The fail-open half is this file's ruled posture on addressing paths, stated
|
|
958
|
+
twice already: `filterApproversWhoCanRead` refuses to empty a live slate on an
|
|
959
|
+
infrastructure hiccup, and `expandPositionUsers` carries "a step routing to
|
|
960
|
+
nobody is worse than one routing to a lapsed holder". A drop is logged with the
|
|
961
|
+
manager's id, his organizations and the request's, so the fix ("repair the link"
|
|
962
|
+
/ "grant the membership" / "retarget the step") is legible without a debugger.
|
|
963
|
+
|
|
964
|
+
## ⚠️ This moves one input from accepted to refused
|
|
965
|
+
|
|
966
|
+
A node whose **sole** approver is a cross-org `manager` and which is authored
|
|
967
|
+
with the **non-default** `onEmptyApprovers: 'fail'` used to open successfully;
|
|
968
|
+
it now throws `NO_APPROVERS`. Nothing new is thrown — a screened-out manager
|
|
969
|
+
leaves only a `type:value` literal, which the pre-existing empty-slate test
|
|
970
|
+
already classifies as empty, and `'fail'` already throws on empty. Every
|
|
971
|
+
screened sibling has reached that same bucket since it was written.
|
|
972
|
+
|
|
973
|
+
**The default policy is unaffected**: `admin_rescue` still opens the request
|
|
974
|
+
(decidable by a privileged admin) and warns, and `auto_approve` still
|
|
975
|
+
auto-approves. Both directions and both policies are pinned in
|
|
976
|
+
`manager-approver-org-screen.test.ts`.
|
|
977
|
+
|
|
978
|
+
## What this does NOT decide
|
|
979
|
+
|
|
980
|
+
- **#7497** (does approver routing imply record read visibility?) stays open.
|
|
981
|
+
The screen reads `sys_member`, which looks like the D2 read filter beside it,
|
|
982
|
+
and the code says at length why it is the *sibling* treatment instead: two of
|
|
983
|
+
the three org-scoped expansions already screen on `sys_member.organization_id`,
|
|
984
|
+
and `sys_user` offers no other tenancy fact. No reads are granted and no read
|
|
985
|
+
screen is applied to any type that lacked one.
|
|
986
|
+
- **`team`** is still unscreened — it is a sibling graph expansion that is not
|
|
987
|
+
org-scoped either, tracked as #10230, and it touches this same file.
|
|
988
|
+
- `APPROVER_ORG_SCOPED` is untouched. It answers ADR-0105 D9 *retargetability*
|
|
989
|
+
(may an author write `organization:` on this type?), not screening, and
|
|
990
|
+
`manager: false` remains correct.
|
|
991
|
+
- e222a53: **BREAKING** (compile-time only): twelve logger sink types that declared an
|
|
992
|
+
optional `error` now declare a **non-optional** `warn`, so a durability report
|
|
993
|
+
always has somewhere to land (#9754, #10556).
|
|
994
|
+
|
|
995
|
+
`minor`, not `major`: during the launch window this stack ships breaking changes
|
|
996
|
+
as `minor` — every publishable package versions in lockstep, so a `major` would
|
|
997
|
+
promote the whole release. `patch` would be wrong in the other direction, because
|
|
998
|
+
this *can* break a consumer's build.
|
|
999
|
+
|
|
1000
|
+
`error` stays optional on every one of these types — hosts legitimately inject
|
|
1001
|
+
reduced sinks, and requiring `error` was measured and rejected as #9754 option C.
|
|
1002
|
+
What changes is that its *absence* now has a declared, guaranteed destination.
|
|
1003
|
+
Call sites keep the `logger?.warn?.(…)` spelling as the backstop for hosts the
|
|
1004
|
+
type cannot reach, so **no runtime behaviour changes**: nothing that printed
|
|
1005
|
+
before stops printing, and nothing silent starts printing.
|
|
1006
|
+
|
|
1007
|
+
### Who has to change, and what to do
|
|
1008
|
+
|
|
1009
|
+
Only a caller that hands one of these sinks an object with **no `warn` method** —
|
|
1010
|
+
for example `{ info }` or `{ error }` alone. Add a `warn` member; there is no
|
|
1011
|
+
rename, no removal, and no stored value or metadata key to rewrite. Every
|
|
1012
|
+
construction site inside this repo already supplied one, so the in-repo cost was
|
|
1013
|
+
zero; the compile error is reserved for the callers that were silently discarding
|
|
1014
|
+
these reports.
|
|
1015
|
+
|
|
1016
|
+
The affected types, by package:
|
|
1017
|
+
|
|
1018
|
+
- `@objectstack/cloud-connection` — the internal `PluginContext['logger']`
|
|
1019
|
+
- `@objectstack/metadata-protocol` — `IndexMigrationLogger`
|
|
1020
|
+
- `@objectstack/plugin-approvals` — the internal `MinimalLogger` of `lifecycle-hooks`
|
|
1021
|
+
- `@objectstack/plugin-audit` — `AuthEventAuditLogger`, `ReadAuditLogger`
|
|
1022
|
+
- `@objectstack/plugin-auth` — `ReconcileMembershipDeps['logger']`, the internal
|
|
1023
|
+
`LoggerLike` of `member-role-canonical`, and `AuthManagerOptions['logger']`
|
|
1024
|
+
- `@objectstack/plugin-email` — `ReclaimLogger`, via `ReclaimAttachmentContentOptions`
|
|
1025
|
+
- `@objectstack/plugin-reports` — `ReportServiceOptions['logger']`
|
|
1026
|
+
- `@objectstack/plugin-sharing` — the internal `MinimalLogger` of `bulk-recompute`,
|
|
1027
|
+
`rule-hooks` and `record-share-cascade`
|
|
1028
|
+
- `@objectstack/plugin-webhooks` — `OptionalLogger`, via `AutoEnqueuerOptions`
|
|
1029
|
+
- `@objectstack/service-knowledge` — `KnowledgeLogger`
|
|
1030
|
+
|
|
1031
|
+
`AuthManagerOptions['logger']` is the one most likely to be reached from outside:
|
|
1032
|
+
`AuthManager` is public surface, its `logger` option stays optional, and a logger
|
|
1033
|
+
that *is* supplied must now carry `warn`. The only non-test construction site in
|
|
1034
|
+
this repo passes the kernel `Logger`, whose `warn` is already required.
|
|
1035
|
+
|
|
1036
|
+
`ReportService` and `AutoEnqueuer` additionally stopped defaulting their logger
|
|
1037
|
+
field to `{}`. The field is now honestly optional rather than holding an empty
|
|
1038
|
+
object that declared it could report and discarded everything. Behaviour is
|
|
1039
|
+
unchanged in both directions.
|
|
1040
|
+
|
|
1041
|
+
<!-- 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. -->
|
|
1042
|
+
|
|
1043
|
+
### Patch Changes
|
|
1044
|
+
|
|
1045
|
+
- 07bd1ca: Apply the subject object's field-level read controls to the approval payload
|
|
1046
|
+
snapshot at serve time (#10749), so an approver no longer receives fields the
|
|
1047
|
+
app author declared they may not read.
|
|
1048
|
+
|
|
1049
|
+
`sys_approval_request.payload_json` stores the submitted record's raw row,
|
|
1050
|
+
captured from the flow's `$record` variable — which the automation layer hands
|
|
1051
|
+
over with the record's own FLS never applying. The column is a `textarea` on
|
|
1052
|
+
`sys_approval_request`, so to every read door it is an opaque string: the
|
|
1053
|
+
field-visibility machinery governs *columns of objects* and cannot see inside a
|
|
1054
|
+
JSON column. Every field-level read control declared on the SUBJECT object —
|
|
1055
|
+
`requiredPermissions` (ADR-0066 D3), a permission set marking a field
|
|
1056
|
+
non-readable, a `maskingRule` — was therefore unenforceable on the approval
|
|
1057
|
+
path, for every app.
|
|
1058
|
+
|
|
1059
|
+
Per the maintainer's ruling (2026-08-22, Option B) the full snapshot **stays at
|
|
1060
|
+
rest**: the approval record remains audit evidence of what was actually
|
|
1061
|
+
submitted, which write-time trimming would have given away. Redaction happens at
|
|
1062
|
+
**serve** time, keyed on the reading caller, so the same row answers an admin
|
|
1063
|
+
with the whole snapshot and a restricted approver with only the fields they may
|
|
1064
|
+
read. The readable set is not recomputed — it comes from the security service's
|
|
1065
|
+
`getReadableFields`, documented as the same field mask the read middleware
|
|
1066
|
+
applies, so this seam cannot drift from data-plane FLS.
|
|
1067
|
+
|
|
1068
|
+
Two doors are covered, because `payload_json` has two independent readers and a
|
|
1069
|
+
seam covering one manufactures the belief that the path is masked:
|
|
1070
|
+
|
|
1071
|
+
- the **service door** (`getRequest` / `listRequests`, behind
|
|
1072
|
+
`GET /api/v1/approvals/requests[/:id]`), which serves the parsed `payload`.
|
|
1073
|
+
Redaction runs BEFORE display enrichment, so `payload_display` and
|
|
1074
|
+
`payload_labels` — both built by walking the snapshot's own keys — cannot ship
|
|
1075
|
+
a restricted field's name, its authored label, or the title of the record it
|
|
1076
|
+
points at;
|
|
1077
|
+
- the **generic data door**: the object declares
|
|
1078
|
+
`enable.apiMethods: ['get','list']`, so a plain `find`/`findOne` returns the
|
|
1079
|
+
raw string without the service ever running. Covered by object-scoped engine
|
|
1080
|
+
middleware, which reaches the whole family sharing that producer (REST data
|
|
1081
|
+
routes, ObjectQL, CSV/XLSX export, MCP). Middleware rather than an `afterFind`
|
|
1082
|
+
hook on purpose: a hook receives `buildSession`'s output, which carries no
|
|
1083
|
+
`onBehalfOf`, so a hook-based seam would drop the ADR-0090 D10 delegator
|
|
1084
|
+
intersection and answer a delegated read more permissively than the service.
|
|
1085
|
+
|
|
1086
|
+
**Behaviour change, argued rather than assumed.** A non-admin caller that was
|
|
1087
|
+
reading restricted keys out of the snapshot now receives fewer keys, and that is
|
|
1088
|
+
a real change for such a consumer. It is shipped as a fix rather than a breaking
|
|
1089
|
+
change because those fields were never that caller's to read: the approval path
|
|
1090
|
+
was a bypass of a declaration the platform enforces everywhere else, and the
|
|
1091
|
+
served type is `payload?: unknown` — never a promised field set. This follows the
|
|
1092
|
+
`__search` companion strip (#7642), which shipped the same way on the same
|
|
1093
|
+
reasoning. Object-level access is deliberately untouched: an approver commonly
|
|
1094
|
+
holds no read grant on the object under approval at all, and
|
|
1095
|
+
`getReadableFields` answers a caller with no field-permission entries with the
|
|
1096
|
+
full set, so every approval drawer shipping today keeps rendering.
|
|
1097
|
+
|
|
1098
|
+
`hidden: true` is deliberately NOT acted on. It is a UI contract ("Hidden from
|
|
1099
|
+
default UI") which, in the spec's own words, "has never governed serialization"
|
|
1100
|
+
— measurably: no read path in the repo strips a value on it. Enforcing it here
|
|
1101
|
+
alone would make the approval path stricter than a direct read of the same row
|
|
1102
|
+
(closing no leak, since the approver can simply read the record) while breaking
|
|
1103
|
+
drawers that render a `hidden` business column. That is a `packages/spec`
|
|
1104
|
+
semantics question and is left open.
|
|
1105
|
+
- f0d7647: **Deliberate search-semantics change.** `ApprovalService.listRequests` /
|
|
1106
|
+
`countRequests` no longer push a free-text predicate onto the payload snapshot
|
|
1107
|
+
column for a caller whose view of that snapshot is masked (#11040).
|
|
1108
|
+
|
|
1109
|
+
Since #10749 the approval snapshot (`sys_approval_request.payload_json`, the
|
|
1110
|
+
submitted record's row) is redacted **at serve time, per reader**: each row is
|
|
1111
|
+
cut down to the fields that caller may read on that row's subject object. The
|
|
1112
|
+
full row deliberately stays at rest, so the approval record remains audit
|
|
1113
|
+
evidence of what was actually submitted.
|
|
1114
|
+
|
|
1115
|
+
The free-text filter, however, is evaluated by the driver against the stored
|
|
1116
|
+
column — before anything is served, and therefore against the unmasked bytes. A
|
|
1117
|
+
predicate over that column is a question about its contents whose answer is row
|
|
1118
|
+
membership, so the field-level read controls #10749 enforces on the way out did
|
|
1119
|
+
not hold on the way in. That is `declared ≠ enforced`, and the platform has
|
|
1120
|
+
already settled the governing principle for it: under `maskingRule` (#8993) a
|
|
1121
|
+
field a caller sees masked is non-filterable, refused loudly, because otherwise
|
|
1122
|
+
equality probes reconstruct the hidden span. This extends that settled posture
|
|
1123
|
+
to the snapshot column, which is reached through a different door.
|
|
1124
|
+
|
|
1125
|
+
**What changes.** For a caller whose view of the snapshot is masked, free-text
|
|
1126
|
+
search matches on `process_name`, `object_name`, `record_id` and `submitter_id`
|
|
1127
|
+
— the columns of `sys_approval_request` itself, which anyone who can see the row
|
|
1128
|
+
reads whole — and no longer on snapshot contents. Such a caller can still find a
|
|
1129
|
+
request by process, object, record id or submitter; they can no longer find one
|
|
1130
|
+
by a value they may not read. Rows returned, their order and pagination are
|
|
1131
|
+
otherwise untouched, and no query is refused: the change only ever removes one
|
|
1132
|
+
disjunct, never denies.
|
|
1133
|
+
|
|
1134
|
+
**What does not change.** A caller the serve path hands the whole snapshot to
|
|
1135
|
+
keeps today's behaviour exactly — same rows, same order. That includes every
|
|
1136
|
+
deployment that has not wired a field-visibility authority (the seam is
|
|
1137
|
+
late-bound, and absent it snapshots are served unredacted), and the case where a
|
|
1138
|
+
wired authority declines to narrow. Consistency with the serve path is the rule
|
|
1139
|
+
here rather than blanket fail-closed: where serve hands over the whole snapshot,
|
|
1140
|
+
keeping the predicate discloses nothing serve does not already disclose.
|
|
1141
|
+
|
|
1142
|
+
The masked/unmasked verdict is read from **the same authority and the same
|
|
1143
|
+
per-caller call the serve path uses**, asked as the caller. It is deliberately
|
|
1144
|
+
not a second, independently derived notion of "redacted" — two derivations drift,
|
|
1145
|
+
and the drift between a serve rule and a filter rule is exactly what this fixes.
|
|
1146
|
+
|
|
1147
|
+
Because redaction is decided per row while a filter is built before any row
|
|
1148
|
+
exists, the predicate-time scope matters: with an `object` filter the subject
|
|
1149
|
+
object is known and the seam is asked about it directly; without one the query
|
|
1150
|
+
spans every object, nothing sound can be asked, and the disjunct is dropped.
|
|
1151
|
+
|
|
1152
|
+
Held by `approval-free-text-scope.test.ts`.
|
|
1153
|
+
- 6d5c4fa: Release these plugins' resources from `destroy()`, the teardown hook the kernel
|
|
1154
|
+
actually calls (#10371). `Plugin` declares `init()`, `start?(ctx)` and
|
|
1155
|
+
`destroy?()` — and no `stop()` — so `ObjectKernel.performShutdown()` and
|
|
1156
|
+
`LiteKernel.destroy()`, which walk the plugins in reverse calling
|
|
1157
|
+
`plugin.destroy()`, walked straight past every plugin whose teardown was spelled
|
|
1158
|
+
`stop()`. `await kernel.shutdown()` resolved with the reports dispatcher still
|
|
1159
|
+
armed, the REST/OpenAPI/Slack connectors still registered on the automation
|
|
1160
|
+
engine, the approvals SLA escalation job still scheduled, and the knowledge
|
|
1161
|
+
event-sync subscription still open.
|
|
1162
|
+
|
|
1163
|
+
Each teardown body now lives in `destroy()`. `stop()` is retained as a
|
|
1164
|
+
delegating alias with its parameter made optional, so an embedder that learned
|
|
1165
|
+
to call it directly — precisely because the kernel never did — keeps working
|
|
1166
|
+
unchanged. No export is removed and the `Plugin` interface is untouched.
|
|
1167
|
+
|
|
1168
|
+
Same defect as #9371 in `@objectstack/service-messaging`, which surfaced as
|
|
1169
|
+
fully green test runs exiting 1 on `EnvironmentTeardownError` and being evicted
|
|
1170
|
+
from the merge queue.
|
|
1171
|
+
- aa765b9: **Who loses access:** members of a team belonging to a *different* organization
|
|
1172
|
+
than the record being approved. Concretely — a request raised in `org_a` routed
|
|
1173
|
+
to a `team` approver whose `sys_team.organization_id` is `org_b` used to place
|
|
1174
|
+
every `sys_team_member` of that team into `pending_approvers`, giving them the
|
|
1175
|
+
approve/reject buttons on a record they are not a tenant of. They no longer
|
|
1176
|
+
enter the slate, and the step falls back to the dead `team:<id>` literal with
|
|
1177
|
+
the existing `#3807` "expanded to nobody" warning — the same shape a cross-org
|
|
1178
|
+
`position` approver has always produced (#10230).
|
|
1179
|
+
|
|
1180
|
+
`team` was the last approver expansion that resolved people without asking
|
|
1181
|
+
which organization was asking; `department`, `position`, `org_membership_level`
|
|
1182
|
+
and (since #10153) `manager` all do. The screen reads the team's own
|
|
1183
|
+
`organization_id`, so it costs one row and a team that fails it never fans out.
|
|
1184
|
+
|
|
1185
|
+
**Who does not lose access**, deliberately: a team stamped with the request's
|
|
1186
|
+
own organization; a team stamped with **no** organization (`organization_id:
|
|
1187
|
+
null` on a platform object means "owned by no organization" — what a seed
|
|
1188
|
+
writes, since a seed cannot know the id the runtime mints at boot); a team id
|
|
1189
|
+
with no `sys_team` row at all; and any request that carries no organization —
|
|
1190
|
+
all four leave routing exactly as it was, because the tenancy fact is absent
|
|
1191
|
+
rather than negative.
|
|
1192
|
+
|
|
1193
|
+
⚠️ One externally observable accept→reject change beyond the routing itself:
|
|
1194
|
+
under the non-default `onEmptyApprovers: 'fail'` policy, a node whose *sole*
|
|
1195
|
+
approver was a cross-org team used to open a request and now throws
|
|
1196
|
+
`NO_APPROVERS`. Under the default (`admin_rescue`) the node still opens.
|
|
1197
|
+
- c5d0c2f: Screen expanded `team` approver members to the request's organization (#10547).
|
|
1198
|
+
|
|
1199
|
+
#10230 made a `team` approver prove the TEAM's tenancy, and deferred the
|
|
1200
|
+
members on purpose. `sys_team_member` carries `team_id` and `user_id` and no
|
|
1201
|
+
organization column, so a team that passed that screen still routed every user
|
|
1202
|
+
id it listed — including a user whose only `sys_member` row is in another
|
|
1203
|
+
organization. Measured on a fixture, not read off the schema: an `org_a`
|
|
1204
|
+
request against an `org_a` team returned `["u_outsider","u_insider"]` with zero
|
|
1205
|
+
`sys_member` reads.
|
|
1206
|
+
|
|
1207
|
+
The expansion now screens the members with the same provably-outside posture
|
|
1208
|
+
the neighbouring screens pin, in ONE `$in` read for the whole slate:
|
|
1209
|
+
|
|
1210
|
+
- membership rows exist for the user and none is the request's organization
|
|
1211
|
+
(present and NEGATIVE) — dropped, with a warning naming the users, the team
|
|
1212
|
+
and both organizations;
|
|
1213
|
+
- no membership rows, an unreadable `sys_member`, a possibly-truncated read, or
|
|
1214
|
+
a request carrying no organization (ABSENT) — routing is left exactly as it
|
|
1215
|
+
was, and the no-organization case performs no read at all.
|
|
1216
|
+
|
|
1217
|
+
Holding membership elsewhere is not disqualifying; holding none here is.
|
|
1218
|
+
|
|
1219
|
+
⚠️ Behaviour change, confined to one non-default policy: a node whose only
|
|
1220
|
+
approver is a team staffed entirely by users provably outside the organization
|
|
1221
|
+
now resolves to no one. Under the default `onEmptyApprovers: 'admin_rescue'` it
|
|
1222
|
+
still opens, routed to the dead `team:<id>` literal as any unresolved slate is;
|
|
1223
|
+
under `onEmptyApprovers: 'fail'` it now throws `NO_APPROVERS` where it
|
|
1224
|
+
previously opened.
|
|
1225
|
+
|
|
1226
|
+
Residual condition on the security value: the screen can only act on tenancy
|
|
1227
|
+
facts that exist. A deployment that stamps an organization on its approval
|
|
1228
|
+
requests but does not materialize `sys_member` rows sees no change — by design,
|
|
1229
|
+
since #3807 recorded what treating an absent fact as a negative one costs.
|
|
1230
|
+
- Updated dependencies [8f04d9a]
|
|
1231
|
+
- Updated dependencies [6936d07]
|
|
1232
|
+
- Updated dependencies [59eb04d]
|
|
1233
|
+
- Updated dependencies [9f05b7d]
|
|
1234
|
+
- Updated dependencies [3b2af5e]
|
|
1235
|
+
- Updated dependencies [7d2d112]
|
|
1236
|
+
- Updated dependencies [5fa0d72]
|
|
1237
|
+
- Updated dependencies [8cc8401]
|
|
1238
|
+
- Updated dependencies [02b3b07]
|
|
1239
|
+
- Updated dependencies [46d34ab]
|
|
1240
|
+
- Updated dependencies [914c413]
|
|
1241
|
+
- Updated dependencies [55809a0]
|
|
1242
|
+
- Updated dependencies [ee2ff45]
|
|
1243
|
+
- Updated dependencies [47cd3ec]
|
|
1244
|
+
- Updated dependencies [52db1d1]
|
|
1245
|
+
- Updated dependencies [5649efb]
|
|
1246
|
+
- Updated dependencies [9d7d2de]
|
|
1247
|
+
- Updated dependencies [c815c50]
|
|
1248
|
+
- Updated dependencies [795ea05]
|
|
1249
|
+
- Updated dependencies [2306a76]
|
|
1250
|
+
- Updated dependencies [e5ea701]
|
|
1251
|
+
- Updated dependencies [26f3588]
|
|
1252
|
+
- Updated dependencies [a40dcc1]
|
|
1253
|
+
- Updated dependencies [def0d3e]
|
|
1254
|
+
- Updated dependencies [8d0bb79]
|
|
1255
|
+
- Updated dependencies [5acb58d]
|
|
1256
|
+
- Updated dependencies [2e3cf95]
|
|
1257
|
+
- Updated dependencies [4c93387]
|
|
1258
|
+
- Updated dependencies [504c8d5]
|
|
1259
|
+
- Updated dependencies [a037f7c]
|
|
1260
|
+
- Updated dependencies [3ee8ddf]
|
|
1261
|
+
- Updated dependencies [16cef97]
|
|
1262
|
+
- Updated dependencies [a79bd35]
|
|
1263
|
+
- Updated dependencies [6ceaa4b]
|
|
1264
|
+
- Updated dependencies [15ea214]
|
|
1265
|
+
- Updated dependencies [de19489]
|
|
1266
|
+
- Updated dependencies [c684d00]
|
|
1267
|
+
- Updated dependencies [923c424]
|
|
1268
|
+
- Updated dependencies [0ab81d1]
|
|
1269
|
+
- Updated dependencies [1ec36b7]
|
|
1270
|
+
- Updated dependencies [5f2e54c]
|
|
1271
|
+
- Updated dependencies [189373b]
|
|
1272
|
+
- Updated dependencies [35ad101]
|
|
1273
|
+
- Updated dependencies [ceb33a9]
|
|
1274
|
+
- Updated dependencies [dccbcec]
|
|
1275
|
+
- Updated dependencies [05bc692]
|
|
1276
|
+
- Updated dependencies [73d9795]
|
|
1277
|
+
- Updated dependencies [8012960]
|
|
1278
|
+
- Updated dependencies [266654d]
|
|
1279
|
+
- Updated dependencies [f34f56b]
|
|
1280
|
+
- Updated dependencies [f399618]
|
|
1281
|
+
- Updated dependencies [75e9301]
|
|
1282
|
+
- Updated dependencies [f334d66]
|
|
1283
|
+
- Updated dependencies [2810695]
|
|
1284
|
+
- @objectstack/platform-objects@17.2.0
|
|
1285
|
+
- @objectstack/spec@17.2.0
|
|
1286
|
+
- @objectstack/core@17.2.0
|
|
1287
|
+
- @objectstack/metadata-core@17.2.0
|
|
1288
|
+
- @objectstack/types@17.2.0
|
|
1289
|
+
- @objectstack/formula@17.2.0
|
|
1290
|
+
|
|
3
1291
|
## 17.1.0
|
|
4
1292
|
|
|
5
1293
|
### Minor Changes
|