@objectstack/plugin-approvals 16.1.0 → 17.0.0-rc.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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +1023 -0
- package/dist/index.d.mts +650 -535
- package/dist/index.d.ts +650 -535
- package/dist/index.js +1713 -207
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1711 -197
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -7
- package/scripts/i18n-extract.config.ts +6 -1
- package/src/approval-actor-impersonation.test.ts +330 -0
- package/src/approval-node.test.ts +160 -0
- package/src/approval-node.ts +57 -0
- package/src/approval-revise.test.ts +41 -34
- package/src/approval-service.test.ts +1408 -40
- package/src/approval-service.ts +1364 -107
- package/src/approvals-plugin.ts +36 -5
- package/src/approver-cross-org.integration.test.ts +206 -0
- package/src/approver-org-scope.test.ts +201 -0
- package/src/approver-org-scope.ts +261 -0
- package/src/index.ts +3 -0
- package/src/lifecycle-hooks.ts +22 -0
- package/src/record-lock-schedule-run.integration.test.ts +206 -0
- package/src/status-mirror-cascade.integration.test.ts +224 -0
- package/src/sys-approval-action.object.ts +9 -0
- package/src/sys-approval-delegation.object.test.ts +42 -0
- package/src/sys-approval-delegation.object.ts +3 -3
- package/src/sys-approval-request.object.test.ts +13 -0
- package/src/sys-approval-request.object.ts +17 -5
- package/src/translations/bundle-ownership.test.ts +48 -0
- package/src/translations/en.objects.generated.ts +111 -5
- package/src/translations/es-ES.objects.generated.ts +111 -5
- package/src/translations/ja-JP.objects.generated.ts +111 -5
- package/src/translations/zh-CN.objects.generated.ts +110 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,1028 @@
|
|
|
1
1
|
# @objectstack/plugin-approvals
|
|
2
2
|
|
|
3
|
+
## 17.0.0-rc.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 14252d3: feat(approvals): cross-organization approver targeting — a plant document can
|
|
8
|
+
require a group-side sign-off (ADR-0105 D9)
|
|
9
|
+
|
|
10
|
+
One organization id used to decide three different things at once in
|
|
11
|
+
`openNodeRequest`: where the request row lives, where its inbox index rows
|
|
12
|
+
live, and **where its approvers are looked up**. The first two are the
|
|
13
|
+
request's own organization by definition. The third is not — a group CFO holds
|
|
14
|
+
her `cfo` position in the GROUP organization while the purchase order she signs
|
|
15
|
+
off lives in the PLANT organization. `expandPositionUsers('cfo', <plant>)`
|
|
16
|
+
matched nobody, the slot fell back to the dead `position:cfo` literal, and a
|
|
17
|
+
group escalation could not be expressed at all.
|
|
18
|
+
|
|
19
|
+
An approver may now declare which organization's directory resolves it:
|
|
20
|
+
|
|
21
|
+
```yaml
|
|
22
|
+
approvers:
|
|
23
|
+
- { type: position, value: plant_manager, group: plant }
|
|
24
|
+
- { type: position, value: cfo, organization: $root, group: finance }
|
|
25
|
+
behavior: per_group
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- **`$root` / `$parent`** walk D6's `parent_organization_id` tree, so the two
|
|
29
|
+
common intents need **no deployment knowledge** — flow metadata is portable
|
|
30
|
+
across environments while organization ids are minted per deployment. A slug
|
|
31
|
+
covers what the symbols cannot, notably a **sibling** organization (a
|
|
32
|
+
shared-services centre approving payables for every plant).
|
|
33
|
+
- Declared **per approver**, so one node can require a plant manager and a
|
|
34
|
+
group CFO in parallel. A node-level form cannot express that without
|
|
35
|
+
splitting into serial nodes, which changes the semantics.
|
|
36
|
+
- **Bounded, not free:** the target must share a `parent_organization_id` root
|
|
37
|
+
with the request's organization. The rule reads only the organization tree —
|
|
38
|
+
never the submitter — so one flow routes identically for everyone.
|
|
39
|
+
|
|
40
|
+
Everything else fails loudly rather than quietly:
|
|
41
|
+
|
|
42
|
+
- a non-`group` posture **refuses** the declaration (a `group` → `isolated`
|
|
43
|
+
migration must not silently reroute approvals);
|
|
44
|
+
- an approver type with no org-scoped directory (`user` / `field` / `manager` /
|
|
45
|
+
`team`) refuses it too, and a new `approval-approver-cross-org-unsupported`
|
|
46
|
+
lint catches that at author time;
|
|
47
|
+
- a targeted approver holding no membership in the request's organization is
|
|
48
|
+
dropped with a warning naming them — D2's union wall would otherwise hide the
|
|
49
|
+
request from someone already routed to, so the node's existing
|
|
50
|
+
`onEmptyApprovers` policy takes over instead of leaving an unopenable task.
|
|
51
|
+
|
|
52
|
+
Nothing changes for an approver without `organization`: same resolution, same
|
|
53
|
+
queries, no extra reads.
|
|
54
|
+
|
|
55
|
+
- f92096b: fix(approvals): an approval action is recorded against the authenticated caller, never a body field (#3800)
|
|
56
|
+
|
|
57
|
+
Every mutating approvals entrypoint takes an `actorId`, and the REST routes
|
|
58
|
+
filled it from `body.actorId ?? body.actor_id ?? context.userId` — so the body
|
|
59
|
+
won. The service then authorized _that value_: `pending_approvers.includes(
|
|
60
|
+
input.actorId)` for a decision, `submitter_id === actorId` for a recall. It never
|
|
61
|
+
checked that the value named the caller.
|
|
62
|
+
|
|
63
|
+
So any authenticated user could POST `{"actorId": "<someone else>"}` and have
|
|
64
|
+
that person's approval recorded, the request finalized, and the owning flow run
|
|
65
|
+
resumed down the `approve` edge — or name a request's submitter and recall it.
|
|
66
|
+
With `api.requireAuth` unset the anonymous-deny never fires either, so an
|
|
67
|
+
unauthenticated request could do the same.
|
|
68
|
+
|
|
69
|
+
#3783 drew this line for the _data-write_ identity and called the audit-row half
|
|
70
|
+
"tolerable". It was not: the same unchecked string was the authorization key, so
|
|
71
|
+
naming someone else was not a mislabelled audit row, it was how you got through
|
|
72
|
+
the door.
|
|
73
|
+
|
|
74
|
+
The actor is now resolved server-side (`ApprovalService.resolveActor`) on all
|
|
75
|
+
nine entrypoints — `decide` / `decideNode`, `recall`, `sendBack`, `resubmit`,
|
|
76
|
+
`reassign`, `remind`, `requestInfo`, `comment`.
|
|
77
|
+
|
|
78
|
+
**The rule is not "`actorId` must equal `context.userId`."** A slot can
|
|
79
|
+
legitimately be keyed by something else: the approver resolver stores the
|
|
80
|
+
`type:value` literal when a graph lookup finds no holders, and the Console picks
|
|
81
|
+
from the caller's own identity list — user id, email, or `role:<r>`. The rule is
|
|
82
|
+
**"the actor must be an identity the server can prove belongs to the caller"**:
|
|
83
|
+
|
|
84
|
+
- A **system** context keeps its explicit actor. The SLA sweep's reserved
|
|
85
|
+
`system:sla` sentinel and the ADR-0043 action link — whose single-use hashed
|
|
86
|
+
token binds exactly one approver — are unchanged. They are the only callers
|
|
87
|
+
holding a trustworthy actor with no session behind them.
|
|
88
|
+
- A caller with **no identity at all** is now refused. This is the anonymous case
|
|
89
|
+
above.
|
|
90
|
+
- **No `actorId`, or one naming the caller**, resolves to the caller. This is the
|
|
91
|
+
common path and what the Console already sends.
|
|
92
|
+
- **Any other value** is accepted only when the server can prove the caller holds
|
|
93
|
+
it — `position:<p>` / `role:<p>` against the positions on the resolved authz
|
|
94
|
+
context, or the caller's own email (one lazy `sys_user` read, taken only when
|
|
95
|
+
nothing cheaper matched). Otherwise `FORBIDDEN`.
|
|
96
|
+
|
|
97
|
+
REST still forwards the body value; it is now a _hint_ the service validates,
|
|
98
|
+
which is what keeps the email and `type:value` slot cases working.
|
|
99
|
+
|
|
100
|
+
**Upgrade note.** A client that deliberately sent another user's `actorId` now
|
|
101
|
+
gets `403 FORBIDDEN` instead of silently succeeding. Send the action as the
|
|
102
|
+
acting user's own session — the field can be omitted entirely, and the caller is
|
|
103
|
+
used. Server-to-server callers that legitimately act for someone else should
|
|
104
|
+
present a system context, as the SLA sweep and the action link already do.
|
|
105
|
+
|
|
106
|
+
This also makes two existing claims true that were previously aspirational: the
|
|
107
|
+
approval object's declared actions say "`actorId` defaults to the caller
|
|
108
|
+
server-side… the service remains the authority on who may act", and
|
|
109
|
+
`attachViewers` documents `can_act` as mirroring "the exact authorization the
|
|
110
|
+
decision methods enforce".
|
|
111
|
+
|
|
112
|
+
- fb90784: fix(approvals): the status mirror names the human who caused the transition (#3783)
|
|
113
|
+
|
|
114
|
+
When an approval moves, the service writes the new status onto the business
|
|
115
|
+
record (`approvalStatusField`). That write is what fires the record-change flows
|
|
116
|
+
bound to that object — so it is the seam "when the invoice is approved, do X"
|
|
117
|
+
runs through. It presented a bare `{ isSystem: true }` context with **no
|
|
118
|
+
`userId`**, at six call sites that each know exactly who acted: a submitter
|
|
119
|
+
submitting, an approver approving, rejecting, sending back, recalling.
|
|
120
|
+
|
|
121
|
+
Combined with #3760 — which stopped letting a `runAs:'user'` run with no trigger
|
|
122
|
+
user touch data — that identity gap made the most natural approvals automation
|
|
123
|
+
there is unwritable in its obvious form. The cascade inherited no user, so its
|
|
124
|
+
data nodes were refused, and the author's only way forward was to declare
|
|
125
|
+
`runAs: 'system'` and take blanket elevation for a case where a perfectly good
|
|
126
|
+
scoped identity existed at the call site all along.
|
|
127
|
+
|
|
128
|
+
The mirror now carries the acting user. It stays `isSystem` — the record is
|
|
129
|
+
normally locked while its approval is live, so only a platform write can land the
|
|
130
|
+
status — because elevation and anonymity are separate choices, and this write
|
|
131
|
+
only ever needed the first. Cascades now run as the deciding user with RLS
|
|
132
|
+
enforced.
|
|
133
|
+
|
|
134
|
+
- **The identity is the authenticated principal, never the request body's
|
|
135
|
+
`actorId`.** `actorId` arrives from the caller (`body.actorId ?? context.userId`)
|
|
136
|
+
and is only checked against the pending approver slate, never against the
|
|
137
|
+
caller. That is tolerable on an audit row; promoting it to the identity of an
|
|
138
|
+
RLS-scoped write would have turned a mislabelled audit trail into identity
|
|
139
|
+
spoofing.
|
|
140
|
+
- **Approval-by-email-link is attributed too.** ADR-0043 action links carry no
|
|
141
|
+
session, so they used to decide as pure system. The single-use hashed token
|
|
142
|
+
binds exactly one approver and is re-checked against the live slate at
|
|
143
|
+
redemption — that is an authentication — so the redeemed decision now presents
|
|
144
|
+
that approver, and an emailed approval cascades identically to one made in the
|
|
145
|
+
UI.
|
|
146
|
+
- **The two machine-driven transitions stay user-less on purpose**: the SLA
|
|
147
|
+
escalation's auto-decision and the dead-run sweep. `system:sla` and
|
|
148
|
+
`system:dead-run` are reserved audit actors, not users, and presenting one as a
|
|
149
|
+
user would put a non-user in `updated_by` and in every downstream flow's
|
|
150
|
+
identity. A flow that wants to react to those declares `runAs:'system'` — the
|
|
151
|
+
honest answer, and now a deliberate one rather than an artefact.
|
|
152
|
+
- **Attribution only — the write is not newly org-scoped.** On an
|
|
153
|
+
ExecutionContext `tenantId` is a driver-scoping knob, not attribution
|
|
154
|
+
(ObjectQL turns it into a tenant predicate), so passing the request's org would
|
|
155
|
+
have silently no-op'd the mirror on a record whose org differs. The automation
|
|
156
|
+
engine already back-fills a run's `tenantId` from the resolved user's grants.
|
|
157
|
+
|
|
158
|
+
**Visible change:** the mirrored record's `updated_by` now names the acting user
|
|
159
|
+
instead of retaining its previous value — ObjectQL's audit stamping is gated on
|
|
160
|
+
the write context's `userId` alone, and `isSystem` buys no exemption. That is the
|
|
161
|
+
attribution this fix is for: the approver who set the record to `approved` is now
|
|
162
|
+
its last modifier.
|
|
163
|
+
|
|
164
|
+
- a6c3f38: feat(approvals): expose the pending node's `lockRecord` policy on the request row (#3814, objectui#2902)
|
|
165
|
+
|
|
166
|
+
An approval node declares `lockRecord` (default `true`), and the record-lock
|
|
167
|
+
`beforeUpdate` hook enforces exactly that: `lockRecord: false` and the record
|
|
168
|
+
stays writable for the whole time the node waits. The behavior was correct and
|
|
169
|
+
has been since Phase B — but it was **invisible to every client**.
|
|
170
|
+
|
|
171
|
+
`rowFromRequest` parses `node_config_json` and projects a whitelist out of it
|
|
172
|
+
(`__flowLabel`, `__nodeLabel`, `__round`, `escalation.timeoutHours`,
|
|
173
|
+
`decisionOutputs`). `lockRecord` was never in that list, and no other field on
|
|
174
|
+
`ApprovalRequestRow` carried the lock either. So the strongest thing a console
|
|
175
|
+
could learn from `GET /approvals/requests` was _"a pending request exists"_ —
|
|
176
|
+
from which it can only assume the record is locked.
|
|
177
|
+
|
|
178
|
+
That assumption is wrong on every opted-out node, and a flow that chains nodes
|
|
179
|
+
with different policies makes it visibly wrong: the same UI state renders for
|
|
180
|
+
"you may edit this" and "the server will reject your save with `RECORD_LOCKED`".
|
|
181
|
+
The console has no third option — guessing the other way would offer an edit
|
|
182
|
+
that dies on save.
|
|
183
|
+
|
|
184
|
+
`ApprovalRequestRow` now carries **`lock_record: boolean`**, read from the same
|
|
185
|
+
snapshot the hook reads, with the same `!== false` default. Present on every
|
|
186
|
+
service read (`openNodeRequest` / `getRequest` / `listRequests`), so the flag a
|
|
187
|
+
client renders and the rule the server applies cannot drift.
|
|
188
|
+
|
|
189
|
+
Additive and backward compatible — nothing to migrate. A client that wants
|
|
190
|
+
node-accurate lock state reads `request.lock_record`; treat `undefined` (an
|
|
191
|
+
older backend) as locked, which is the pre-existing behavior.
|
|
192
|
+
|
|
193
|
+
The showcase's `showcase_budget_approval` now declares `lockRecord: false` on
|
|
194
|
+
its single-approver Manager Review and keeps `true` on the multi-approver
|
|
195
|
+
Executive Review, so both policies are exercised in one flow.
|
|
196
|
+
|
|
197
|
+
- d75edb9: Approval nodes now resolve `field` / `manager` approvers against the record's **live** state at node entry, not the trigger snapshot the flow froze at submit time (#3447). An earlier step — or the approver of an earlier step — can now write the field that routes a later step's approvers, enabling dynamic routing / dynamic co-sign (e.g. a lead reviewer picking which departments co-review, then those departments resolving as parallel approvers). Graph approvers (team / position / department / tier) already resolved live; this brings the in-record types into line.
|
|
198
|
+
|
|
199
|
+
Also fixes two latent defects on the same path: a multi-select user field now fans out into one approver slot per user (previously the array was stringified to a single bogus id), and out-of-office delegation is applied per fanned-out user (previously silently skipped for multi-value fields). When the record can't be re-read (hard-deleted mid-flow, or a backend that can't serve a point read), resolution falls back to the trigger snapshot and warns rather than wedging the flow.
|
|
200
|
+
|
|
201
|
+
- 57a3bb3: fix(automation,approvals): the run-resume route is gated by the node the run is parked on (#3801)
|
|
202
|
+
|
|
203
|
+
`POST /api/v1/automation/:name/runs/:runId/resume` forwarded a caller-supplied
|
|
204
|
+
`{ inputs, output, branchLabel }` straight into `AutomationEngine.resume`, and
|
|
205
|
+
`resumeInternal` validated **machine state only** — the concurrent-resume latch,
|
|
206
|
+
the run exists, the flow exists, the suspended node still exists. Nothing asked
|
|
207
|
+
_who was calling_.
|
|
208
|
+
|
|
209
|
+
Approval nodes suspend and resume through exactly that mechanism. So a resume
|
|
210
|
+
carrying `branchLabel: 'approve'` walked the approve edge with **no approver
|
|
211
|
+
check, no `sys_approval_action` row and no status mirror** — the
|
|
212
|
+
`sys_approval_request` row and the run then disagreed permanently. The only
|
|
213
|
+
thing standing between the route and the approvals rules was convention; the
|
|
214
|
+
showcase spelled it out in a comment ("decide via the approvals API, never a raw
|
|
215
|
+
engine `resume`"), and a comment in an example is not an access control.
|
|
216
|
+
|
|
217
|
+
Removing the route was not the fix: it is load-bearing for **screen flows** —
|
|
218
|
+
the UI flow-runner posts `{ inputs }` there to advance a paused `screen` node.
|
|
219
|
+
The gate therefore keys on **what the run is parked on**:
|
|
220
|
+
|
|
221
|
+
- `ActionDescriptor.resumeAuthority` (`'any'` | `'service'`, default `'any'`) —
|
|
222
|
+
a pausing node declares who may continue it. `approval` declares `'service'`.
|
|
223
|
+
- The engine refuses a `'service'` suspension unless the signal carries
|
|
224
|
+
`RESUME_AUTHORITY_SERVICE` (`@objectstack/spec/contracts`), a **symbol** the
|
|
225
|
+
owning service stamps in-process — a JSON body can never produce one, so the
|
|
226
|
+
transport cannot forge it. `ApprovalService` stamps it on the tail of a
|
|
227
|
+
decision it has already authorized and recorded.
|
|
228
|
+
- The gate follows a **subflow** pause down to the child the signal would
|
|
229
|
+
actually reach, so resuming the parent is not a way around it.
|
|
230
|
+
- Refusal returns `{ success: false, code: 'forbidden' }` and the route answers
|
|
231
|
+
**403**. Nothing is consumed — the request stays pending and the run stays
|
|
232
|
+
parked, so the real decision still lands.
|
|
233
|
+
|
|
234
|
+
`screen` and `wait` pauses are unchanged, as is every path that already went
|
|
235
|
+
through the approvals API. What changes for consumers:
|
|
236
|
+
|
|
237
|
+
- **FROM:** finishing an approval with
|
|
238
|
+
`client.automation.resume(flow, runId, { branchLabel: 'approve' })`
|
|
239
|
+
**TO:** `client.approvals.approve(requestId, …)` (or `.reject` / `.recall`).
|
|
240
|
+
The old call now answers 403 and changes nothing.
|
|
241
|
+
- Registering your own pausing node whose continuation belongs to a service
|
|
242
|
+
rather than to whoever holds the run id? Declare `resumeAuthority: 'service'`
|
|
243
|
+
on its descriptor and stamp `RESUME_AUTHORITY_SERVICE` on the signal from that
|
|
244
|
+
service.
|
|
245
|
+
|
|
246
|
+
A suspension now records the node type that produced it
|
|
247
|
+
(`SuspendedRun.nodeType` / `sys_automation_run.node_type`), captured at suspend
|
|
248
|
+
time so a flow republished mid-pause cannot re-type the node out from under the
|
|
249
|
+
gate; rows written before this fall back to the flow definition.
|
|
250
|
+
|
|
251
|
+
- 2fa4ca1: Dynamic approver routing for approval nodes (#3447 P2) — three new declarative capabilities:
|
|
252
|
+
|
|
253
|
+
**`expression` approvers.** A new approver type whose CEL expression resolves WHO approves at node entry, over exactly three roots: `current.*` (the record's live state), `trigger.*` (the submit-time snapshot) and `vars.*` (flow variables, incl. upstream node outputs). `record` and bare field names are rejected before evaluation — on this platform `record` always means "the record at event time", which is ambiguous at an approval node — with error messages that prescribe the correct spelling. The optional `resolveAs: 'user' | 'department' | 'position' | 'team'` re-expands each resolved id through the same graph lookups the static types use; with `behavior: 'per_group'` each intermediate value (e.g. each returned department) forms its own sign-off group. A missing key fails the node loudly; only a present-but-empty result counts as an empty slate.
|
|
254
|
+
|
|
255
|
+
**`onEmptyApprovers` policy.** What an empty resolved slate does, node-level, for all approver types: `admin_rescue` (default — request opens for privileged takeover, the #3424 behaviour), `fail` (node fails), or `auto_approve` (skip the request, continue down the `approve` edge with `output.autoApproved = true`). To support auto-approve, the automation engine now honours `NodeExecutionResult.branchLabel` on the synchronous completion path — the field existed but was only ever consumed via resume signals.
|
|
256
|
+
|
|
257
|
+
**Decision outputs.** `decide(..., { outputs })` hands structured data from the approver to the flow: the author declares allowed keys on the node (`decisionOutputs`), approvers fill values only, and accepted outputs resume the run as `<nodeId>.<key>` variables — a later approval node's expression can read `vars.<nodeId>.picked_departments`, closing "the previous approver picks the next step's approvers" without a record-field detour. Undeclared keys reject the decision; `decision`/`requestId` are reserved. Multi-approver tallies now always pin to the open-time approver snapshot (previously unanimous re-resolved at each decision against the payload snapshot).
|
|
258
|
+
|
|
259
|
+
Also: `collectCelRootIdentifiers` is exported from `@objectstack/formula` (shared by the new `os lint` rules and the runtime pre-check, so they can never drift), resolution inputs are audited on the request snapshot as `__resolvedFrom`, and three new lint rules gate expressions, empty-slate policies and reserved output keys at author time.
|
|
260
|
+
|
|
261
|
+
- 57bab76: Typed `decisionOutputs` declarations (#3447 follow-up). A `decisionOutputs` entry may now be `{ key, label?, type: 'text' | 'user' | 'department' | 'position' | 'team', multiple? }` alongside the bare-string form — a typed entry tells the decision UI to render the matching record picker (id values; `multiple` collects an id array) instead of free text, turning "paste user ids" into "pick people". The type shapes only the input widget: the runtime whitelist works by `key` either way, via the new `normalizeDecisionOutputs` helper exported from `@objectstack/spec/automation` — the single reader of the union shape shared by the service, the request read, and `os lint`. The request read now carries `decision_output_defs` (normalized declarations) alongside the version-skew-safe `decision_outputs` key list.
|
|
262
|
+
|
|
263
|
+
### Patch Changes
|
|
264
|
+
|
|
265
|
+
- d058594: fix(approvals): refuse `organization` on directory-less approver types instead
|
|
266
|
+
of silently ignoring it (ADR-0105 D9)
|
|
267
|
+
|
|
268
|
+
`user`, `field` and `manager` return EARLY in `resolveApproverSpec` — they name
|
|
269
|
+
a person outright rather than expanding a directory. D9's org resolution was
|
|
270
|
+
placed after those returns, so an `organization` declared on one of them never
|
|
271
|
+
reached the check: it was silently INERT.
|
|
272
|
+
|
|
273
|
+
That is the one behaviour ADR-0105 D9 rules out and the authoring docs
|
|
274
|
+
explicitly promise against ("`organization` on those is refused at runtime").
|
|
275
|
+
The `os lint` rule caught it at author time, but the runtime claim was false —
|
|
276
|
+
and a stored flow that predates the lint, or one assembled programmatically,
|
|
277
|
+
got no signal at all.
|
|
278
|
+
|
|
279
|
+
Resolution now happens at the top of `resolveApproverSpec`, above every early
|
|
280
|
+
return, so the refusal reaches all three types. The ordinary path is unchanged
|
|
281
|
+
and still costs nothing: with no `organization` declared the resolver returns
|
|
282
|
+
the request's organization without reading anything.
|
|
283
|
+
|
|
284
|
+
Found by cloud's group-posture dogfood driving a real `group` boot — the
|
|
285
|
+
resolver's own unit tests could not see it, because they call the resolver
|
|
286
|
+
directly and never traverse the early return.
|
|
287
|
+
|
|
288
|
+
- 879ea13: ADR-0105 Phase 0 + Phase 1: group tenancy posture; organization scope as a
|
|
289
|
+
first-class authorization dimension.
|
|
290
|
+
|
|
291
|
+
> This release carries BREAKING spec removals (see "Enforce-or-remove" below)
|
|
292
|
+
> but is recorded as `minor`: every publishable package is in the Changesets
|
|
293
|
+
> lockstep group, so one `major` would promote the whole monorepo. Breaking
|
|
294
|
+
> changes ship as `minor` during the launch window — the migration notes below
|
|
295
|
+
> are what reach consumers in `CHANGELOG.md`.
|
|
296
|
+
|
|
297
|
+
## Tenancy is now a spectrum (D1)
|
|
298
|
+
|
|
299
|
+
`single | group | isolated`, resolved by the `tenancy` service and selected with
|
|
300
|
+
the new `OS_TENANCY_POSTURE` env var. Existing deployments are unchanged:
|
|
301
|
+
`OS_TENANCY_POSTURE` unset derives the posture from `OS_MULTI_ORG_ENABLED`
|
|
302
|
+
(`true` ⇒ `isolated`, else `single`). An unrecognized value throws at boot
|
|
303
|
+
rather than silently landing in a posture with no organization wall.
|
|
304
|
+
|
|
305
|
+
- `single` — no wall (unchanged).
|
|
306
|
+
- `group` — **new.** Organizations are membership boundaries over one shared
|
|
307
|
+
dataset; Layer 0 becomes `organization_id IN accessible_org_ids` (union / MOAC
|
|
308
|
+
semantics). Enforced by the OPEN engine.
|
|
309
|
+
- `isolated` — today's `multi`, renamed. Behavior, enterprise `org-scoping`
|
|
310
|
+
probe and degraded-boot handling all unchanged.
|
|
311
|
+
|
|
312
|
+
## Organization scope is a first-class context field (D2)
|
|
313
|
+
|
|
314
|
+
`ExecutionContext.accessible_org_ids` — every organization the caller holds a
|
|
315
|
+
currently-valid membership in (ADR-0091 validity windows) — is resolved once by
|
|
316
|
+
`resolveAuthzContext` and carried by every transport. The `group` wall reads it
|
|
317
|
+
directly; RLS policies may reference it as
|
|
318
|
+
`organization_id IN (current_user.accessible_org_ids)`. An empty or absent set
|
|
319
|
+
fails the wall closed.
|
|
320
|
+
|
|
321
|
+
Only the Layer 0 PREDICATE widens. Composition is untouched: the wall is still
|
|
322
|
+
computed independently of the RLS compiler, AND-composed outermost, and
|
|
323
|
+
crossable only by a true `PLATFORM_ADMIN` on a posture-permitting object — so
|
|
324
|
+
ADR-0095's W1/W2 invariants hold in every posture.
|
|
325
|
+
|
|
326
|
+
## Two P0 correctness fixes (D3, D4) — behavior changes
|
|
327
|
+
|
|
328
|
+
**D3 — app-authored org-scoped RLS policies are no longer silently dropped**
|
|
329
|
+
(finding F1, framework#3539). `collectRLSPolicies` used to strip any policy whose
|
|
330
|
+
`using` contained the substring `current_user.organization_id` when isolation was
|
|
331
|
+
inactive, which swallowed app-authored policies as well as the platform's own.
|
|
332
|
+
Stripping is now decided by PROVENANCE (identity against the shipped
|
|
333
|
+
declaration). **Upgrade impact:** in a deployment with no organization wall, an
|
|
334
|
+
app-authored policy referencing the active organization is now RETAINED and
|
|
335
|
+
fails closed (zero rows) with a one-time warning, where it previously vanished
|
|
336
|
+
and the object read unscoped. `getReadFilter` shared the defect, so analytics and
|
|
337
|
+
raw-SQL consumers were affected too. If a policy was only ever meant for
|
|
338
|
+
multi-org, delete it or install `@objectstack/organizations`.
|
|
339
|
+
|
|
340
|
+
**D4 — `viewAllRecords`/`modifyAllRecords` never cross an organization
|
|
341
|
+
boundary** (finding F2, framework#3540). Under a wall-less posture nothing
|
|
342
|
+
bounded the wildcard superuser bits `organization_admin` carries, so a
|
|
343
|
+
deployment that accumulated organizations (personal orgs on signup) made every
|
|
344
|
+
owner/admin an environment-wide superuser. `auto-org-admin-grant` now grants a
|
|
345
|
+
de-VAMA'd `organization_admin_no_bypass` variant when no wall is enforced, and
|
|
346
|
+
revokes the superseded variant whenever the posture changes. **Upgrade impact:**
|
|
347
|
+
in `single` posture an org owner/admin keeps full CRUD but loses the blanket
|
|
348
|
+
ownership/sharing/RLS bypass. Deliberate deployment-wide visibility remains
|
|
349
|
+
available through `admin_full_access` or an explicitly authored permission set —
|
|
350
|
+
it just stops being a side effect of a better-auth membership role.
|
|
351
|
+
|
|
352
|
+
## Engine-owned organization stamping (D5)
|
|
353
|
+
|
|
354
|
+
Under any wall-enforcing posture the engine stamps `organization_id` from the
|
|
355
|
+
caller's active organization on an insert that omits it, and validates every
|
|
356
|
+
supplied value against the wall. Idempotent with the enterprise auto-stamp
|
|
357
|
+
(neither overwrites a supplied value). This also closes a real hole: the
|
|
358
|
+
pre-existing post-image check required a non-array payload, so a BULK insert
|
|
359
|
+
could carry a forged `organization_id` per row. One forged row now denies the
|
|
360
|
+
whole write.
|
|
361
|
+
|
|
362
|
+
## Group structure, extension fields and red-line lints (D6, D7)
|
|
363
|
+
|
|
364
|
+
- `sys_organization` gains `parent_organization_id` and `sort_order` — a
|
|
365
|
+
**reporting dimension only**.
|
|
366
|
+
- New lint `validateOrgAxisRedLines` (`org-axis-permission-inheritance`,
|
|
367
|
+
`org-axis-cross-org-bu-grant`), wired into `os lint` / `os compile` /
|
|
368
|
+
`os validate`: an RLS policy or sharing rule that walks the org tree is an
|
|
369
|
+
error, as is a business-unit grant on a platform-global object.
|
|
370
|
+
- Extension fields on better-auth-managed objects ride the existing ADR-0092
|
|
371
|
+
whitelist. A new guard derives better-auth's real field surface from
|
|
372
|
+
`getAuthTables()` at the pinned version and fails the build on any name
|
|
373
|
+
collision, so a library upgrade cannot silently take ownership of a column.
|
|
374
|
+
|
|
375
|
+
## Enforce-or-remove (D11) — BREAKING
|
|
376
|
+
|
|
377
|
+
Both removals are of surface that had **zero runtime consumers**, so no
|
|
378
|
+
behavior changes; authoring them is now a no-op instead of a lint warning.
|
|
379
|
+
|
|
380
|
+
- **`PermissionSet.contextVariables` — REMOVED.** The RLS compiler never read
|
|
381
|
+
it. FROM → TO: a set a policy needs as `field IN (current_user.<key>)` is now
|
|
382
|
+
supplied by a registered membership resolver (below); a constant belongs in
|
|
383
|
+
the policy itself as a literal (`status = 'published'`).
|
|
384
|
+
- **`Territory` / `TerritoryModel` / `TerritoryType` (`security/territory.zod.ts`)
|
|
385
|
+
— REMOVED.** No runtime object, stack field or resolver existed. FROM → TO:
|
|
386
|
+
matrix requirements are served by multi-position × business-unit anchoring; a
|
|
387
|
+
generalized dimension-security module will arrive with its own ADR.
|
|
388
|
+
- **`ExecutionContext.rlsMembership` — PRODUCTIZED.** The bag the compiler has
|
|
389
|
+
merged since ADR-0056 finally has a producer: register an
|
|
390
|
+
`IRlsMembershipResolver` (`@objectstack/spec/contracts`) under the
|
|
391
|
+
`rls-membership-resolver` service, declaring the keys it owns. Fail-closed by
|
|
392
|
+
construction — an unresolved key makes its policies drop out. Kernel-owned
|
|
393
|
+
keys (`accessible_org_ids`, `org_user_ids`, …) are reserved and cannot be
|
|
394
|
+
overwritten from this seam.
|
|
395
|
+
|
|
396
|
+
## Edition boundary (D12)
|
|
397
|
+
|
|
398
|
+
The `group` posture's enforcement primitives ship OPEN — the union wall,
|
|
399
|
+
`accessible_org_ids` resolution, D5 stamping/validation, the D3/D4 correctness
|
|
400
|
+
fixes and the D6 lints — because the correctness of a wall is never a paid
|
|
401
|
+
feature (cloud ADR-0016 铁律「强制免费、治理收费」). `isolated` keeps its existing
|
|
402
|
+
enterprise `org-scoping` probe, so the current commercial boundary for
|
|
403
|
+
legal-entity isolation is unchanged by this release.
|
|
404
|
+
|
|
405
|
+
- 2ba560a: fix(plugin-approvals): give the decision actions a visual hierarchy (objectui#2762 P1-5)
|
|
406
|
+
|
|
407
|
+
The `sys_approval_request` decision actions all declared as equal-weight
|
|
408
|
+
buttons, so the drawer's action bar rendered five identical outlined
|
|
409
|
+
buttons with no emphasis on the primary path. `approval_approve` now
|
|
410
|
+
declares `variant: 'primary'` and `approval_reject` declares
|
|
411
|
+
`variant: 'danger'`, so a metadata-driven renderer highlights Approve and
|
|
412
|
+
styles Reject as destructive — matching the hierarchy the mobile card
|
|
413
|
+
already has. Pure metadata; the secondary levers stay unstyled (tertiary).
|
|
414
|
+
|
|
415
|
+
- 2dda6e7: fix(plugin-approvals): localize the declared decision-action labels (objectui#2762 P0-3)
|
|
416
|
+
|
|
417
|
+
The Approval Center's decision drawer rendered the `sys_approval_request`
|
|
418
|
+
declared actions with their literal metadata labels — English **Approve /
|
|
419
|
+
Reject / Reassign / Send back / Request info** in a zh-CN workspace, sitting
|
|
420
|
+
next to the same page's localized 通过 / 拒绝 inbox buttons. The plugin's
|
|
421
|
+
translation bundle covered fields and views but had no `_actions` node, so
|
|
422
|
+
the console's `_actions.<name>.label` resolution had nothing to hit.
|
|
423
|
+
|
|
424
|
+
- Re-ran `os i18n extract` against the plugin's config: the bundles now carry
|
|
425
|
+
`_actions` translations (label, confirmText, successMessage, param labels
|
|
426
|
+
and helpText) for all eight decision actions — `approval_approve`,
|
|
427
|
+
`approval_reject`, `approval_reassign`, `approval_send_back`,
|
|
428
|
+
`approval_request_info`, `approval_remind`, `approval_recall`,
|
|
429
|
+
`approval_resubmit` — in zh-CN, ja-JP and es-ES (en keeps the metadata
|
|
430
|
+
literals).
|
|
431
|
+
- The extract also surfaced other untranslated gaps, now filled in all three
|
|
432
|
+
locales: the `returned` status option, the `sys_approval_action.action`
|
|
433
|
+
audit options (`reassign` / `remind` / `request_info` / `comment` /
|
|
434
|
+
`revise` / `resubmit` / `ooo_substitute`), the `attachments` field, and the
|
|
435
|
+
`my_pending` / `recent` view empty states.
|
|
436
|
+
|
|
437
|
+
- 474fe39: feat(approvals): declare approver value bindings; retire `queue` approver authoring (#3508)
|
|
438
|
+
|
|
439
|
+
- `@objectstack/spec` exports `APPROVER_VALUE_BINDINGS` — the single declaration of how a
|
|
440
|
+
designer must source each approver row's `value`: `user`/`team`/`department`/`position`
|
|
441
|
+
are DATA-record lookups on the system directory objects (`sys_user` / `sys_team` /
|
|
442
|
+
`sys_business_unit` / `sys_position`; `position` commits the machine **name**, the
|
|
443
|
+
others the row id), `org_membership_level` is a closed enum (`ORG_MEMBERSHIP_LEVELS`),
|
|
444
|
+
`manager` is auto-resolved, `field` names a trigger-object field, and `queue` is
|
|
445
|
+
unsupported. Also exports `NON_AUTHORABLE_APPROVER_TYPES`.
|
|
446
|
+
- `queue` approver type is deprecated-for-authoring: it still parses (stored flows keep
|
|
447
|
+
loading and rendering) but is published in `xEnumDeprecated`, so designers stop
|
|
448
|
+
offering it — the runtime has no queue resolution and the slot routes to nobody. The
|
|
449
|
+
approver `value` xRef now also maps `manager`, so designers can render its
|
|
450
|
+
auto-resolved state. No authored key is removed; nothing to migrate. If a flow carries
|
|
451
|
+
`{ type: 'queue' }`, replace it with `team` / `department` / `position` (or a concrete
|
|
452
|
+
`user`) until a real ownership-queue implementation lands.
|
|
453
|
+
- `@objectstack/plugin-approvals` now warns at resolution time when a stored `queue`
|
|
454
|
+
approver is skipped.
|
|
455
|
+
- `@objectstack/lint` adds `approval-approver-type-unsupported` (warning) for approver
|
|
456
|
+
types that are declared but not implemented by the runtime.
|
|
457
|
+
|
|
458
|
+
- 0bc685a: fix(approvals): return decision attachments as file values, not "[object Object]" (#3504)
|
|
459
|
+
|
|
460
|
+
`sys_approval_action.attachments` is a `Field.file`, so the column **stores an
|
|
461
|
+
opaque `sys_file` id** (ADR-0104 D3 — the stored form of every media field). The
|
|
462
|
+
ObjectQL read path resolves that id into its expanded
|
|
463
|
+
`{ id, name, size, mimeType, url }` form on the way out. But `rowFromAction`
|
|
464
|
+
mapped the column with `.map(String)`, collapsing each expanded value to the
|
|
465
|
+
literal string `"[object Object]"`. Every `listActions` consumer (the approval
|
|
466
|
+
inbox timeline) then received garbage: the attachment chip had no filename and
|
|
467
|
+
its id was `"[object Object]"`, so opening it 404'd.
|
|
468
|
+
|
|
469
|
+
- `ApprovalActionRow.attachments` is now `ApprovalActionAttachment[]` — the
|
|
470
|
+
expanded file value plus its id, so a consumer can label and open an
|
|
471
|
+
attachment without needing read access to the system `sys_file` object (which
|
|
472
|
+
regular approvers do not have).
|
|
473
|
+
- Three read forms are accepted: the expanded value (the normal case), a bare id
|
|
474
|
+
(nothing to expand it into — storage service absent, file not committed), and
|
|
475
|
+
a legacy inline blob written before file-as-reference (`file_id` /
|
|
476
|
+
`mime_type`), until the backfill converts it. The id test reuses the
|
|
477
|
+
platform's `isFileIdToken`, so this and the engine's read resolver cannot
|
|
478
|
+
disagree about what counts as an id.
|
|
479
|
+
- The decision _input_ (`ApprovalDecisionInput.attachments`) is unchanged — it
|
|
480
|
+
still takes fileId strings, which is also exactly what the column stores. Only
|
|
481
|
+
the read shape changed.
|
|
482
|
+
|
|
483
|
+
- b949059: fix(approvals): a dead approval run no longer leaves the record RECORD_LOCKED (#3456)
|
|
484
|
+
|
|
485
|
+
The record lock is keyed on a **pending** `sys_approval_request`, and it could
|
|
486
|
+
not tell _the run that owns that request_ from _an unrelated user editing the
|
|
487
|
+
record_. So a flow that touched its own target record while its own approval was
|
|
488
|
+
still pending — a manual `resume` with no decision, or a node that writes the
|
|
489
|
+
record between opening the approval and the decision — died on its own
|
|
490
|
+
`RECORD_LOCKED`, and the record stayed locked behind the dead run. Recovery
|
|
491
|
+
existed (#3424 lets an admin `recall`/`reject` to release it) but nothing made it
|
|
492
|
+
self-healing.
|
|
493
|
+
|
|
494
|
+
Both halves are now closed.
|
|
495
|
+
|
|
496
|
+
**Prevention — the owning run may write its own record.** The automation engine
|
|
497
|
+
stamps `flowRunId` onto the run context at setup, alongside `runAs`, and it
|
|
498
|
+
travels with every data node's ObjectQL context into `ctx.provenance`. The lock
|
|
499
|
+
hook exempts a write whose `flowRunId` matches the pending request's `flow_run_id`.
|
|
500
|
+
It is keyed on run identity rather than elevation on purpose: a `runAs:'user'`
|
|
501
|
+
run stays fully RLS-scoped while it writes. `flowRunId` is pure provenance —
|
|
502
|
+
server-constructed like `isSystem`, never client-supplied, evaluated by no
|
|
503
|
+
security middleware, and the only write it permits is to the one record its own
|
|
504
|
+
run already holds a pending request against.
|
|
505
|
+
|
|
506
|
+
**Recovery — a sweep releases records held by runs that died anyway.** A pending
|
|
507
|
+
request whose owning run has reached a terminal state (`completed`, `failed`,
|
|
508
|
+
`cancelled`, `timed_out`) can never be decided, so it is finalised as `recalled`
|
|
509
|
+
— releasing the lock — and audited under the reserved actor `system:dead-run`
|
|
510
|
+
with the run and its status in the comment, so it is never mistaken for a
|
|
511
|
+
submitter's withdrawal. It runs on the existing approvals sweep clock, which also
|
|
512
|
+
covers the case no in-band handler can: a run killed by a process crash.
|
|
513
|
+
|
|
514
|
+
The sweep is fail-safe by construction. It acts only on an explicit terminal
|
|
515
|
+
status from a closed set; `paused` (the normal state of a live approval),
|
|
516
|
+
`running`, an unrecognised status, an unknown run, a `getRun` that throws, and a
|
|
517
|
+
deployment with no automation engine are all read as "still alive". The failure
|
|
518
|
+
mode is "a dead run's lock survives until an admin recalls it" — today's
|
|
519
|
+
behaviour — never "a live approval is destroyed".
|
|
520
|
+
|
|
521
|
+
Also fixes `AutomationEngine.getRun`, which returned the **first** log entry for
|
|
522
|
+
a run id rather than the latest. A run that pauses and later finishes records two
|
|
523
|
+
entries under one id, so every suspend-then-finish run — every approval, screen
|
|
524
|
+
and wait flow — reported itself as `paused` forever, both on the Runs
|
|
525
|
+
observability surface and to this sweep.
|
|
526
|
+
|
|
527
|
+
One shape was left out here and closed separately in #3712: a `runAs:'user'` run
|
|
528
|
+
with no trigger user (a schedule) resolved no ObjectQL context at all, so it
|
|
529
|
+
carried no `flowRunId` and stayed subject to the lock. It now passes a
|
|
530
|
+
provenance-only context — the run id and nothing the security middleware keys on
|
|
531
|
+
— so it is attributable without acquiring a principal, and its documented
|
|
532
|
+
unscoped posture (#1888) is unchanged.
|
|
533
|
+
|
|
534
|
+
- be1c52c: fix(approvals): admin override for a request routed to an unstaffed approver (#3424)
|
|
535
|
+
|
|
536
|
+
An `approval` node routed to a `position` (or `team`/`department`) with **no
|
|
537
|
+
holders** resolved to only the unresolvable `position:<name>` literal in
|
|
538
|
+
`pending_approvers` — no concrete user was in the slate. Every normal
|
|
539
|
+
`decide` / `reassign` / `recall` then returned `FORBIDDEN` (not a pending
|
|
540
|
+
approver) and, with `lockRecord`, the target record stayed `RECORD_LOCKED`
|
|
541
|
+
forever: a data-availability dead-end with no in-product recovery (the only exit
|
|
542
|
+
was editing the DB by hand). Very easy to hit in fresh/demo orgs (positions
|
|
543
|
+
seeded, holders not) and whenever a role is vacated in production.
|
|
544
|
+
|
|
545
|
+
A **platform or tenant admin** — the same posture the engine's superuser bypass
|
|
546
|
+
already trusts — may now act on any _pending_ request to release it: **approve,
|
|
547
|
+
reject, reassign** it to a real approver, or **recall** it. The override finalizes
|
|
548
|
+
the request (which releases the record lock, keyed on a pending request); a
|
|
549
|
+
tenant admin's authority is org-scoped, a platform admin's is not, and the
|
|
550
|
+
decision is audited under the admin's own id. An admin approval is authoritative,
|
|
551
|
+
finalizing the node even under `unanimous` / `quorum` / `per_group` rather than
|
|
552
|
+
counting as one vote among the (empty) slate.
|
|
553
|
+
|
|
554
|
+
- `sys_approval_request.viewer` gains `can_override` (server-computed): true for a
|
|
555
|
+
privileged admin on a pending request. The `approve` / `reject` / `reassign`
|
|
556
|
+
declared actions OR it into their `visible` gate, so the console surfaces the
|
|
557
|
+
recovery path without a hand-wired button. Existing approver/submitter gating is
|
|
558
|
+
unchanged.
|
|
559
|
+
- `openNodeRequest` now logs a loud warning when a node resolves to **no concrete
|
|
560
|
+
approver**, so the misconfiguration is visible instead of silently locking the
|
|
561
|
+
record. The literal-fallback behavior (kept for 15.x slot back-compat) is
|
|
562
|
+
otherwise unchanged.
|
|
563
|
+
|
|
564
|
+
- c5ff96d: fix(approvals): a schedule-triggered run can write its own locked record (#3712)
|
|
565
|
+
|
|
566
|
+
#3456 let the run that opened a pending approval write its own target record,
|
|
567
|
+
keyed on `flowRunId`. It worked for every run that resolves an identity and
|
|
568
|
+
missed the one that doesn't: an effective `runAs:'user'` run with **no trigger
|
|
569
|
+
user** — a schedule being the canonical case — passed no ObjectQL context at
|
|
570
|
+
all, so nothing carried the run id and the run still died on its own
|
|
571
|
+
`RECORD_LOCKED`.
|
|
572
|
+
|
|
573
|
+
The blocker was never the lock. It was that "no identity" and "no context" were
|
|
574
|
+
the same thing on the wire, so a run could not say _who it was_ without also
|
|
575
|
+
claiming _what it was allowed to do_.
|
|
576
|
+
|
|
577
|
+
**A run with no principal now passes provenance alone.**
|
|
578
|
+
`resolveRunDataContext` returns `{ flowRunId }` — no `userId`, no `positions`,
|
|
579
|
+
no `permissions`, not even `isSystem: false`. Every principal gate keys on one
|
|
580
|
+
of those fields (the elevation short-circuit on `isSystem`, the ADR-0103
|
|
581
|
+
engine-owned write guard and the ADR-0090 D12 delegated-admin gate on `userId`,
|
|
582
|
+
the empty-principal fall-open on all three), so this context authorizes
|
|
583
|
+
**identically to no context at all**. The run keeps the documented #1888
|
|
584
|
+
unscoped posture, its loud `[runAs]` warning, and the
|
|
585
|
+
`flow-schedule-runas-unscoped` build-time lint. Nothing about what it may touch
|
|
586
|
+
changed — only that it can now be attributed.
|
|
587
|
+
|
|
588
|
+
**Provenance moved out of the hook session, into `ctx.provenance`.** `session`
|
|
589
|
+
answers _who is calling_ and is absent when no identity envelope was supplied —
|
|
590
|
+
a distinction real gates depend on (the attachment access gate skips bare-kernel
|
|
591
|
+
writes on exactly that test). Folding a run id into `session` would have forced
|
|
592
|
+
an identity-less run to present an empty session, silently turning "no caller"
|
|
593
|
+
into "an anonymous caller" and narrowing the #1888 fail-open for attachments
|
|
594
|
+
alone. `HookContext.provenance.flowRunId` says what produced the write; the
|
|
595
|
+
approvals lock reads it there.
|
|
596
|
+
|
|
597
|
+
Also relaxes `BaseEngineOptionsSchema.context` to a partial envelope
|
|
598
|
+
(`ExecutionContextInput`). `positions`/`permissions`/`isSystem` carry parse-time
|
|
599
|
+
defaults, which made them _required_ on a caller-supplied option and asserted
|
|
600
|
+
something untrue — that every data-engine context carries a principal. Callers
|
|
601
|
+
have always passed slices (`{ isSystem: true }` for a system read); the type now
|
|
602
|
+
says so.
|
|
603
|
+
|
|
604
|
+
Migration: nothing to change unless you read the run id inside a hook. If you
|
|
605
|
+
wrote `ctx.session.flowRunId`, read `ctx.provenance.flowRunId` instead — the
|
|
606
|
+
field never shipped under the old name.
|
|
607
|
+
|
|
608
|
+
- d2a8695: fix(approvals)!: an approval request is visible to its participants, not to the whole tenant (#3590)
|
|
609
|
+
|
|
610
|
+
`getRequest` / `listRequests` / `countRequests` deliberately query with
|
|
611
|
+
`SYSTEM_CTX` to bypass RLS — as the code comments say, the approver-visibility
|
|
612
|
+
rule spans identity forms RLS cannot model cleanly, so it has to be expressed in
|
|
613
|
+
the service. Only the **tenant** half of that rule was ever applied. The
|
|
614
|
+
participant half was named in the comment and never written, so **any
|
|
615
|
+
authenticated user could read any approval request in their tenant** — its
|
|
616
|
+
payload snapshot, its full decision history, and (once decision attachments
|
|
617
|
+
derived their access from the request, #3580) its files.
|
|
618
|
+
|
|
619
|
+
`approverId` on `listRequests` is a _filter_, not authorization: omitting it
|
|
620
|
+
returned the whole tenant.
|
|
621
|
+
|
|
622
|
+
A caller now sees a request when they are a participant — the submitter, a
|
|
623
|
+
current approver (via the normalized approver index, so every identity form the
|
|
624
|
+
write path recorded is covered), or someone who has already acted on it (a past
|
|
625
|
+
approver whose slot has moved on, a commenter). Admins with override authority
|
|
626
|
+
keep the unrestricted view the "all requests" console surface depends on, and a
|
|
627
|
+
tokenless context sees nothing.
|
|
628
|
+
|
|
629
|
+
Keying on the concrete user id is sufficient rather than an approximation:
|
|
630
|
+
position/team/manager/field approvers are resolved to concrete user ids at open
|
|
631
|
+
time, and the `type:value` literal is only the fallback for a spec that resolved
|
|
632
|
+
to _nobody_ — a slot no one can act on either way. So this cannot hide a request
|
|
633
|
+
from someone who could actually act on it.
|
|
634
|
+
|
|
635
|
+
**A write path's own result is not re-gated.** Every operation echoes back the
|
|
636
|
+
request it just changed; the operation already authorized itself, and re-asking
|
|
637
|
+
would answer wrong for a context carrying no `userId` (a flow-driven resume, a
|
|
638
|
+
service-to-service call), turning a successful write into `null`.
|
|
639
|
+
|
|
640
|
+
Marked breaking because a client that listed requests without an `approverId`
|
|
641
|
+
filter and expected the whole tenant will now receive only its own — which is
|
|
642
|
+
the point.
|
|
643
|
+
|
|
644
|
+
- 84e7be9: feat(plugin-approvals): expose per-group membership of pending approvers (objectui#2807)
|
|
645
|
+
|
|
646
|
+
`per_group` (会签) requests now carry `pending_approver_groups` on the
|
|
647
|
+
enriched row — a map from each still-pending approver id to the group key(s)
|
|
648
|
+
it fills (e.g. `{ "u_devadmin": ["finance", "legal"] }`). A client can label
|
|
649
|
+
each "waiting on" chip with the group it represents instead of showing
|
|
650
|
+
duplicate, context-free names.
|
|
651
|
+
|
|
652
|
+
- Resolved in `attachDecisionProgress` from the same open-time
|
|
653
|
+
`__approverGroups` snapshot the `decision_progress` groups already use, so
|
|
654
|
+
the two never disagree.
|
|
655
|
+
- Only the **pending** slots are mapped (a resolved approver has left
|
|
656
|
+
`pending_approvers`), and **synthetic** (unnamed, `#N`) group keys are
|
|
657
|
+
dropped — a `· #0` sub-tag would be noise.
|
|
658
|
+
- Absent for non-`per_group` behaviors. Display-only; the engine's
|
|
659
|
+
finalization tally stays authoritative.
|
|
660
|
+
- Added to the `ApprovalRequestRow` contract in `@objectstack/spec`.
|
|
661
|
+
|
|
662
|
+
- debc23a: feat(approvals): enrich inbox rows with `payload_labels` (snapshot field labels)
|
|
663
|
+
|
|
664
|
+
The approvals inbox summary title-cased raw snapshot machine keys
|
|
665
|
+
(`assessment_status` → "Assessment Status") because the API sent no field
|
|
666
|
+
labels. `ApprovalService.enrichRows` now attaches `payload_labels` (snapshot
|
|
667
|
+
field key → the target object's field label), symmetric with the existing
|
|
668
|
+
`payload_display` (which resolves the values), and `ApprovalRequestRow` gains
|
|
669
|
+
the field. For a single-locale project the schema label is already the
|
|
670
|
+
localized string, so a client can render the human field name (e.g. "考核状态")
|
|
671
|
+
instead of a prettified English key.
|
|
672
|
+
|
|
673
|
+
- 0f8ad09: feat(spec)+fix(approvals): publish approver value data sources, order the type enum for authors, stop silent dead approver slots (#3508 / #3807 follow-ups)
|
|
674
|
+
|
|
675
|
+
Four follow-ups from browser-verifying the #3508 approver work end to end.
|
|
676
|
+
|
|
677
|
+
**`APPROVER_VALUE_SOURCES` — the designer stops guessing where candidates live.**
|
|
678
|
+
`xRef.map` only ever named a picker KIND (`'team'`), never where that picker's
|
|
679
|
+
rows come from, so the designer carried its own copy of the data contract — and
|
|
680
|
+
the first copy was wrong: every directory kind was wired to `GET
|
|
681
|
+
/api/v1/meta/:type`, the metadata REGISTRY, which does not hold `sys_user` /
|
|
682
|
+
`sys_team` / `sys_business_unit` / `sys_position` rows. Candidates came back
|
|
683
|
+
empty and the control degraded to free text (#3508). The binding is now
|
|
684
|
+
projected onto the published JSON schema as `xRef.sources` — `{ source: 'data',
|
|
685
|
+
object, valueField }` for the record-backed kinds, the closed enum inline for
|
|
686
|
+
`org_membership_level` — derived from `APPROVER_VALUE_BINDINGS` so the two
|
|
687
|
+
cannot drift, and inheriting its `satisfies` exhaustiveness (a new
|
|
688
|
+
`ApproverType` member that declares no source is a compile error). Presentation
|
|
689
|
+
— which field to show, whether to open a people-picker, what subtitle to use —
|
|
690
|
+
stays a renderer decision.
|
|
691
|
+
|
|
692
|
+
**`ApproverType` declaration order is now the authoring recommendation.**
|
|
693
|
+
objectui#2834 argued for leading with indirect bindings and shipped that order
|
|
694
|
+
in its own options array — which the Studio inspector never reads: it derives
|
|
695
|
+
the picker from this enum via the published schema, so `user` still came first.
|
|
696
|
+
The intent only takes effect if the enum carries it, so the enum now reads
|
|
697
|
+
`manager, position, department, team, field, expression, org_membership_level,
|
|
698
|
+
user` (deprecated `role` / `queue` still parse and stay out of every picker via
|
|
699
|
+
`xEnumDeprecated`). Binding one specific person is the least portable choice an
|
|
700
|
+
author can make — it breaks when the flow moves to another environment (that id
|
|
701
|
+
does not exist there) and again when that person leaves.
|
|
702
|
+
|
|
703
|
+
**A graph approver that expands to nobody no longer does it in silence.**
|
|
704
|
+
`queue` already warned (#3508); every OTHER graph type — `team`, `department`,
|
|
705
|
+
`position`, `org_membership_level`, `manager` — fell back to the same
|
|
706
|
+
unactionable `type:value` literal without a word. That silence is what let
|
|
707
|
+
#3807 hide for as long as it did: the request opened with an empty slate and
|
|
708
|
+
the first symptom was a permanently stuck approval (#3424). The fallback stays
|
|
709
|
+
(15.x slots and substring fixtures depend on it); it now logs the type, value
|
|
710
|
+
and organization that produced it. `user` / `field` stay quiet — they take the
|
|
711
|
+
id they were given and never had an "expanded to nobody" state.
|
|
712
|
+
|
|
713
|
+
**`plugin-sharing`'s identical org scope is pinned by tests.**
|
|
714
|
+
`BusinessUnitGraphService.orgScope` has the same strict `organization_id`
|
|
715
|
+
equality #3807 fixed in approvals. It is unreachable today — every materialized
|
|
716
|
+
`sys_sharing_rule` carries `organization_id = null`, so the filter is skipped —
|
|
717
|
+
and widening an authorization path on a defect that cannot currently fire is
|
|
718
|
+
not a change to make blind. New tests lock both the reachable paths and the
|
|
719
|
+
divergence itself, so if sharing ever adopts the null-org=env-wide reading it
|
|
720
|
+
is a deliberate edit to a named test rather than a silent behaviour change.
|
|
721
|
+
|
|
722
|
+
- 376a061: Surface the approval node's author-declared `decisionOutputs` keys on the request read as `ApprovalRequestRow.decision_outputs` (#3447 P2 UI enablement). The set varies per request (each node declares its own), so it rides the row rather than the object's static action params — a decision UI renders one input per key and POSTs `outputs` with the decision.
|
|
723
|
+
- 3ea7271: fix(approvals): a `department` approver resolves against env-wide business units (#3807)
|
|
724
|
+
|
|
725
|
+
`expandBusinessUnitUsers` scoped its `sys_business_unit` reads with a strict
|
|
726
|
+
`organization_id = <request org>` equality, so a unit whose `organization_id`
|
|
727
|
+
is `null` was invisible: the seed check found no row, the expansion returned
|
|
728
|
+
`[]`, and the approver fell back to the dead `department:<id>` literal that
|
|
729
|
+
routes to nobody.
|
|
730
|
+
|
|
731
|
+
That is the normal case, not an edge case. An app's org tree is seeded, and a
|
|
732
|
+
seed cannot know the organization id the runtime mints at boot, so every seeded
|
|
733
|
+
unit carries `organization_id = null` — while an approval request always
|
|
734
|
+
carries an org. Every business unit a flow author could pick therefore resolved
|
|
735
|
+
to nobody, silently: the request opens, the slate is empty, and (with
|
|
736
|
+
`lockRecord`) the record stays locked with no one able to act (#3424 is the
|
|
737
|
+
downstream shape of the same dead end). Verified against a live showcase stack:
|
|
738
|
+
a `{ type: 'department', value: 'bu_hq_finance' }` approver produced
|
|
739
|
+
`pending_approvers: "department:bu_hq_finance"` while the unit's member sat
|
|
740
|
+
right there in `sys_business_unit_member`.
|
|
741
|
+
|
|
742
|
+
Both the seed check and the subtree descent now scope to **this org ∪
|
|
743
|
+
env-wide** — `$or: [{ organization_id: <org> }, { organization_id: null }]` —
|
|
744
|
+
the same predicate `sys_metadata`'s pending-draft listing settled on for the
|
|
745
|
+
identical reason (a strict equality silently dropping env-wide rows). The wall
|
|
746
|
+
between two organizations is unchanged: another org's unit still fails the
|
|
747
|
+
match, and a null-org parent does not drag another org's child unit into the
|
|
748
|
+
subtree.
|
|
749
|
+
|
|
750
|
+
Note the same strict-equality scope exists in `plugin-sharing`'s
|
|
751
|
+
`BusinessUnitGraphService.orgScope`. It is not reachable today — every
|
|
752
|
+
materialized `sys_sharing_rule` row carries `organization_id = null`, so the
|
|
753
|
+
filter is skipped — and is left alone here rather than widen an
|
|
754
|
+
access-granting path on a defect that cannot currently fire.
|
|
755
|
+
|
|
756
|
+
- deb538f: fix(storage): let an object delegate file-read authorization to its service
|
|
757
|
+
|
|
758
|
+
Fixes a regression from the governed-download change (ADR-0104 D3 wave 2): a
|
|
759
|
+
**legitimate approver could see a decision attachment's filename but got 403
|
|
760
|
+
opening it**, found by driving app-showcase in a browser as a real non-admin
|
|
761
|
+
approver.
|
|
762
|
+
|
|
763
|
+
Cause: a field-owned file's download was authorized by testing whether the
|
|
764
|
+
caller can READ the owning row. For an ordinary business object that is right —
|
|
765
|
+
row readability _is_ the access rule. For `sys_approval_action` it is the wrong
|
|
766
|
+
authority: the audit table is deliberately closed to ordinary approver
|
|
767
|
+
positions (`operation 'find' … is not permitted for positions [auditor,
|
|
768
|
+
everyone]`), so the test denied the very approver the attachment was filed for.
|
|
769
|
+
The approvals _service_ has always had the real rule, which is why the timeline
|
|
770
|
+
listing the attachment returned 200 while the bytes returned 403.
|
|
771
|
+
|
|
772
|
+
An object may now name a service to answer the question instead:
|
|
773
|
+
|
|
774
|
+
- `ObjectSchema.fileAccessDelegate` — a kernel service that authorizes
|
|
775
|
+
downloads of files owned by that object's media fields.
|
|
776
|
+
- `IFileAccessDelegate.authorizeFileRead(recordId, context)` — the contract.
|
|
777
|
+
- `sys_approval_action` declares `'approvals'`; `ApprovalService.authorizeFileRead`
|
|
778
|
+
reuses the _same_ gate `listActions` applies (visibility of the parent
|
|
779
|
+
request) rather than inventing a second, looser rule for the bytes.
|
|
780
|
+
|
|
781
|
+
**Fails closed**: a declared delegate that is missing or does not implement the
|
|
782
|
+
method denies, rather than silently reverting to the raw read it was declared to
|
|
783
|
+
replace. Objects without the declaration are unchanged.
|
|
784
|
+
|
|
785
|
+
Verified in the browser against app-showcase, both sides of the gate: the
|
|
786
|
+
approver now downloads the real PDF (200), and an anonymous request is still
|
|
787
|
+
refused (401) — the anonymous capability URL the original change closed stays
|
|
788
|
+
closed. A decision attachment ends up exactly as readable as the decision it
|
|
789
|
+
hangs off: never more, and no longer less.
|
|
790
|
+
|
|
791
|
+
- db48ad5: fix(security,approvals,metadata-core): restore batch routes on the eight objects the #3391 P1 companion fix missed (#3026)
|
|
792
|
+
|
|
793
|
+
The #3391 P1 contract made the bulk gate `bulk ∧ derived(child)`: a batch
|
|
794
|
+
request is admitted only when the object grants the `bulk` **primitive** and the
|
|
795
|
+
batched child operation is itself allowed. Before that, the `*Many` routes
|
|
796
|
+
checked only the child verb, so a boilerplate CRUD-five whitelist
|
|
797
|
+
(`['get','list','create','update','delete']`) batched fine.
|
|
798
|
+
|
|
799
|
+
The companion fix — adding the `bulk` primitive wherever an explicit whitelist
|
|
800
|
+
survived — was applied only inside `platform-objects`. Eight objects carrying
|
|
801
|
+
the same boilerplate live in other packages and kept the gap, so `/batch`,
|
|
802
|
+
`createMany`, `updateMany` and `deleteMany` answered `405
|
|
803
|
+
OBJECT_API_METHOD_NOT_ALLOWED` on objects whose single-record create/update/
|
|
804
|
+
delete were wide open. `data-objectstack` rethrows that 405 without falling back
|
|
805
|
+
to per-row writes, which surfaced as a hard error on multi-select delete in the
|
|
806
|
+
Setup grids.
|
|
807
|
+
|
|
808
|
+
Objects reclaimed (whitelist now `['get','list','create','update','delete','bulk']`):
|
|
809
|
+
`sys_capability`, `sys_permission_set`, `sys_position`,
|
|
810
|
+
`sys_position_permission_set`, `sys_user_permission_set`, `sys_user_position`
|
|
811
|
+
(plugin-security); `sys_approval_delegation` (plugin-approvals);
|
|
812
|
+
`sys_view_definition` (metadata-core).
|
|
813
|
+
|
|
814
|
+
No new authority is granted: `bulk` only permits batching verbs each object
|
|
815
|
+
already exposes one record at a time, and every batched row still passes the
|
|
816
|
+
same row- and field-level permission checks. The whitelists stay explicit rather
|
|
817
|
+
than being deleted — seven of the eight are `managedBy`, and
|
|
818
|
+
`reconcileManagedApiMethods` (ADR-0103 D3) early-returns on a non-array
|
|
819
|
+
`apiMethods`, so dropping the line would silently disable the managed-write
|
|
820
|
+
backstop.
|
|
821
|
+
|
|
822
|
+
- 83c161f: feat(automation)!: a flow run with no trigger user may no longer touch data (#3760)
|
|
823
|
+
|
|
824
|
+
An effective `runAs:'user'` run that resolves **no trigger user** used to execute
|
|
825
|
+
its data nodes **UNSCOPED** — it presented no principal, and the data security
|
|
826
|
+
middleware skips when there is no principal, so the run read and wrote every row.
|
|
827
|
+
`runAs:'user'` is an access-_narrowing_ declaration; failing to resolve it must
|
|
828
|
+
never resolve to a grant (ADR-0049). It now **refuses** the operation
|
|
829
|
+
(`UnscopedRunDataAccessError`), naming `runAs:'system'` as the fix.
|
|
830
|
+
|
|
831
|
+
**This was never really about schedules.** The docs, the spec, the runtime
|
|
832
|
+
warning and the lint all described a schedule-shaped problem, and the lint only
|
|
833
|
+
ever matched that shape. But the runtime predicate is "no user", and the
|
|
834
|
+
commonest way to have no user is a **record-change flow fired by a write that
|
|
835
|
+
carried none**: `isSystem` does _not_ suppress trigger dispatch — only
|
|
836
|
+
`skipTriggers` does, and exactly three first-party paths set it — so every
|
|
837
|
+
plugin/service system write, the approvals status mirror, and a `runAs:'system'`
|
|
838
|
+
flow's own data node dispatched record-change flows with `userId: undefined`.
|
|
839
|
+
Ordinary users reach those writes routinely (submitting for approval mirrors a
|
|
840
|
+
status onto the target record), so the fail-open was reachable by unprivileged
|
|
841
|
+
input and was the common case, not the rare one.
|
|
842
|
+
|
|
843
|
+
Deliberately **not** implemented as "inherit the triggering write's posture and
|
|
844
|
+
run as `isSystem`". That reads like a relabel but is a privilege escalation: the
|
|
845
|
+
security middleware's `isSystem` short-circuit fires _before_ its
|
|
846
|
+
package-managed-row, system-row, audience-anchor and delegated-admin gates, all
|
|
847
|
+
of which a principal-less context still has to clear. Such a run cannot write
|
|
848
|
+
`sys_user_position` today; as `isSystem` it could. "Unscoped" was never
|
|
849
|
+
equivalent to "system".
|
|
850
|
+
|
|
851
|
+
**Breaking — how to migrate.** A flow that reacts to system writes and needs to
|
|
852
|
+
act beyond one user's grants declares `runAs: 'system'`, making the elevation
|
|
853
|
+
explicit and audit-attributable. Otherwise ensure the trigger supplies a user.
|
|
854
|
+
Flows that touch no data are unaffected (`runAs` is moot), and the failure is
|
|
855
|
+
isolated: the trigger already swallows flow errors, so the originating write
|
|
856
|
+
still succeeds. The engine warns at run _setup_, before any node executes.
|
|
857
|
+
|
|
858
|
+
**#3712's user-less provenance path is subsumed, not broken.** That fix let a
|
|
859
|
+
run with no trigger user write its own approval-locked record by carrying a
|
|
860
|
+
provenance-only ObjectQL context (the run id, nothing else). Such a run can no
|
|
861
|
+
longer perform a data operation at all — presenting no principal is exactly what
|
|
862
|
+
made the write unscoped — so it is refused before the lock is consulted. The
|
|
863
|
+
capability survives via the explicit route: a schedule that must write records
|
|
864
|
+
declares `runAs:'system'`, which the lock hook exempts on its own `isSystem`
|
|
865
|
+
branch. The `flowRunId` exemption itself stays live and load-bearing for what
|
|
866
|
+
#3703 built it for — a `runAs:'user'` run that _does_ have a user — where the
|
|
867
|
+
exemption is still provenance rather than privilege.
|
|
868
|
+
|
|
869
|
+
Also in this change:
|
|
870
|
+
|
|
871
|
+
- **`flow-schedule-runas-unscoped` → `flow-runas-unscoped`, and it now fails the
|
|
872
|
+
build.** It read as a gate and behaved as a comment — `os compile` documented
|
|
873
|
+
that the flow lint "NEVER fails the build" — which is close to no net at all
|
|
874
|
+
for the audience it protects, very often an AI generating flows in bulk. It now
|
|
875
|
+
also covers the other provably user-less triggers (`time_relative`, `api`), per
|
|
876
|
+
ADR-0073 D5. It still cannot cover `record_change`, which is undecidable at
|
|
877
|
+
authoring time — that is exactly why the runtime refusal exists.
|
|
878
|
+
- **Three seed writes stopped firing automation.** The seed loader's pass-2
|
|
879
|
+
deferred-reference back-fill and both of `AppPlugin`'s basic-insert fallbacks
|
|
880
|
+
inlined a bare `{ isSystem: true }` instead of the shared seed options, so they
|
|
881
|
+
seeded with record-change automation live — the self-trigger vector
|
|
882
|
+
`skipTriggers` exists to prevent, on the writes that skipped it.
|
|
883
|
+
- **ADR-0073 amended.** Its severity rationale ("an unprivileged user cannot
|
|
884
|
+
trigger a schedule, so there is no untrusted-input path") is falsified, and its
|
|
885
|
+
rejection of fail-closed ("breaks legitimate scheduled CRUD — 2/3 example flows
|
|
886
|
+
relied on the default") expired when those flows were fixed to declare
|
|
887
|
+
`runAs:'system'`. Refusal is an interim posture, forward-compatible with the
|
|
888
|
+
ADR's `automation` principal: when that lands, the refusal point becomes the
|
|
889
|
+
place that resolves it.
|
|
890
|
+
|
|
891
|
+
- Updated dependencies [50616d9]
|
|
892
|
+
- Updated dependencies [08b5a3d]
|
|
893
|
+
- Updated dependencies [d99aeb3]
|
|
894
|
+
- Updated dependencies [4727eb8]
|
|
895
|
+
- Updated dependencies [f63cd09]
|
|
896
|
+
- Updated dependencies [fa3d0cf]
|
|
897
|
+
- Updated dependencies [af5a224]
|
|
898
|
+
- Updated dependencies [71f76e1]
|
|
899
|
+
- Updated dependencies [37b1346]
|
|
900
|
+
- Updated dependencies [99736a0]
|
|
901
|
+
- Updated dependencies [fe67e34]
|
|
902
|
+
- Updated dependencies [fdb4f50]
|
|
903
|
+
- Updated dependencies [1bd5652]
|
|
904
|
+
- Updated dependencies [14252d3]
|
|
905
|
+
- Updated dependencies [7fb436c]
|
|
906
|
+
- Updated dependencies [879ea13]
|
|
907
|
+
- Updated dependencies [201b31f]
|
|
908
|
+
- Updated dependencies [e2616e0]
|
|
909
|
+
- Updated dependencies [6fdc5c6]
|
|
910
|
+
- Updated dependencies [8b9d71e]
|
|
911
|
+
- Updated dependencies [33f5e23]
|
|
912
|
+
- Updated dependencies [259af21]
|
|
913
|
+
- Updated dependencies [587fc91]
|
|
914
|
+
- Updated dependencies [1986594]
|
|
915
|
+
- Updated dependencies [ad4af62]
|
|
916
|
+
- Updated dependencies [d44dbfa]
|
|
917
|
+
- Updated dependencies [474fe39]
|
|
918
|
+
- Updated dependencies [0bc685a]
|
|
919
|
+
- Updated dependencies [b949059]
|
|
920
|
+
- Updated dependencies [be1c52c]
|
|
921
|
+
- Updated dependencies [c5ff96d]
|
|
922
|
+
- Updated dependencies [84e7be9]
|
|
923
|
+
- Updated dependencies [a6c3f38]
|
|
924
|
+
- Updated dependencies [debc23a]
|
|
925
|
+
- Updated dependencies [0f8ad09]
|
|
926
|
+
- Updated dependencies [8f9689f]
|
|
927
|
+
- Updated dependencies [57a3bb3]
|
|
928
|
+
- Updated dependencies [5f9a987]
|
|
929
|
+
- Updated dependencies [9f060e5]
|
|
930
|
+
- Updated dependencies [bc17d39]
|
|
931
|
+
- Updated dependencies [db02d47]
|
|
932
|
+
- Updated dependencies [0bfdf46]
|
|
933
|
+
- Updated dependencies [376a061]
|
|
934
|
+
- Updated dependencies [7c7e246]
|
|
935
|
+
- Updated dependencies [f35cdc5]
|
|
936
|
+
- Updated dependencies [9ea2bc5]
|
|
937
|
+
- Updated dependencies [c2d9098]
|
|
938
|
+
- Updated dependencies [a227ed7]
|
|
939
|
+
- Updated dependencies [9613396]
|
|
940
|
+
- Updated dependencies [e47b342]
|
|
941
|
+
- Updated dependencies [4ed7ed4]
|
|
942
|
+
- Updated dependencies [2fa4ca1]
|
|
943
|
+
- Updated dependencies [f5a2320]
|
|
944
|
+
- Updated dependencies [deb538f]
|
|
945
|
+
- Updated dependencies [5b89711]
|
|
946
|
+
- Updated dependencies [0c8a22f]
|
|
947
|
+
- Updated dependencies [763931e]
|
|
948
|
+
- Updated dependencies [de9af8a]
|
|
949
|
+
- Updated dependencies [c4df271]
|
|
950
|
+
- Updated dependencies [a41ba5c]
|
|
951
|
+
- Updated dependencies [189854c]
|
|
952
|
+
- Updated dependencies [0e3a226]
|
|
953
|
+
- Updated dependencies [524151c]
|
|
954
|
+
- Updated dependencies [1d4756e]
|
|
955
|
+
- Updated dependencies [720c5ad]
|
|
956
|
+
- Updated dependencies [a8d1e24]
|
|
957
|
+
- Updated dependencies [d1cabaa]
|
|
958
|
+
- Updated dependencies [41642b0]
|
|
959
|
+
- Updated dependencies [4cca74c]
|
|
960
|
+
- Updated dependencies [88ef03e]
|
|
961
|
+
- Updated dependencies [9e2caf3]
|
|
962
|
+
- Updated dependencies [81ce41a]
|
|
963
|
+
- Updated dependencies [85e1e4e]
|
|
964
|
+
- Updated dependencies [dac6a08]
|
|
965
|
+
- Updated dependencies [394b7a1]
|
|
966
|
+
- Updated dependencies [677b591]
|
|
967
|
+
- Updated dependencies [d77d1b7]
|
|
968
|
+
- Updated dependencies [5b79a34]
|
|
969
|
+
- Updated dependencies [c757854]
|
|
970
|
+
- Updated dependencies [0045682]
|
|
971
|
+
- Updated dependencies [2a5f04a]
|
|
972
|
+
- Updated dependencies [4f740b0]
|
|
973
|
+
- Updated dependencies [67452d1]
|
|
974
|
+
- Updated dependencies [4921a95]
|
|
975
|
+
- Updated dependencies [0fc6219]
|
|
976
|
+
- Updated dependencies [605e190]
|
|
977
|
+
- Updated dependencies [c6c59f1]
|
|
978
|
+
- Updated dependencies [b0e78a8]
|
|
979
|
+
- Updated dependencies [f31cc8d]
|
|
980
|
+
- Updated dependencies [f343dc4]
|
|
981
|
+
- Updated dependencies [8269e32]
|
|
982
|
+
- Updated dependencies [74f7339]
|
|
983
|
+
- Updated dependencies [a6c35a2]
|
|
984
|
+
- Updated dependencies [c2f1002]
|
|
985
|
+
- Updated dependencies [db48ad5]
|
|
986
|
+
- Updated dependencies [f163028]
|
|
987
|
+
- Updated dependencies [f07808c]
|
|
988
|
+
- Updated dependencies [7ffc3d3]
|
|
989
|
+
- Updated dependencies [88346ba]
|
|
990
|
+
- Updated dependencies [4631592]
|
|
991
|
+
- Updated dependencies [32ff033]
|
|
992
|
+
- Updated dependencies [5ac93d4]
|
|
993
|
+
- Updated dependencies [93f267f]
|
|
994
|
+
- Updated dependencies [0024abf]
|
|
995
|
+
- Updated dependencies [acbf364]
|
|
996
|
+
- Updated dependencies [5487c20]
|
|
997
|
+
- Updated dependencies [aa8b847]
|
|
998
|
+
- Updated dependencies [7687f7b]
|
|
999
|
+
- Updated dependencies [1659072]
|
|
1000
|
+
- Updated dependencies [abceb0d]
|
|
1001
|
+
- Updated dependencies [0c302a7]
|
|
1002
|
+
- Updated dependencies [6633337]
|
|
1003
|
+
- Updated dependencies [f00d8d4]
|
|
1004
|
+
- Updated dependencies [503be86]
|
|
1005
|
+
- Updated dependencies [cde1975]
|
|
1006
|
+
- Updated dependencies [0bc685a]
|
|
1007
|
+
- Updated dependencies [c073b8c]
|
|
1008
|
+
- Updated dependencies [11949fc]
|
|
1009
|
+
- Updated dependencies [b098b0e]
|
|
1010
|
+
- Updated dependencies [4d00b13]
|
|
1011
|
+
- Updated dependencies [9aa5510]
|
|
1012
|
+
- Updated dependencies [57bab76]
|
|
1013
|
+
- Updated dependencies [b90086a]
|
|
1014
|
+
- Updated dependencies [b95577a]
|
|
1015
|
+
- Updated dependencies [83c161f]
|
|
1016
|
+
- Updated dependencies [d8c4957]
|
|
1017
|
+
- Updated dependencies [f24cb83]
|
|
1018
|
+
- Updated dependencies [5dbbb92]
|
|
1019
|
+
- Updated dependencies [69f1dfd]
|
|
1020
|
+
- @objectstack/spec@17.0.0-rc.0
|
|
1021
|
+
- @objectstack/platform-objects@17.0.0-rc.0
|
|
1022
|
+
- @objectstack/core@17.0.0-rc.0
|
|
1023
|
+
- @objectstack/formula@17.0.0-rc.0
|
|
1024
|
+
- @objectstack/metadata-core@17.0.0-rc.0
|
|
1025
|
+
|
|
3
1026
|
## 16.1.0
|
|
4
1027
|
|
|
5
1028
|
### Patch Changes
|