@objectstack/core 17.0.0-rc.6 → 17.1.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 +3361 -0
- package/dist/index.cjs +521 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +584 -15
- package/dist/index.d.ts +584 -15
- package/dist/index.js +492 -45
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,3366 @@
|
|
|
1
1
|
# @objectstack/core
|
|
2
2
|
|
|
3
|
+
## 17.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2782805: feat(security): the REST 401 anonymous-deny body carries `code: "UNAUTHENTICATED"` alongside the existing `error` / `message` keys (#9487)
|
|
8
|
+
|
|
9
|
+
Every other REST error family answers `{ error, code }`, with the machine code
|
|
10
|
+
in `code` — the 401 family was the one outlier, answering
|
|
11
|
+
`{ error: "UNAUTHENTICATED", message }` with no `code` key at all. A client
|
|
12
|
+
keying on `body.code` (the shape the other families teach, and the first read
|
|
13
|
+
of `@objectstack/client`'s `err.code`) read `undefined` for every
|
|
14
|
+
authentication failure.
|
|
15
|
+
|
|
16
|
+
`ANONYMOUS_DENY_BODY` now carries `code: "UNAUTHENTICATED"` as well.
|
|
17
|
+
**Additive only** (maintainer-ruled): no key is removed or moved — `error`
|
|
18
|
+
keeps holding the same code value it always has, so every existing reader
|
|
19
|
+
keeps working. The wire effect surfaces through `@objectstack/rest`'s
|
|
20
|
+
`enforceAuth`, which writes this constant verbatim on every `/data`, `/meta`
|
|
21
|
+
and `/reports` 401. This does not settle ADR-0112 D5 (flat vs nested envelope
|
|
22
|
+
convergence); both declared envelope families are unchanged in kind.
|
|
23
|
+
- e43d63a: feat(identity): API keys are minted against the minter's active organization, and carry it into the request (#8287)
|
|
24
|
+
|
|
25
|
+
<!-- adr-0087: not-required (no-migration-prescription) One additive column on
|
|
26
|
+
an `isSystem` object declaring `protection: { lock: 'full' }`, which tenants
|
|
27
|
+
cannot author, so there is no consumer metadata to migrate and nothing
|
|
28
|
+
authorable is renamed, retired or tombstoned — no conversion to register. The
|
|
29
|
+
behavioural change is that a minted key now carries an organization, that a key
|
|
30
|
+
which cannot carry one is refused under the posture where it could never read
|
|
31
|
+
anything, and that an ex-member's key stops authenticating. -->
|
|
32
|
+
|
|
33
|
+
On a deployment running `OS_TENANCY_POSTURE=isolated`, a minted API key could
|
|
34
|
+
read **nothing at all**. `sys_api_key` carried no organization column, so key
|
|
35
|
+
authentication established a user but no active organization — and the
|
|
36
|
+
`isolated` Layer 0 wall is `organization_id = activeOrganizationId`, which with
|
|
37
|
+
no active organization matches no row. Every organization-scoped read answered
|
|
38
|
+
`200` with `total 0` while the console went on offering minting, so a tenant
|
|
39
|
+
admin could mint a valid-looking secret and discover only at call time that it
|
|
40
|
+
read nothing. (There was no cross-tenant leak — the failure was in the other
|
|
41
|
+
direction.)
|
|
42
|
+
|
|
43
|
+
**The column was absent by an inherited rule, not by oversight.**
|
|
44
|
+
`resolveInjectedSystemColumns` injects `organization_id` into every registered
|
|
45
|
+
object *except* `managedBy: 'better-auth'` ones, and `sys_api_key` carries that
|
|
46
|
+
flag — even though better-auth's `apiKey` plugin is not loaded and the table is
|
|
47
|
+
hand-rolled ObjectStack. So the fix needs the declaration *and* the ADR-0105 D7
|
|
48
|
+
extension-field registration to stay consistent. The read side, by contrast,
|
|
49
|
+
was **already wired**: `resolveApiKeyPrincipal` already read an organization
|
|
50
|
+
into `tenantId` and `resolveAuthzContext` already adopted it — it was reading a
|
|
51
|
+
column no mint path ever wrote.
|
|
52
|
+
|
|
53
|
+
**What changes**
|
|
54
|
+
|
|
55
|
+
- `sys_api_key` declares `active_organization_id` (+ index, and the column is
|
|
56
|
+
shown in the "My Keys" and "All" list views, because the card's complaint was
|
|
57
|
+
a credential whose reach its owner could not see).
|
|
58
|
+
- `POST /api/v1/keys` **inherits** the caller's active organization — there is
|
|
59
|
+
deliberately no org parameter and no cross-org key — and **re-checks the
|
|
60
|
+
caller's `sys_member` membership at mint time**, honouring ADR-0091 validity
|
|
61
|
+
windows. Under a walled posture it refuses (400) rather than minting a key
|
|
62
|
+
with no organization, and refuses (403) for an organization the caller is not
|
|
63
|
+
a member of. The mint response echoes the organization the key is pinned to.
|
|
64
|
+
- The verifier reads **one spelling** (Prime Directive #12): the
|
|
65
|
+
`row.organization_id ?? row.organizationId` chain it used to carry was a
|
|
66
|
+
consumer-side tolerance for a producer that did not exist.
|
|
67
|
+
- An **ex-member's key fails closed at verify time** — no principal, not a
|
|
68
|
+
degrade to a user-only principal, which would resurrect the same
|
|
69
|
+
`200 + total 0` silent-empty. Checked at verify rather than by revoking on
|
|
70
|
+
membership loss, because membership ends through many paths (better-auth org
|
|
71
|
+
endpoints, SCIM, a direct `sys_member` delete, a lapsing validity window) and
|
|
72
|
+
a hook must catch every one or it silently misses. It costs **zero extra
|
|
73
|
+
queries**: the resolver has already read `sys_member` for this user.
|
|
74
|
+
- **Pre-existing org-less keys are never backfilled** — that would silently
|
|
75
|
+
upgrade credentials minted under a different promise. They keep working under
|
|
76
|
+
`single` (no wall) and under `group` (whose wall derives from the owner's
|
|
77
|
+
memberships independently of the active organization, so they already work
|
|
78
|
+
there), and are **refused under `isolated`**, where they are provably dead
|
|
79
|
+
today.
|
|
80
|
+
|
|
81
|
+
**The column is deliberately named `active_organization_id`, not
|
|
82
|
+
`organization_id`** — the `sys_session` spelling, for the same concept: the
|
|
83
|
+
organization a credential makes *active*. `objectHasOrgIdField` tests for the
|
|
84
|
+
literal `organization_id`, and Layer 0 exempts objects without it, so the other
|
|
85
|
+
name would have made `sys_api_key` itself org-walled. Both walled postures
|
|
86
|
+
exclude NULL, so every pre-existing org-less row would have vanished from its
|
|
87
|
+
**own owner's** "My Keys" list while, under `group`, continuing to
|
|
88
|
+
authenticate — a live credential nobody could see or revoke, which is a fresh
|
|
89
|
+
instance of the very class this change removes.
|
|
90
|
+
- a38408a: fix(core): both kernels agree that a duplicate plugin registration OVERWRITES, and say so out loud (#9864)
|
|
91
|
+
|
|
92
|
+
Registering two plugins under the same `name` used to mean two different things
|
|
93
|
+
depending on which kernel was running:
|
|
94
|
+
|
|
95
|
+
| kernel | behaviour before |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `ObjectKernel` (what `os serve` runs) | accepted and overwrote, with **no check and no distinguishing log line** — `Plugin registered: <name>@<version>` printed twice, reading as two plugins running |
|
|
98
|
+
| `LiteKernel` (tests, serverless, edge) | threw `[Kernel] Plugin '<name>' already registered` |
|
|
99
|
+
|
|
100
|
+
Under the maintainer's ruling (2026-08-19, option B) both kernels now apply one
|
|
101
|
+
declared contract: **duplicate registration by `name` overwrites — last-one-wins
|
|
102
|
+
— and emits a `warn` naming the plugin and both versions.**
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
WARN Plugin superseded: 'com.objectstack.audit' — the later registration (v2.0.0)
|
|
106
|
+
REPLACED the earlier one (v1.0.0). Only the later instance is initialized and
|
|
107
|
+
started; the earlier one is discarded without ever running init(). Duplicate
|
|
108
|
+
registration by name is last-one-wins on both kernels by declared contract
|
|
109
|
+
(#9864) — register the plugin once if that is not what you meant.
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**This declares and warns about behaviour that already shipped; it does not fix a
|
|
113
|
+
user-visible bug.** The overwrite is load-bearing today — it is exactly what lets
|
|
114
|
+
a stack's own `plugins` entry supersede a plugin the CLI auto-registered earlier
|
|
115
|
+
in the same boot (`AuditPlugin`, #9863) — and every boot path that worked before
|
|
116
|
+
works the same way now. What changes is that the behaviour is declared, audible,
|
|
117
|
+
and pinned against **both** kernels
|
|
118
|
+
(`packages/core/src/plugin-registration.contract.test.ts`) rather than being an
|
|
119
|
+
accident of whichever kernel a reader happened to open. This was the fourth
|
|
120
|
+
measured instance of one contract implemented twice across the two kernels
|
|
121
|
+
(#5170, #5282, #8357 adjacent).
|
|
122
|
+
|
|
123
|
+
**What this changes for a caller**
|
|
124
|
+
|
|
125
|
+
- `LiteKernel.use()` no longer throws on a duplicate name. FROM: catch
|
|
126
|
+
`[Kernel] Plugin '<name>' already registered` to detect a double registration.
|
|
127
|
+
TO: there is no throw to catch — a duplicate is a `warn` and the later instance
|
|
128
|
+
wins. Code that registered a plugin twice and relied on the refusal should
|
|
129
|
+
register it once instead.
|
|
130
|
+
- `ObjectKernel` emits one `warn` where it previously emitted nothing, and
|
|
131
|
+
**suppresses** its `Plugin registered:` line for the superseding registration,
|
|
132
|
+
so the count of those lines equals the number of plugins that actually boot.
|
|
133
|
+
- The level is part of the contract: `warn`, never `info`. The CLI's default
|
|
134
|
+
kernel level is `warn`, and its boot-quiet window replays `warn` while
|
|
135
|
+
discarding in-window `info` — an `info` notice would be invisible on exactly
|
|
136
|
+
the boot path where this was measured.
|
|
137
|
+
|
|
138
|
+
**Measured, not assumed:** the displaced instance holds nothing that needs
|
|
139
|
+
teardown. Registration is legal only while the kernel is `idle`, so a supersede
|
|
140
|
+
can only ever displace a plugin that has never been initialized; `init()`,
|
|
141
|
+
`start()` and `destroy()` all run later, over a registry the displaced entry has
|
|
142
|
+
already left. `PluginLoader.loadPlugin()` — which `ObjectKernel` runs first — is
|
|
143
|
+
pure validation plus a name-keyed map write of its own, and invokes nothing on
|
|
144
|
+
the plugin. Calling `destroy()` on the displaced instance would be the bug, not
|
|
145
|
+
the fix: it is the paired teardown for an `init()` that never ran.
|
|
146
|
+
- 5f5e234: fix(security): `sys_permission_set.active` and `sys_position.active` now actually stop granting access (#8613)
|
|
147
|
+
|
|
148
|
+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is
|
|
149
|
+
added, renamed or retired. `active` is a ROW property of a `sys_permission_set`
|
|
150
|
+
/ `sys_position` record, not a key on `PermissionSetSchema` (which is a strict
|
|
151
|
+
object in `packages/spec` and deliberately declares no such key — see
|
|
152
|
+
`permission-set-projection.ts`'s ROW_STATE_COLUMNS). No spec schema, export or
|
|
153
|
+
stored metadata shape changes, so there is no conversion to register and no
|
|
154
|
+
tombstone to write. The change is a runtime predicate at the authorization
|
|
155
|
+
resolution seam; the remedy for an affected deployment is operational
|
|
156
|
+
(re-activate rows that were switched off), not a metadata migration. -->
|
|
157
|
+
|
|
158
|
+
**BREAKING for deployments that already switched a permission set or position
|
|
159
|
+
off.** Both objects ship a Deactivate action whose confirmation dialog promises,
|
|
160
|
+
in all four locales, that access stops:
|
|
161
|
+
|
|
162
|
+
> Deactivate this permission set? Existing assignments stay in place but stop
|
|
163
|
+
> granting access until re-activated.
|
|
164
|
+
> Deactivate this position? Users keep their assignment but the position stops
|
|
165
|
+
> granting permissions until re-activated.
|
|
166
|
+
|
|
167
|
+
Nothing read the column. Measured on the real resolver: a position seeded
|
|
168
|
+
`active: false` still granted its permission sets, and a permission set seeded
|
|
169
|
+
`active: false` still returned `posture: PLATFORM_ADMIN` with its system
|
|
170
|
+
permissions. Deactivation moved a badge in Setup and nothing else — while the
|
|
171
|
+
admin who had just revoked a compromised or over-broad grant was told the
|
|
172
|
+
opposite, and whose likely next step was therefore *not* the action that would
|
|
173
|
+
have worked (delete the set, or remove the assignments).
|
|
174
|
+
|
|
175
|
+
**What changes at runtime.** `resolveAuthzContext` / `resolveUserAuthzGrants`
|
|
176
|
+
(`@objectstack/core`) — the single seam every transport resolves authorization
|
|
177
|
+
through — now drop a deactivated row **before** any derivation:
|
|
178
|
+
|
|
179
|
+
- a deactivated `sys_position` no longer contributes its
|
|
180
|
+
`sys_position_permission_set` grants, and its name leaves `positions` (so the
|
|
181
|
+
name-reuse path cannot resolve the same grant one layer down);
|
|
182
|
+
- a deactivated `sys_permission_set` contributes no name, no
|
|
183
|
+
`system_permissions`, no `tab_permissions`, **and no `PLATFORM_ADMIN`
|
|
184
|
+
posture** — the flag is applied before the posture is derived, not after;
|
|
185
|
+
- the `plugin-security` DB loader applies the same predicate, which is what
|
|
186
|
+
judges a set reached by NAME through an active position of the same name.
|
|
187
|
+
|
|
188
|
+
Both tables were already read at that seam, so this costs **zero new hot-path
|
|
189
|
+
queries**.
|
|
190
|
+
|
|
191
|
+
**⚠️ Read this before upgrading.** Any `sys_permission_set` or `sys_position`
|
|
192
|
+
row currently carrying `active: false` **stops granting the moment this
|
|
193
|
+
lands** — on live data, with no migration step to notice. That is the correct
|
|
194
|
+
direction (it is what the dialog said when someone clicked Deactivate), but on
|
|
195
|
+
an installation that used the switch believing it was inert it is a real
|
|
196
|
+
revocation. Before upgrading, list the deactivated rows and re-activate any that
|
|
197
|
+
are still meant to grant:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
GET /api/v1/data/sys_permission_set?filters=[["active","=",false]]
|
|
201
|
+
GET /api/v1/data/sys_position?filters=[["active","=",false]]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
A row whose `active` column is **absent or NULL** is unaffected: the predicate
|
|
205
|
+
is "explicitly deactivated", never "explicitly active", so rows that predate the
|
|
206
|
+
column keep granting exactly as before.
|
|
207
|
+
|
|
208
|
+
**Break-glass, closed in the same change** (`@objectstack/plugin-auth`).
|
|
209
|
+
Enforcing the flag opened a one-click, installation-wide lockout: deactivating
|
|
210
|
+
`admin_full_access` un-makes every platform admin at once, through a payload
|
|
211
|
+
that touches neither `name` nor any identity table, and re-activating requires
|
|
212
|
+
the permission the click just took away (the seeders deliberately never
|
|
213
|
+
reconcile `active`, so no restart restores it). The last-administrator guard now
|
|
214
|
+
judges that write like the delete and rename spellings it already refused, and
|
|
215
|
+
an environment whose break-glass set is *already* off is read as emptied rather
|
|
216
|
+
than as a bootstrap window — so it does not silently disarm the guard for every
|
|
217
|
+
other identity write. Re-activation itself stays permitted, or the refusal would
|
|
218
|
+
have no way out from inside the product.
|
|
219
|
+
- f8eb736: feat(security): bind the break-glass standing-key lists to what the authz resolver actually reads — the correspondence stops being prose (#8734)
|
|
220
|
+
|
|
221
|
+
`plugin-auth`'s last-administrator guard (ADR-0024 D5.2) decides whether a
|
|
222
|
+
pending write can empty the administrator population by testing the payload
|
|
223
|
+
against three standing-key lists (`MEMBER_STANDING_KEYS`,
|
|
224
|
+
`GRANT_STANDING_KEYS`, `PERMISSION_SET_STANDING_KEYS`). A payload touching none
|
|
225
|
+
of them is skipped without any reads — so a column `resolveAuthzContext` starts
|
|
226
|
+
reading that a list omits is a write class the guard **silently stops judging**,
|
|
227
|
+
on the one path whose failure mode is an installation-wide administrator lockout
|
|
228
|
+
with no in-product recovery.
|
|
229
|
+
|
|
230
|
+
Nothing bound the two together. The correspondence lived in a comment, and it
|
|
231
|
+
had already gone false once: #6084 wrote — naming `active` explicitly — that
|
|
232
|
+
everything a permission-set write touches other than `name` is invisible to "who
|
|
233
|
+
is an administrator". That was true when written; #8613 made `active` a
|
|
234
|
+
resolution-time predicate and the sentence became false. Nothing mechanical
|
|
235
|
+
would have caught it, because the guard's own tests stay green precisely when
|
|
236
|
+
the guard is never consulted.
|
|
237
|
+
|
|
238
|
+
**The mechanism is two links, and the first one is a measurement.**
|
|
239
|
+
|
|
240
|
+
- `@objectstack/core` now exports `ADMIN_STANDING_SURFACE` — declared beside the
|
|
241
|
+
resolver, listing every table the administrator-derivation path reads, each
|
|
242
|
+
classified `derives` or `reads-only` with its reason, and for the deriving
|
|
243
|
+
tables every column read. It is asserted **equal** to what the real
|
|
244
|
+
`resolveAuthzContext` reads, observed at runtime through a recording engine
|
|
245
|
+
that records every property access and every `where` key per table. Observation
|
|
246
|
+
rather than source extraction because the reads that matter have moved into
|
|
247
|
+
helpers: `active` is read by `isRowActive(row)` and the ADR-0091 window bounds
|
|
248
|
+
by `isGrantActive(row, now)`, neither named at the resolver's own call site —
|
|
249
|
+
the exact shape #8613 had.
|
|
250
|
+
|
|
251
|
+
- `@objectstack/plugin-auth` now exports its standing-key lists plus
|
|
252
|
+
`STANDING_KEYS_BY_TABLE` and `STANDING_KEY_EXCLUSIONS`, and a gate requires
|
|
253
|
+
every column of that measured surface to have an answer: it is standing-bearing
|
|
254
|
+
(in a list) or it is excluded with the reason it cannot empty the administrator
|
|
255
|
+
population. There is no third state — the third state is what `active` was
|
|
256
|
+
between #6084 and #8613.
|
|
257
|
+
|
|
258
|
+
So a resolver change that starts reading a new column fails at the first link
|
|
259
|
+
until the declaration is updated, and at the second until the guard has an
|
|
260
|
+
explicit answer for it. Landing #8613 green would have required writing down that
|
|
261
|
+
deactivating `admin_full_access` cannot empty the administrator population —
|
|
262
|
+
which is false, and which is what the old comment asserted by accident.
|
|
263
|
+
|
|
264
|
+
**No guard behaviour changes.** Every list keeps exactly the values it had; the
|
|
265
|
+
gate is one-directional by construction (it can only ever demand that the guard
|
|
266
|
+
judges *more*), because the other direction would put pressure on a break-glass
|
|
267
|
+
guard to fire less often.
|
|
268
|
+
|
|
269
|
+
The table-level half is covered too: a resolver that started deriving
|
|
270
|
+
administrator standing from a **new** table is invisible to any column-set
|
|
271
|
+
comparison, since the table is absent from both sides — so the surface enumerates
|
|
272
|
+
every table the path reads, and an unclassified one fails.
|
|
273
|
+
|
|
274
|
+
### Patch Changes
|
|
275
|
+
|
|
276
|
+
- 7ff3975: feat(spec): `IHttpServer` gains an optional `afterResponse` response-observing
|
|
277
|
+
hook so HTTP metrics are transport-agnostic instead of Hono-only (#9835)
|
|
278
|
+
|
|
279
|
+
The contract addition (additive — a new optional member plus the
|
|
280
|
+
`HttpResponseObservation` / `HttpResponseObserver` types and the reserved
|
|
281
|
+
`UNMATCHED_ROUTE_PATTERN` label): a transport invokes each registered observer
|
|
282
|
+
exactly once per answered request with `{ method, routePattern, status,
|
|
283
|
+
elapsedMs }`, after the response exists — the observation point the `use()`
|
|
284
|
+
middleware contract cannot express (it runs before dispatch and never sees a
|
|
285
|
+
status). `routePattern` is REQUIRED to be the registered route pattern
|
|
286
|
+
(`/api/v1/data/:id`), never the concrete path, so no adapter re-decides metric
|
|
287
|
+
cardinality. Optionality is feature-detected runtime-real
|
|
288
|
+
(`typeof server.afterResponse === 'function'`); a transport that does not
|
|
289
|
+
implement the seam reports **no** HTTP metrics — zero there means "not
|
|
290
|
+
instrumented", never "no traffic".
|
|
291
|
+
|
|
292
|
+
Implementations and consumers in the same change:
|
|
293
|
+
|
|
294
|
+
- `@objectstack/plugin-hono-server`: `HonoHttpServer` implements the seam (the
|
|
295
|
+
ruled #9650 raw-app middleware becomes its delivery path — same reach,
|
|
296
|
+
including `getRawApp()` mounts and middleware-refused 429s); unrouted
|
|
297
|
+
requests are now labelled with the reserved `unmatched` pattern (previously
|
|
298
|
+
they could surface as `/*`).
|
|
299
|
+
- `@objectstack/observability`: new `armHttpRequestCounter(server, metrics)`
|
|
300
|
+
arms the `http_requests_total` counter through the seam at most once per
|
|
301
|
+
server (first caller wins), which is what makes "exactly one counter per
|
|
302
|
+
server" structural.
|
|
303
|
+
- `@objectstack/runtime`: the dispatcher offers its `observability.metrics`
|
|
304
|
+
registry to the seam (a host that wires only the dispatcher now counts every
|
|
305
|
+
inbound surface) and suppresses its own per-route copy of
|
|
306
|
+
`http_requests_total` when the transport implements the seam — retiring the
|
|
307
|
+
#9833 double count. Request-id echo, the duration histogram, the error
|
|
308
|
+
counter and the error reporter are unchanged.
|
|
309
|
+
- `@objectstack/http-conformance`: `NodeHttpServer` implements the seam, and a
|
|
310
|
+
new cross-adapter conformance suite locks the semantics for both adapters.
|
|
311
|
+
- `@objectstack/core`: re-exports the new contract types/constant.
|
|
312
|
+
- 24173e9: fix(rest): read an offset-free import cell in the business timezone, not the host `TZ` (#8485)
|
|
313
|
+
|
|
314
|
+
`parseDateCell` ended in `new Date(s)`. A spreadsheet cell like
|
|
315
|
+
`2026-08-01 06:00:00` carries no offset, so ECMAScript resolves it against the
|
|
316
|
+
**process** timezone, and the instant bulk import stored became a property of
|
|
317
|
+
the deployment host:
|
|
318
|
+
|
|
319
|
+
```
|
|
320
|
+
TZ=Asia/Shanghai → 2026-07-31T22:00:00.000Z
|
|
321
|
+
TZ=UTC → 2026-08-01T06:00:00.000Z
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Same file, same tenant, same cell — eight hours apart, decided by a setting
|
|
325
|
+
nobody authoring the spreadsheet can see, and never consulting the business
|
|
326
|
+
timezone the route had already resolved one frame up
|
|
327
|
+
(`ExecutionContext.timezone`, the platform-default → global → tenant cascade).
|
|
328
|
+
|
|
329
|
+
Since the export renders `datetime` cells in that business timezone (#8373), the
|
|
330
|
+
advertised export → edit in a spreadsheet → re-import round trip was lossless
|
|
331
|
+
only where the host `TZ` happened to equal the business zone. `import-coerce.ts`
|
|
332
|
+
opens by calling itself "the inverse of `export-format.ts`"; it now is one, and
|
|
333
|
+
the regression proof asserts inverse-ness on the **pair** — every fixture under
|
|
334
|
+
a host `TZ` deliberately different from the business timezone, because a test
|
|
335
|
+
that runs only under a matching `TZ` cannot fail.
|
|
336
|
+
|
|
337
|
+
**An offset-free datetime cell is now read in the caller's business timezone**,
|
|
338
|
+
through `@objectstack/core`'s new `zonedWallClockToUtcMs` — the DST-safe wall
|
|
339
|
+
clock → instant primitive that `zonedDateStartToUtcMs` (the date-bucket drill
|
|
340
|
+
path) is now the midnight special case of. One implementation of zone
|
|
341
|
+
arithmetic, `Intl` offsets from the platform tz database, never hand-rolled;
|
|
342
|
+
generalising the existing one rather than hand-rolling a second in `rest` is
|
|
343
|
+
what keeps the export and import halves of this seam from drifting apart again.
|
|
344
|
+
Two wall clocks are not a bijection with instants, and both degenerate DST
|
|
345
|
+
readings resolve to the earlier candidate instant — a gap reading lands just
|
|
346
|
+
before the gap, an ambiguous reading on its first occurrence (pinned, measured).
|
|
347
|
+
|
|
348
|
+
Three things deliberately do **not** move:
|
|
349
|
+
|
|
350
|
+
- **A cell that carries an explicit offset** (`…Z`, `…+08:00`) already names one
|
|
351
|
+
instant and is honoured exactly as written. This change affects naive cells
|
|
352
|
+
only.
|
|
353
|
+
- **The date-only fast path stays UTC.** `YYYY-MM-DD` is UTC per ECMAScript and
|
|
354
|
+
a `date` is a timezone-naive calendar day (ADR-0053); sweeping it into the
|
|
355
|
+
zoned handling to make the code look uniform would silently re-time every
|
|
356
|
+
date-only import to fix nothing.
|
|
357
|
+
- **No timezone resolved ⇒ UTC**, never the process clock. That is the fallback
|
|
358
|
+
the export's cell path takes in the same case, so the round trip stays exact
|
|
359
|
+
for deployments that configure no zone — and a process-`TZ` fallback would
|
|
360
|
+
preserve the defect for exactly the deployments that cannot see it. This is
|
|
361
|
+
the one **behaviour change for existing deployments**: a host with a non-UTC
|
|
362
|
+
`TZ` and no resolved business timezone previously read naive cells in the host
|
|
363
|
+
clock and now reads them as UTC. An explicitly resolved `'UTC'` is a resolved
|
|
364
|
+
zone, not a missing one.
|
|
365
|
+
|
|
366
|
+
Two adjacent legs of the same defect, both on the naive-cell path:
|
|
367
|
+
|
|
368
|
+
- **A naive cell landing in a `date` or `time` field** now takes the typed
|
|
369
|
+
components verbatim (`2026-08-01 06:00:00` → `2026-08-01` / `06:00:00`).
|
|
370
|
+
Those branches also read the process clock, so a host east of the cell stored
|
|
371
|
+
the *previous calendar day* for a `date` column.
|
|
372
|
+
- **An xlsx date cell.** An Excel serial date carries no timezone; ExcelJS
|
|
373
|
+
materialises it as a `Date` whose UTC components are the sheet's wall clock,
|
|
374
|
+
and `import-prepare.ts` rendered it with `toISOString()` — stamping a `Z` the
|
|
375
|
+
file never had. That fabricated offset then outranked the business timezone by
|
|
376
|
+
the very carve-out above, so every real date cell in a user-authored workbook
|
|
377
|
+
imported as UTC whatever the tenant's zone. It now flattens to the same
|
|
378
|
+
offset-free `YYYY-MM-DD HH:mm:ss` a CSV export writes, which is what that
|
|
379
|
+
function's contract already claimed to produce.
|
|
380
|
+
- e1bb0ca: fix(qa): `HttpTestAdapter` resolves the Data Protocol mount from the server's `/discovery`, and falls back to the convention loudly (#7983)
|
|
381
|
+
|
|
382
|
+
The record-shaped `os test` action types (`create_record`, `read_record`,
|
|
383
|
+
`update_record`, `delete_record`, `query_records`) built their URLs from the
|
|
384
|
+
**defaults** of `RestApiConfigSchema.apiPath` and
|
|
385
|
+
`CrudEndpointsConfigSchema.dataPrefix`, because the adapter is handed an origin
|
|
386
|
+
and nothing else. A deployment that moved the mount got a 404 that reads like the
|
|
387
|
+
suite author's own URL mistake rather than a platform limitation.
|
|
388
|
+
|
|
389
|
+
The adapter now asks the server, following the `getRoute` precedent in
|
|
390
|
+
`@objectstack/client`: **one memoised `GET {apiBase}/discovery` per run** (`os
|
|
391
|
+
test` builds one adapter for the whole run), addressing whatever `routes.data`
|
|
392
|
+
advertises, with the schema-derived convention as the fallback. Measured on a
|
|
393
|
+
booted stack (REST route generator + dispatcher bridge), before and after:
|
|
394
|
+
|
|
395
|
+
| deployment | before | after |
|
|
396
|
+
|---|---|---|
|
|
397
|
+
| stock | created | created |
|
|
398
|
+
| `crud.dataPrefix: '/objects'` | `HTTP Error 404` | created |
|
|
399
|
+
| `api.apiPath: '/api/2026-01'` | `HTTP Error 404` | `HTTP Error 404`, now naming the mount |
|
|
400
|
+
|
|
401
|
+
The `apiPath` row is **not** closed, and the reason is structural: `apiPath`
|
|
402
|
+
moves the base that `/discovery` is itself mounted under, so the document that
|
|
403
|
+
would name the new mount sits behind the prefix that is missing. The one
|
|
404
|
+
discovery document at a fixed path does not rescue it — `/.well-known/objectstack`
|
|
405
|
+
advertises the **dispatcher's** `${prefix}/data`, measured as `/api/v1/data`
|
|
406
|
+
under all three configs above — so it is deliberately not probed: trusting it
|
|
407
|
+
would attach a false provenance ("discovery told us") to the same 404.
|
|
408
|
+
|
|
409
|
+
Instead that case degrades loudly. Falling back to the convention prints a
|
|
410
|
+
warning naming the mount it will address, the probe that failed and the remedy,
|
|
411
|
+
and every 404/405 from a record action now carries the mount it addressed and
|
|
412
|
+
where that mount came from. `api_call` is unchanged, issues no probe, and remains
|
|
413
|
+
the escape hatch for a host the probe cannot reach.
|
|
414
|
+
- 402c125: fix(objectql): a temporal filter comparand the platform cannot interpret is refused at the engine door instead of answering 200 with zero rows (#8690)
|
|
415
|
+
|
|
416
|
+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is
|
|
417
|
+
renamed, retired or tombstoned — no spec schema is touched at all. The change
|
|
418
|
+
is a new runtime refusal at the engine's filter collection point, plus the
|
|
419
|
+
routing decline that stops the raw-SQL analytics path bypassing it. -->
|
|
420
|
+
|
|
421
|
+
A `datetime` / `date` / `time` field filtered with a bare string the platform
|
|
422
|
+
cannot read — `last_30_days`, `not-a-date-at-all` — was bound **as written**
|
|
423
|
+
all the way to the driver, where the comparison is false for every row. The
|
|
424
|
+
caller received `HTTP 200`, an empty result set, and nothing to indicate the
|
|
425
|
+
filter was meaningless. An unknown `{placeholder}` in the same position was
|
|
426
|
+
already refused loudly (`FILTER_TOKEN_UNKNOWN` / 400, listing the resolvable
|
|
427
|
+
tokens), so one API answered two shapes of unusable comparand two different
|
|
428
|
+
ways.
|
|
429
|
+
|
|
430
|
+
It is concretely reachable rather than theoretical: `last_7_days` /
|
|
431
|
+
`last_30_days` / `last_90_days` are **declared preset names** in the dashboard
|
|
432
|
+
schema. The shipped console lowers them to `{N_days_ago}` macros before they
|
|
433
|
+
reach the API, so the console path was always safe — but a saved report, an
|
|
434
|
+
integration, an MCP client or an AI-authored query sends the preset name itself
|
|
435
|
+
and got a silent zero. An empty chart is the hardest failure to debug: it is
|
|
436
|
+
indistinguishable from "there is genuinely no data".
|
|
437
|
+
|
|
438
|
+
Such a comparand is now refused at the ObjectQL engine's single filter
|
|
439
|
+
collection point, with `code: 'INVALID_FILTER'` and `status: 400`, naming the
|
|
440
|
+
field, the value, the key path and the spellings that would work. That seam is
|
|
441
|
+
the one place holding the caller's comparand and the field's **declared type**
|
|
442
|
+
at the same moment, and every verb (`find` / `findOne` / `count` / `aggregate`
|
|
443
|
+
/ `update` / `delete`) and both filter spellings (the array sugar and the
|
|
444
|
+
lowered condition) pass through it, so all four backends inherit one answer
|
|
445
|
+
rather than four. `NativeSQLStrategy` additionally **declines** such a query so
|
|
446
|
+
the raw-SQL analytics path falls through to that door instead of binding the
|
|
447
|
+
value into its own statement.
|
|
448
|
+
|
|
449
|
+
Deliberately unchanged, each by ruling: a `{placeholder}` keeps its existing
|
|
450
|
+
refusal one layer down (the door runs before token resolution and steps around
|
|
451
|
+
them, so `{30_days_ago}` still resolves normally); non-string comparands are
|
|
452
|
+
untouched (a number is epoch milliseconds, a `Date` is an instant); and the
|
|
453
|
+
**empty string** keeps today's behaviour exactly — it binds as `''` and matches
|
|
454
|
+
every non-null row, which is a separate question that remains its own card.
|
|
455
|
+
- Updated dependencies [56656aa]
|
|
456
|
+
- Updated dependencies [07e630e]
|
|
457
|
+
- Updated dependencies [2f65b1b]
|
|
458
|
+
- Updated dependencies [720ee95]
|
|
459
|
+
- Updated dependencies [f287435]
|
|
460
|
+
- Updated dependencies [9aa8890]
|
|
461
|
+
- Updated dependencies [7c9c1dd]
|
|
462
|
+
- Updated dependencies [75b7c24]
|
|
463
|
+
- Updated dependencies [d5552ca]
|
|
464
|
+
- Updated dependencies [d9813a9]
|
|
465
|
+
- Updated dependencies [8640fb2]
|
|
466
|
+
- Updated dependencies [2420641]
|
|
467
|
+
- Updated dependencies [2ad91c3]
|
|
468
|
+
- Updated dependencies [f57fb38]
|
|
469
|
+
- Updated dependencies [00777a0]
|
|
470
|
+
- Updated dependencies [d491625]
|
|
471
|
+
- Updated dependencies [420804d]
|
|
472
|
+
- Updated dependencies [716ac9b]
|
|
473
|
+
- Updated dependencies [62b1427]
|
|
474
|
+
- Updated dependencies [7ea1372]
|
|
475
|
+
- Updated dependencies [23abe27]
|
|
476
|
+
- Updated dependencies [985a9cd]
|
|
477
|
+
- Updated dependencies [a8189ae]
|
|
478
|
+
- Updated dependencies [26e70fb]
|
|
479
|
+
- Updated dependencies [42b05af]
|
|
480
|
+
- Updated dependencies [2b292ce]
|
|
481
|
+
- Updated dependencies [abcf853]
|
|
482
|
+
- Updated dependencies [8b9eba5]
|
|
483
|
+
- Updated dependencies [d575779]
|
|
484
|
+
- Updated dependencies [94f7ef8]
|
|
485
|
+
- Updated dependencies [c5ac5e4]
|
|
486
|
+
- Updated dependencies [a777944]
|
|
487
|
+
- Updated dependencies [dd88e1c]
|
|
488
|
+
- Updated dependencies [856527c]
|
|
489
|
+
- Updated dependencies [870f710]
|
|
490
|
+
- Updated dependencies [79c46da]
|
|
491
|
+
- Updated dependencies [7ff3975]
|
|
492
|
+
- Updated dependencies [29d055b]
|
|
493
|
+
- Updated dependencies [65589d6]
|
|
494
|
+
- Updated dependencies [2c86fe3]
|
|
495
|
+
- Updated dependencies [e196c6a]
|
|
496
|
+
- Updated dependencies [4ab7523]
|
|
497
|
+
- Updated dependencies [19539b4]
|
|
498
|
+
- Updated dependencies [11b779e]
|
|
499
|
+
- Updated dependencies [739fe5b]
|
|
500
|
+
- Updated dependencies [4bfe1a5]
|
|
501
|
+
- Updated dependencies [2065e31]
|
|
502
|
+
- Updated dependencies [b69d0f5]
|
|
503
|
+
- Updated dependencies [4d47afe]
|
|
504
|
+
- Updated dependencies [e4e5c6e]
|
|
505
|
+
- Updated dependencies [9a56784]
|
|
506
|
+
- Updated dependencies [d00d2f6]
|
|
507
|
+
- Updated dependencies [df0c12d]
|
|
508
|
+
- Updated dependencies [d31785f]
|
|
509
|
+
- Updated dependencies [c308a4f]
|
|
510
|
+
- Updated dependencies [e2899f6]
|
|
511
|
+
- Updated dependencies [3851f87]
|
|
512
|
+
- Updated dependencies [2a29caa]
|
|
513
|
+
- Updated dependencies [09a6eee]
|
|
514
|
+
- Updated dependencies [1a7f907]
|
|
515
|
+
- Updated dependencies [cd455c8]
|
|
516
|
+
- Updated dependencies [30d3752]
|
|
517
|
+
- Updated dependencies [c80e7ae]
|
|
518
|
+
- Updated dependencies [09a9a8a]
|
|
519
|
+
- Updated dependencies [07026cf]
|
|
520
|
+
- Updated dependencies [5d4f3d5]
|
|
521
|
+
- Updated dependencies [4d80e8b]
|
|
522
|
+
- Updated dependencies [30b1c63]
|
|
523
|
+
- Updated dependencies [079b457]
|
|
524
|
+
- Updated dependencies [e43b211]
|
|
525
|
+
- Updated dependencies [890b38f]
|
|
526
|
+
- Updated dependencies [8bee54b]
|
|
527
|
+
- Updated dependencies [7a537ce]
|
|
528
|
+
- Updated dependencies [593c4bf]
|
|
529
|
+
- Updated dependencies [ff08691]
|
|
530
|
+
- Updated dependencies [60e0f90]
|
|
531
|
+
- Updated dependencies [90c5285]
|
|
532
|
+
- Updated dependencies [7901b2d]
|
|
533
|
+
- Updated dependencies [56bca91]
|
|
534
|
+
- Updated dependencies [79394d7]
|
|
535
|
+
- Updated dependencies [730fd9a]
|
|
536
|
+
- Updated dependencies [44bc51d]
|
|
537
|
+
- Updated dependencies [73cfddf]
|
|
538
|
+
- Updated dependencies [d634e66]
|
|
539
|
+
- @objectstack/spec@17.1.0
|
|
540
|
+
|
|
541
|
+
## 17.0.0
|
|
542
|
+
|
|
543
|
+
### Major Changes
|
|
544
|
+
|
|
545
|
+
- 29c6c9d: feat(spec,core,runtime)!: declarative `apis:` refuses loudly instead of parsing into silence; the `ApiRegistry` family retires (#4936, #4939)
|
|
546
|
+
|
|
547
|
+
The declarative API-endpoint surface was **zero-execution end to end**, and said nothing
|
|
548
|
+
about it. Metadata loading worked perfectly — a stack declared `apis:`, `defineStack`
|
|
549
|
+
accepted it, and `GET /api/v1/meta/api` returned every endpoint with every key intact.
|
|
550
|
+
The execution side never fired once. On a real boot (showcase, 47 plugins) both declared
|
|
551
|
+
paths answered a bare `404 {"error":"Not found"}` — not even the dispatcher's semantic
|
|
552
|
+
404, because **no route was ever mounted** for a declared path, so the request died at
|
|
553
|
+
Hono's `notFound`. Behind that, the dispatcher's `handleApiEndpoint` branch resolved the
|
|
554
|
+
metadata service and called `matchEndpoint` on it — a method **no implementation in the
|
|
555
|
+
repo has ever provided**. The branch returned "not handled" on every request ever served.
|
|
556
|
+
|
|
557
|
+
So every key on `ApiEndpointSchema` was declared ≠ enforced: `path`/`method` (never
|
|
558
|
+
mounted), `type`/`target`/`objectParams` (never executed), `cacheTtl`,
|
|
559
|
+
`inputMapping`/`outputMapping`, `rateLimit`, `summary`/`description` — and
|
|
560
|
+
**`authRequired`**, a security semantic that parsed green and gated nothing at all. That
|
|
561
|
+
is false compliance, the failure ADR-0049 exists to stop, not debt.
|
|
562
|
+
|
|
563
|
+
## BREAKING — a non-empty `apis:` is now rejected
|
|
564
|
+
|
|
565
|
+
Metadata that parsed cleanly before is now **refused at publish/validate**, with the
|
|
566
|
+
prescription in the rejection itself:
|
|
567
|
+
|
|
568
|
+
```
|
|
569
|
+
apis: `apis:` (declarative ApiEndpoint) is DECLARED BUT NOT EXECUTABLE in this runtime,
|
|
570
|
+
so a non-empty array is rejected instead of silently accepted (#4936). …
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
**FROM → TO.** `apis: [ …endpoints… ]` → `apis: []` (or delete the key; both are still
|
|
574
|
+
accepted, and an empty array is not a special case). To actually serve the route today,
|
|
575
|
+
mount it **in code** — a plugin manifest `contributes.routes` entry, or an `http.server`
|
|
576
|
+
route. That is now the only honest path, and the one `examples/app-showcase` uses
|
|
577
|
+
(`src/system/server/recalc-endpoint.ts`).
|
|
578
|
+
|
|
579
|
+
The refusal lives on `ObjectStackDefinitionSchema` itself, which is the single choke
|
|
580
|
+
point every path runs through — `defineStack`, the metadata plugin's artifact ingestion,
|
|
581
|
+
`os validate`, the lint scorer and `EnvironmentArtifactSchema`. There is no path that
|
|
582
|
+
forgot to check.
|
|
583
|
+
|
|
584
|
+
**The `ApiEndpoint` vocabulary is deliberately KEPT.** Retiring it was considered and
|
|
585
|
+
rejected: endpoint shapes are an industry-stable form, so a retirement would only mean
|
|
586
|
+
re-introducing the identical schema later. Your endpoint definitions stay valid TypeScript
|
|
587
|
+
and stay in the spec; only _authoring them into a stack_ is refused, and only until the
|
|
588
|
+
executor lands. Keep them commented next to your stack — that is what the showcase does.
|
|
589
|
+
The executor (route mounting + endpoint matching + per-key wiring for
|
|
590
|
+
`authRequired`/`cacheTtl`/`inputMapping`/`outputMapping`/`rateLimit`) is tracked by
|
|
591
|
+
**#5040**, which replaces this rejection with real execution.
|
|
592
|
+
|
|
593
|
+
## BREAKING — the `ApiRegistry` / `ApiEndpointRegistration` family is removed (#4939)
|
|
594
|
+
|
|
595
|
+
The repo carried a **second**, unrelated declaration shape for "an API endpoint":
|
|
596
|
+
`ApiEndpointRegistrationSchema` and the ~500-line `ApiRegistry` service that
|
|
597
|
+
`createApiRegistryPlugin()` registered under `api-registry`. Nothing composed it — every
|
|
598
|
+
assembly site lived in `packages/core/examples/`, with no registration in
|
|
599
|
+
`packages/runtime`, `packages/cli` or any `examples/app-*`, and a real boot carried no
|
|
600
|
+
such service. The whole family was therefore inert, including
|
|
601
|
+
`ApiEndpointRegistration.requiredPermissions`, whose docs promised **in the present tense**
|
|
602
|
+
that "the gateway layer automatically validates these permissions" while no gateway read
|
|
603
|
+
it. Two declaration shapes, both dead; this retirement converges them on one.
|
|
604
|
+
|
|
605
|
+
Removed from `@objectstack/spec/api`: `ApiEndpointRegistration(Schema)`,
|
|
606
|
+
`ApiRegistry(Schema)`, `ApiRegistryEntry(Schema)`, `ApiMetadataSchema`,
|
|
607
|
+
`ApiParameterSchema`, `ApiResponseSchema`, `ApiDiscoveryQuerySchema`,
|
|
608
|
+
`ApiDiscoveryResponseSchema`, `ApiProtocolType`, `HttpStatusCode`,
|
|
609
|
+
`ObjectQLReferenceSchema`, `SchemaDefinition` (12 JSON-Schema defs, 67 authorable keys).
|
|
610
|
+
Removed from `@objectstack/core`: `ApiRegistry`, `createApiRegistryPlugin`.
|
|
611
|
+
Removed from `@objectstack/plugin-hono-server`: the `useApiRegistry` option — it was
|
|
612
|
+
defaulted to `true` and read by nothing, configuring a service that was never composed.
|
|
613
|
+
|
|
614
|
+
**FROM → TO.** There is no replacement shape to migrate to, because nothing executed the
|
|
615
|
+
old one: delete the registration objects. If you were assembling an `ApiRegistryEntry`,
|
|
616
|
+
you were building a value only your own code read — keep it as your own type. Declarative
|
|
617
|
+
endpoints have one vocabulary now, `ApiEndpointSchema`.
|
|
618
|
+
|
|
619
|
+
`ConflictResolutionStrategy` **survives** the removal and moved to
|
|
620
|
+
`@objectstack/spec/api`'s `router.zod` — same name, same four values
|
|
621
|
+
(`error`/`priority`/`first-wins`/`last-wins`), same import path. It is pinned there by two
|
|
622
|
+
independent ratchets and is not part of the retired surface.
|
|
623
|
+
|
|
624
|
+
## Also in this change
|
|
625
|
+
|
|
626
|
+
- **BREAKING (`@objectstack/runtime`):** `HttpDispatcher.handleApiEndpoint()` is deleted,
|
|
627
|
+
along with its now-orphaned private `callData` delegate, and `/__api-endpoint` leaves
|
|
628
|
+
`LEGACY_CHAIN_PREFIXES` and the route ledger. The method was public, so this is an API
|
|
629
|
+
removal — but it returned `{ handled: false }` for every call it ever received, so no
|
|
630
|
+
caller can observe a behaviour change beyond the missing symbol. Delete the call.
|
|
631
|
+
Absence is now loud (ADR-0076): the surface is refused at authoring rather than 404ing
|
|
632
|
+
at runtime with dead code behind it.
|
|
633
|
+
- `examples/app-showcase` no longer declares endpoints, and its coverage manifest no
|
|
634
|
+
longer claims the capability is `demonstrated` — that entry read "executed by the runtime
|
|
635
|
+
dispatcher (handleApiEndpoint)", which was exactly the advertise-what-you-don't-deliver
|
|
636
|
+
claim Prime Directive #10 forbids.
|
|
637
|
+
- The endpoint-level `rateLimit` tracking pointers left by #4910/#5006 now name **#5040**,
|
|
638
|
+
the live executor card, instead of #4936, which closes with this change.
|
|
639
|
+
|
|
640
|
+
### Minor Changes
|
|
641
|
+
|
|
642
|
+
- 879ea13: ADR-0105 Phase 0 + Phase 1: group tenancy posture; organization scope as a
|
|
643
|
+
first-class authorization dimension.
|
|
644
|
+
|
|
645
|
+
> This release carries BREAKING spec removals (see "Enforce-or-remove" below)
|
|
646
|
+
> but is recorded as `minor`: every publishable package is in the Changesets
|
|
647
|
+
> lockstep group, so one `major` would promote the whole monorepo. Breaking
|
|
648
|
+
> changes ship as `minor` during the launch window — the migration notes below
|
|
649
|
+
> are what reach consumers in `CHANGELOG.md`.
|
|
650
|
+
|
|
651
|
+
## Tenancy is now a spectrum (D1)
|
|
652
|
+
|
|
653
|
+
`single | group | isolated`, resolved by the `tenancy` service and selected with
|
|
654
|
+
the new `OS_TENANCY_POSTURE` env var. Existing deployments are unchanged:
|
|
655
|
+
`OS_TENANCY_POSTURE` unset derives the posture from `OS_MULTI_ORG_ENABLED`
|
|
656
|
+
(`true` ⇒ `isolated`, else `single`). An unrecognized value throws at boot
|
|
657
|
+
rather than silently landing in a posture with no organization wall.
|
|
658
|
+
|
|
659
|
+
- `single` — no wall (unchanged).
|
|
660
|
+
- `group` — **new.** Organizations are membership boundaries over one shared
|
|
661
|
+
dataset; Layer 0 becomes `organization_id IN accessible_org_ids` (union / MOAC
|
|
662
|
+
semantics). Enforced by the OPEN engine.
|
|
663
|
+
- `isolated` — today's `multi`, renamed. Behavior, enterprise `org-scoping`
|
|
664
|
+
probe and degraded-boot handling all unchanged.
|
|
665
|
+
|
|
666
|
+
## Organization scope is a first-class context field (D2)
|
|
667
|
+
|
|
668
|
+
`ExecutionContext.accessible_org_ids` — every organization the caller holds a
|
|
669
|
+
currently-valid membership in (ADR-0091 validity windows) — is resolved once by
|
|
670
|
+
`resolveAuthzContext` and carried by every transport. The `group` wall reads it
|
|
671
|
+
directly; RLS policies may reference it as
|
|
672
|
+
`organization_id IN (current_user.accessible_org_ids)`. An empty or absent set
|
|
673
|
+
fails the wall closed.
|
|
674
|
+
|
|
675
|
+
Only the Layer 0 PREDICATE widens. Composition is untouched: the wall is still
|
|
676
|
+
computed independently of the RLS compiler, AND-composed outermost, and
|
|
677
|
+
crossable only by a true `PLATFORM_ADMIN` on a posture-permitting object — so
|
|
678
|
+
ADR-0095's W1/W2 invariants hold in every posture.
|
|
679
|
+
|
|
680
|
+
## Two P0 correctness fixes (D3, D4) — behavior changes
|
|
681
|
+
|
|
682
|
+
**D3 — app-authored org-scoped RLS policies are no longer silently dropped**
|
|
683
|
+
(finding F1, framework#3539). `collectRLSPolicies` used to strip any policy whose
|
|
684
|
+
`using` contained the substring `current_user.organization_id` when isolation was
|
|
685
|
+
inactive, which swallowed app-authored policies as well as the platform's own.
|
|
686
|
+
Stripping is now decided by PROVENANCE (identity against the shipped
|
|
687
|
+
declaration). **Upgrade impact:** in a deployment with no organization wall, an
|
|
688
|
+
app-authored policy referencing the active organization is now RETAINED and
|
|
689
|
+
fails closed (zero rows) with a one-time warning, where it previously vanished
|
|
690
|
+
and the object read unscoped. `getReadFilter` shared the defect, so analytics and
|
|
691
|
+
raw-SQL consumers were affected too. If a policy was only ever meant for
|
|
692
|
+
multi-org, delete it or install `@objectstack/organizations`.
|
|
693
|
+
|
|
694
|
+
**D4 — `viewAllRecords`/`modifyAllRecords` never cross an organization
|
|
695
|
+
boundary** (finding F2, framework#3540). Under a wall-less posture nothing
|
|
696
|
+
bounded the wildcard superuser bits `organization_admin` carries, so a
|
|
697
|
+
deployment that accumulated organizations (personal orgs on signup) made every
|
|
698
|
+
owner/admin an environment-wide superuser. `auto-org-admin-grant` now grants a
|
|
699
|
+
de-VAMA'd `organization_admin_no_bypass` variant when no wall is enforced, and
|
|
700
|
+
revokes the superseded variant whenever the posture changes. **Upgrade impact:**
|
|
701
|
+
in `single` posture an org owner/admin keeps full CRUD but loses the blanket
|
|
702
|
+
ownership/sharing/RLS bypass. Deliberate deployment-wide visibility remains
|
|
703
|
+
available through `admin_full_access` or an explicitly authored permission set —
|
|
704
|
+
it just stops being a side effect of a better-auth membership role.
|
|
705
|
+
|
|
706
|
+
## Engine-owned organization stamping (D5)
|
|
707
|
+
|
|
708
|
+
Under any wall-enforcing posture the engine stamps `organization_id` from the
|
|
709
|
+
caller's active organization on an insert that omits it, and validates every
|
|
710
|
+
supplied value against the wall. Idempotent with the enterprise auto-stamp
|
|
711
|
+
(neither overwrites a supplied value). This also closes a real hole: the
|
|
712
|
+
pre-existing post-image check required a non-array payload, so a BULK insert
|
|
713
|
+
could carry a forged `organization_id` per row. One forged row now denies the
|
|
714
|
+
whole write.
|
|
715
|
+
|
|
716
|
+
## Group structure, extension fields and red-line lints (D6, D7)
|
|
717
|
+
|
|
718
|
+
- `sys_organization` gains `parent_organization_id` and `sort_order` — a
|
|
719
|
+
**reporting dimension only**.
|
|
720
|
+
- New lint `validateOrgAxisRedLines` (`org-axis-permission-inheritance`,
|
|
721
|
+
`org-axis-cross-org-bu-grant`), wired into `os lint` / `os compile` /
|
|
722
|
+
`os validate`: an RLS policy or sharing rule that walks the org tree is an
|
|
723
|
+
error, as is a business-unit grant on a platform-global object.
|
|
724
|
+
- Extension fields on better-auth-managed objects ride the existing ADR-0092
|
|
725
|
+
whitelist. A new guard derives better-auth's real field surface from
|
|
726
|
+
`getAuthTables()` at the pinned version and fails the build on any name
|
|
727
|
+
collision, so a library upgrade cannot silently take ownership of a column.
|
|
728
|
+
|
|
729
|
+
## Enforce-or-remove (D11) — BREAKING
|
|
730
|
+
|
|
731
|
+
Both removals are of surface that had **zero runtime consumers**, so no
|
|
732
|
+
behavior changes; authoring them is now a no-op instead of a lint warning.
|
|
733
|
+
|
|
734
|
+
- **`PermissionSet.contextVariables` — REMOVED.** The RLS compiler never read
|
|
735
|
+
it. FROM → TO: a set a policy needs as `field IN (current_user.<key>)` is now
|
|
736
|
+
supplied by a registered membership resolver (below); a constant belongs in
|
|
737
|
+
the policy itself as a literal (`status = 'published'`).
|
|
738
|
+
- **`Territory` / `TerritoryModel` / `TerritoryType` (`security/territory.zod.ts`)
|
|
739
|
+
— REMOVED.** No runtime object, stack field or resolver existed. FROM → TO:
|
|
740
|
+
matrix requirements are served by multi-position × business-unit anchoring; a
|
|
741
|
+
generalized dimension-security module will arrive with its own ADR.
|
|
742
|
+
- **`ExecutionContext.rlsMembership` — PRODUCTIZED.** The bag the compiler has
|
|
743
|
+
merged since ADR-0056 finally has a producer: register an
|
|
744
|
+
`IRlsMembershipResolver` (`@objectstack/spec/contracts`) under the
|
|
745
|
+
`rls-membership-resolver` service, declaring the keys it owns. Fail-closed by
|
|
746
|
+
construction — an unresolved key makes its policies drop out. Kernel-owned
|
|
747
|
+
keys (`accessible_org_ids`, `org_user_ids`, …) are reserved and cannot be
|
|
748
|
+
overwritten from this seam.
|
|
749
|
+
|
|
750
|
+
## Edition boundary (D12)
|
|
751
|
+
|
|
752
|
+
The `group` posture's enforcement primitives ship OPEN — the union wall,
|
|
753
|
+
`accessible_org_ids` resolution, D5 stamping/validation, the D3/D4 correctness
|
|
754
|
+
fixes and the D6 lints — because the correctness of a wall is never a paid
|
|
755
|
+
feature (cloud ADR-0016 铁律「强制免费、治理收费」). `isolated` keeps its existing
|
|
756
|
+
enterprise `org-scoping` probe, so the current commercial boundary for
|
|
757
|
+
legal-entity isolation is unchanged by this release.
|
|
758
|
+
|
|
759
|
+
- 32ccb23: feat(spec,core,runtime)!: ADR-0112 batch 1 — one error-code vocabulary, SCREAMING_SNAKE, schema-enforced (#3841)
|
|
760
|
+
|
|
761
|
+
Settles #3841 per ADR-0112: the top-level `error.code` vocabulary is
|
|
762
|
+
SCREAMING_SNAKE, in two tiers.
|
|
763
|
+
|
|
764
|
+
- **`StandardErrorCode` members renamed in place** (`validation_error` →
|
|
765
|
+
`VALIDATION_ERROR`, all 53). Breaking for importers that branch on the old
|
|
766
|
+
lowercase members; the type name and member _meanings_ are unchanged.
|
|
767
|
+
- **New `ERROR_CODE_LEDGER`** (`@objectstack/spec/api`): service-specific codes
|
|
768
|
+
(`AUTH_REQUIRED`, `VALIDATION_FAILED`, `ATTACHMENT_DOWNLOAD_DENIED`, …) are
|
|
769
|
+
registered per owning package. `ErrorCode` = standard ∪ registered.
|
|
770
|
+
- **`ApiErrorSchema.code` is now `ErrorCode`**, not `z.string()` — an
|
|
771
|
+
unregistered code fails parse, so the envelope conformance suites assert
|
|
772
|
+
values, not just shape.
|
|
773
|
+
- **`FieldErrorSchema.code` widened to `z.string()`** (ADR-0112 D6): field-level
|
|
774
|
+
codes are a separate vocabulary the enum never described; #3977 owns its real
|
|
775
|
+
catalog.
|
|
776
|
+
- **Derived codes changed case on the wire**: `standardErrorCodeForHttpStatus`
|
|
777
|
+
now yields SCREAMING members (`permission_denied` → `PERMISSION_DENIED`,
|
|
778
|
+
`method_not_allowed` → `METHOD_NOT_ALLOWED`, …) — this map was #3842's
|
|
779
|
+
designated one-file sweep point for exactly this decision.
|
|
780
|
+
- **`ANONYMOUS_DENY_CODE` is `'UNAUTHENTICATED'`** (was `'unauthenticated'`) —
|
|
781
|
+
the promoted code on anonymous-denied requests and the REST `enforceAuth`
|
|
782
|
+
body change spelling with it.
|
|
783
|
+
|
|
784
|
+
`error-catalog.mdx` and the error-handling guides are rewritten to the single
|
|
785
|
+
vocabulary; a spec test now locks the catalog page's headings to the enum so
|
|
786
|
+
they cannot drift apart again. Remaining lowercase emitters (cloud-connection,
|
|
787
|
+
plugin-auth envelope codes, metadata-protocol, …) are the batch-2 sweep.
|
|
788
|
+
|
|
789
|
+
- 98877c9: feat(core,platform-objects,spec): the ADR-0119 D2 migration-journal runner — a migration killed mid-run is resumable to completion or compensable to clean, with journal rows proving which (#4617)
|
|
790
|
+
|
|
791
|
+
**The gap D1 left open.** ADR-0119 D1 made `engine.transaction()` reachable
|
|
792
|
+
through the contract, which is the right answer for multi-write atomicity that
|
|
793
|
+
fits in one transaction. Migration-class work does not fit: a million-row
|
|
794
|
+
backfill cannot hold one write-lock for its duration, `driver-memory`'s
|
|
795
|
+
`beginTransaction` deep-clones the entire database (O(db) per begin),
|
|
796
|
+
`ObjectQL.transaction()` binds the **default driver only** so a multi-datasource
|
|
797
|
+
migration silently commits part of its work outside it, and a process **killed**
|
|
798
|
+
— as distinct from a thrown error — defeats in-process rollback entirely. So the
|
|
799
|
+
unit of atomicity is the _chunk_, and durability across chunks is a journal.
|
|
800
|
+
|
|
801
|
+
Four consumers had each converged on the same four moves — dry-run preflight,
|
|
802
|
+
undo journal, LIFO compensation, re-entrant forward recovery (ADR-0105 D13
|
|
803
|
+
promotion, ADR-0117 D8's ownership backfill, the org lifecycle transitions, and
|
|
804
|
+
D10 master-data distribution #4585). One copy is engineering; four is platform
|
|
805
|
+
debt, and the fourth author would have had to rediscover the invariant below
|
|
806
|
+
from scratch.
|
|
807
|
+
|
|
808
|
+
**New: `runMigrationJournal` (`@objectstack/core`).** Preflight runs every
|
|
809
|
+
step's read-only validator before any step writes, so a plan that would fail at
|
|
810
|
+
step 3 has not written step 1. Rows are chunked per the `bulk-write.ts`
|
|
811
|
+
discipline; each chunk's writes run inside `engine.transaction()`. On failure,
|
|
812
|
+
committed chunks are compensated newest-first, each in its own transaction. On
|
|
813
|
+
restart, a rediscovered run resumes forward from the first chunk lacking
|
|
814
|
+
`chunk_done`, or unwinds, per the plan's `onCrash` policy. Forward and
|
|
815
|
+
compensate callbacks receive an `attempt` counter; `attempt > 1` means the prior
|
|
816
|
+
outcome is UNKNOWN and the callback must recheck by natural key before
|
|
817
|
+
re-writing — the same at-least-once contract `bulk-write.ts` already documents,
|
|
818
|
+
reused rather than re-derived.
|
|
819
|
+
|
|
820
|
+
**The invariant that carries the design:** `chunk_done(i)` is written **inside**
|
|
821
|
+
the chunk's own transaction, so `done ⇔ committed` holds by construction;
|
|
822
|
+
`chunk_started(i)` is written autonomously **before** it. That asymmetry is what
|
|
823
|
+
gives `started ∧ ¬done` exactly one meaning — _the outcome is unknown_ — which
|
|
824
|
+
is the only state a crash can leave and the only state recovery reasons about.
|
|
825
|
+
Making both writes symmetric would look tidier and would destroy recovery.
|
|
826
|
+
|
|
827
|
+
**New: `sys_migration_journal` (`@objectstack/platform-objects`).** Rows keyed
|
|
828
|
+
`(run_id, seq)` under a unique index, so a resumed run that miscomputes its next
|
|
829
|
+
sequence fails loudly rather than double-recording an event. Registered
|
|
830
|
+
unconditionally alongside `sys_migration` because recovery must be discoverable
|
|
831
|
+
with **zero host wiring** — a journal some kernels compose and others do not is
|
|
832
|
+
a journal a boot scanner cannot rely on (ADR-0078). Distinct in grain from
|
|
833
|
+
`sys_migration`, which holds one durable verdict per named migration; this holds
|
|
834
|
+
many rows per _run_. Read-only over the API; writes go through the runner in
|
|
835
|
+
system context.
|
|
836
|
+
|
|
837
|
+
**The runner refuses rather than degrades**, in four places: the runtime cannot
|
|
838
|
+
roll back; any preflight fails; the plan declares `onCrash: 'compensate'` but a
|
|
839
|
+
step cannot compensate; or a resume's plan hash disagrees with the journal
|
|
840
|
+
(resuming a changed plan would apply chunk boundaries the journal never
|
|
841
|
+
described). A compensation failure halts and is journalled — never swallowed —
|
|
842
|
+
and the run ends `failed`, not `compensated`, because a database in a state no
|
|
843
|
+
clean story covers must not be reported as a tidy rollback.
|
|
844
|
+
|
|
845
|
+
**`engineCanRollBack` is now shared.** The two-level probe (engine method AND
|
|
846
|
+
default-driver `beginTransaction`) was the same condition written twice — here
|
|
847
|
+
and in `batchData`'s atomic gate. It now lives in `@objectstack/core` and
|
|
848
|
+
`@objectstack/metadata-protocol` imports it, as a type predicate so callers do
|
|
849
|
+
not each re-narrow the optional member by hand. Two copies of "can this runtime
|
|
850
|
+
actually roll back?" drift by one clause and leave one caller believing it has
|
|
851
|
+
atomicity it does not have.
|
|
852
|
+
|
|
853
|
+
Boot reconciliation and `os migrate resume` land separately; `findInterruptedRuns`
|
|
854
|
+
is the discovery primitive they will consume, and is exported here.
|
|
855
|
+
|
|
856
|
+
**Docs:** ADR-0118 (plugin-reachable transactions) is renumbered **ADR-0119**.
|
|
857
|
+
It merged one day after an unrelated ADR-0118 (非用户 actor 的平台契约) and the
|
|
858
|
+
earlier merge holds the number; citations of "ADR-0118 D1/D2/D3/D4" written
|
|
859
|
+
before 2026-08-03 mean the renumbered record.
|
|
860
|
+
|
|
861
|
+
- 0af50a3: fix(driver-sql,service-analytics): a bare-day upper bound covers the whole day on `Field.datetime` (#3777)
|
|
862
|
+
|
|
863
|
+
A bare `YYYY-MM-DD` comparand anchors to midnight UTC. That is right for a
|
|
864
|
+
lower bound and was silently wrong for an upper one: the dashboard date-range
|
|
865
|
+
filter compiles `{ $gte: from, $lte: to }` with bare-day bounds, so on a
|
|
866
|
+
`datetime` column every row created after 00:00 of the `to` day vanished from
|
|
867
|
+
the result — no error, the chart renders, the numbers are just smaller. The
|
|
868
|
+
default configuration hit it: the filter's default field is `created_at`
|
|
869
|
+
(a system-injected `Field.datetime`) and 7 of the 13 presets end "today".
|
|
870
|
+
|
|
871
|
+
The translation is operator-sensitive and half-open, applied at every
|
|
872
|
+
comparison emitter:
|
|
873
|
+
|
|
874
|
+
- `SqlDriver` (and `SqliteWasmDriver` by inheritance): `$lte`/`<=` with a
|
|
875
|
+
bare-day comparand on a `datetime` column compiles to `< next-day-midnight`
|
|
876
|
+
in the column's storage form; `$between [min, max]` with a bare-day max
|
|
877
|
+
decomposes to `>= min AND < next-day(max)`. Both the plain and the
|
|
878
|
+
legacy-repair (mixed-storage) column paths, both `where` spellings.
|
|
879
|
+
- `NativeSQLStrategy`: `dateRange` windows and `lte` filters bind `< next-day`
|
|
880
|
+
instead of an inclusive `BETWEEN`/`<=` when the bound is a bare day.
|
|
881
|
+
- The `/analytics/sql` rendering and the dataset preview evaluator apply the
|
|
882
|
+
same rule, so the echoed SQL and drafted numbers reproduce execution.
|
|
883
|
+
|
|
884
|
+
`@objectstack/core` gains the shared primitive `nextUtcCalendarDay(value)`:
|
|
885
|
+
the next calendar day of a valid bare `YYYY-MM-DD` (else `null` — instants,
|
|
886
|
+
`Date`s and impossible days are never widened).
|
|
887
|
+
|
|
888
|
+
Unchanged on purpose, per the semantics table on #3777: `date`/`time` columns
|
|
889
|
+
(`<= day` is already whole-day-correct there), full-ISO/`Date` comparands
|
|
890
|
+
(instant semantics), and `$gte`/`$gt`/`$lt` (midnight anchoring is correct for
|
|
891
|
+
those). No authored metadata changes: a dashboard's existing
|
|
892
|
+
`{ $gte, $lte }` window now simply includes its final day.
|
|
893
|
+
|
|
894
|
+
- 3c628ce: feat(auth)!: retire the `api.requireAuth` opt-out — anonymous access to object data is always denied (#3963)
|
|
895
|
+
|
|
896
|
+
`api.requireAuth: false` let a deployment open its ENTIRE data plane with one
|
|
897
|
+
config key. It is removed. Auth is a kernel concern, not a deployment posture:
|
|
898
|
+
anonymous callers are denied on every HTTP surface that reaches object data,
|
|
899
|
+
unconditionally.
|
|
900
|
+
|
|
901
|
+
Every surface that legitimately serves a session-less caller already derives its
|
|
902
|
+
own narrow authorization from a DECLARATION, so none of them needed the global
|
|
903
|
+
switch:
|
|
904
|
+
|
|
905
|
+
- control plane (`/auth/*`, `/health`, `/ready`, `/discovery`, ADR-0069
|
|
906
|
+
remediation) — the auth-gate allowlist;
|
|
907
|
+
- public form submission — `publicFormGrant` (ADR-0056 Option A);
|
|
908
|
+
- share links — the capability token, validated then read as SYSTEM;
|
|
909
|
+
- a `book.audience: 'public'` read — the ADR-0046 §6.7 audience gate (#3995);
|
|
910
|
+
- MCP — an OAuth token or API key.
|
|
911
|
+
|
|
912
|
+
**Breaking changes.**
|
|
913
|
+
|
|
914
|
+
- `api.requireAuth` is a retired key. It is tombstoned (`retiredKey`) in both
|
|
915
|
+
`RestApiConfigSchema` and the stack `api` block, so authoring it now fails with
|
|
916
|
+
a fix-it message rather than being silently stripped (the ADR-0104 / #3733
|
|
917
|
+
quiet-failure this whole line of work has been closing). `os migrate meta`
|
|
918
|
+
drops it via the protocol-17 conversion `stack-api-require-auth-removed`.
|
|
919
|
+
- `shouldDenyAnonymous` (@objectstack/core) no longer takes a `requireAuth`
|
|
920
|
+
input; it denies any anonymous, non-system caller outside the control-plane
|
|
921
|
+
allowlist.
|
|
922
|
+
- A stack that mounts **no auth at all** now FAILS AT BOOT when it would serve a
|
|
923
|
+
data API (`objectstack serve`, plugin-dev), instead of getting an explicit
|
|
924
|
+
fail-open. Enable auth (the `auth` tier or AuthPlugin), or run without the data
|
|
925
|
+
API. There is no anonymous-data carve-out any more — publishing a public
|
|
926
|
+
surface is done by declaration (see above).
|
|
927
|
+
|
|
928
|
+
**Migration.** Delete `api.requireAuth` from the stack config (or run
|
|
929
|
+
`os migrate meta`). If you were serving data publicly with `requireAuth: false`,
|
|
930
|
+
replace it with the declaration that fits: a public form view, a share link, or
|
|
931
|
+
`book.audience: 'public'`. If you have an auth-less stack that intentionally
|
|
932
|
+
served data, it must now mount auth or stop serving the data API.
|
|
933
|
+
|
|
934
|
+
- 82da264: feat: declare `ExecutionContext.authGate`, so the ADR-0069 gate sits inside the closed field set (#7280)
|
|
935
|
+
|
|
936
|
+
The ADR-0069 authentication-policy gate (expired password, enforced MFA) rode
|
|
937
|
+
the execution context **undeclared**: REST's `computeExecCtx` spread it onto the
|
|
938
|
+
assembled envelope with `...(authGate ? { authGate } : {})` behind an `as any`,
|
|
939
|
+
and its `enforceAuth` read it back ten lines later. Nothing was broken — but the
|
|
940
|
+
closed entry field set shipped in #6216 is derived from `keyof ExecutionContext`,
|
|
941
|
+
so a field that exists only inside an `as any` is **outside every closure gate by
|
|
942
|
+
construction**: `ENTRY_EXECUTION_CONTEXT_FIELDS` could not list it,
|
|
943
|
+
`ExecutionContextEntryFields` could not demand it, and the runtime pin that
|
|
944
|
+
reconciles the closed set against `ExecutionContextSchema.shape` could not see
|
|
945
|
+
it. It was the exact blind spot that gate exists to remove, sitting one `as any`
|
|
946
|
+
outside it.
|
|
947
|
+
|
|
948
|
+
**@objectstack/spec** declares the field:
|
|
949
|
+
|
|
950
|
+
```ts
|
|
951
|
+
authGate: z.object({ code: z.string(), message: z.string() }).optional();
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
Both inner keys are required, matching the sole producer
|
|
955
|
+
(`AuthManager.computeAuthGate`, which sets both on every return branch) — `code`
|
|
956
|
+
is the stable machine code a client branches on, `message` is what the blocked
|
|
957
|
+
user reads, and the transport seam renders both as the `403` body.
|
|
958
|
+
|
|
959
|
+
**@objectstack/core** picks it up as an ENTRY-decided field — it is resolved from
|
|
960
|
+
the request's own session at the transport entry point, never written mid-request
|
|
961
|
+
— so `ExecutionContextAssemblyInput` gains a **required** `authGate` input on the
|
|
962
|
+
same footing as `accessToken`: every face states its decision instead of omitting
|
|
963
|
+
it. A guest principal never carries one (no authenticated session for a policy
|
|
964
|
+
gate to attach to). Also exported: `normalizeAuthGate`, which completes a session
|
|
965
|
+
user's loose `authGate` into the declared shape at the one producer rather than
|
|
966
|
+
tolerating a partial shape downstream — a gate naming a `code` but no `message`
|
|
967
|
+
no longer renders a `403` body with `message: undefined`. `AuthGate` is now
|
|
968
|
+
derived from the schema instead of being a second hand-written declaration.
|
|
969
|
+
|
|
970
|
+
**@objectstack/rest** passes the resolved gate as an assembler input and drops the
|
|
971
|
+
post-assembly spread; the remaining `as any` covers `__kernel` alone.
|
|
972
|
+
**@objectstack/runtime** (the runtime / MCP dispatcher) passes `authGate:
|
|
973
|
+
undefined` on the record: it enforces the same gate at its own seam
|
|
974
|
+
(`HttpDispatcher.enforceAuthGate` re-reads the session and calls
|
|
975
|
+
`evaluateAuthGate`) and never reads `context.authGate`, so carrying it there
|
|
976
|
+
would be a second copy no consumer reads.
|
|
977
|
+
|
|
978
|
+
**No runtime behaviour change on either surface.** The shared assembler omits
|
|
979
|
+
`undefined`-valued keys, so the key is present exactly when it was before. The one
|
|
980
|
+
new behaviour is the normalization above, on a shape the sole producer never
|
|
981
|
+
emits today.
|
|
982
|
+
|
|
983
|
+
- f586f1a: refactor: one shared `ExecutionContext` assembler, two named anonymous entries (#6216)
|
|
984
|
+
|
|
985
|
+
`resolveAuthzContext` already made AUTHORIZATION resolution single-sourced; the
|
|
986
|
+
step after it — turning the resolved envelope into the `ExecutionContext` that
|
|
987
|
+
reaches enforcement — was still one hand-written copy per transport, and the
|
|
988
|
+
copies drifted twice for real: **#6071** (the REST copy never set
|
|
989
|
+
`principalKind`, so every enforcement judgment reading it was silently
|
|
990
|
+
never-true on that face) and **#6206 / #6551** (a dropped `accessible_org_ids`
|
|
991
|
+
produced real 403s on the share-link faces).
|
|
992
|
+
|
|
993
|
+
**@objectstack/core** gains the single assembly, with the anonymous divergence
|
|
994
|
+
as named API rather than drift (maintainer ruling 2026-08-08 on #6216, Option
|
|
995
|
+
A):
|
|
996
|
+
|
|
997
|
+
- `assembleExecutionContext(input)` — the **fail-closed default** entry. No
|
|
998
|
+
resolved principal → `undefined`, and the surface answers 401.
|
|
999
|
+
- `assembleExecutionContextOrGuest(input)` — the **explicit guest** entry. No
|
|
1000
|
+
resolved principal → a first-class guest envelope (`principalKind: 'guest'`,
|
|
1001
|
+
`positions: ['guest']`), whose consumers are live (`explain-engine`'s
|
|
1002
|
+
guest ⇒ `EXTERNAL` posture floor). Adopted only by a surface whose product
|
|
1003
|
+
semantics serve anonymous principals.
|
|
1004
|
+
- The field set is **closed by type**: `ExecutionContextEntryFields` requires a
|
|
1005
|
+
decision for every `ExecutionContext` field that is not explicitly declared
|
|
1006
|
+
non-entry-resolved, so a new field cannot reach one transport and miss
|
|
1007
|
+
another. Also exported: `ENTRY_EXECUTION_CONTEXT_FIELDS`,
|
|
1008
|
+
`EntryExecutionContextField`, `ExecutionContextAssemblyInput`,
|
|
1009
|
+
`OAuthTokenProvenance`, `EntryLocalization`.
|
|
1010
|
+
|
|
1011
|
+
**@objectstack/runtime** (`resolveExecutionContext`, the runtime / MCP
|
|
1012
|
+
dispatcher) and **@objectstack/rest** (`computeExecCtx`) now assemble through
|
|
1013
|
+
that module — the dispatcher via the guest entry, REST via the fail-closed
|
|
1014
|
+
default.
|
|
1015
|
+
|
|
1016
|
+
**No runtime behaviour change on either surface.** The remaining per-face
|
|
1017
|
+
divergences are required inputs rather than silent omissions: REST passes
|
|
1018
|
+
`accessToken: undefined` (it has never carried the session bearer on the
|
|
1019
|
+
envelope, and `session.accessToken` is a published hook surface) and
|
|
1020
|
+
`oauth: undefined` (OAuth bearers are honoured on the `/mcp` door alone). The
|
|
1021
|
+
one measurable difference is that a key whose value was `undefined` is now
|
|
1022
|
+
omitted rather than spelled — invisible to `ctx.x` reads, to `JSON.stringify`
|
|
1023
|
+
and to spreading the envelope.
|
|
1024
|
+
|
|
1025
|
+
- 763931e: feat(filters): evaluate `{filter-token}` placeholders server-side (#3582)
|
|
1026
|
+
|
|
1027
|
+
Filter values travel as JSON, so a time- or user-scoped slice writes a
|
|
1028
|
+
placeholder instead of code:
|
|
1029
|
+
|
|
1030
|
+
```ts
|
|
1031
|
+
filter: { close_date: { $gte: '{current_year_start}' }, owner: '{current_user_id}' }
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
The vocabulary has been in `@objectstack/spec` for a while (`date-macros.zod.ts`,
|
|
1035
|
+
`context-tokens.zod.ts`) and `objectstack build` rejects tokens outside it
|
|
1036
|
+
(#3574). What was missing is the half that _substitutes a value_: **nothing on
|
|
1037
|
+
the server ever did**. A placeholder reached the driver as the literal string
|
|
1038
|
+
`'{current_year_start}'`, compared as text, and matched nothing.
|
|
1039
|
+
|
|
1040
|
+
That failure is invisible — an empty widget looks exactly like a metric that is
|
|
1041
|
+
legitimately zero — so apps worked around it by computing dates at module load,
|
|
1042
|
+
which freezes "this year" into the built artifact and quietly goes stale.
|
|
1043
|
+
|
|
1044
|
+
**New: `resolveFilterTokens()` in `@objectstack/core`**, wired into the two
|
|
1045
|
+
server-side seams every filter passes through:
|
|
1046
|
+
|
|
1047
|
+
- **ObjectQL read path** — `find` / `findOne` / `count` / `aggregate`, so REST
|
|
1048
|
+
queries, related lists, saved-view filters and flow `find_records` all resolve.
|
|
1049
|
+
It runs before the middleware chain, so only author-supplied filters are
|
|
1050
|
+
inspected; RLS/sharing filters are injected downstream from concrete values.
|
|
1051
|
+
- **Analytics dataset executor** — a dataset's intrinsic `filter`, a widget's
|
|
1052
|
+
`runtimeFilter`, measure-scoped filters, and time-dimension `dateRange`s.
|
|
1053
|
+
This path needs its own call: `NativeSQLStrategy` compiles raw SQL and binds
|
|
1054
|
+
comparands directly, so a dashboard widget never passes through `engine.find()`.
|
|
1055
|
+
|
|
1056
|
+
Behavioural notes:
|
|
1057
|
+
|
|
1058
|
+
- Date tokens resolve to ISO strings (`YYYY-MM-DD`, or a full timestamp for
|
|
1059
|
+
`{now}` / `{N_hours_ago}` / `{N_minutes_ago}`). Turning that into a column's
|
|
1060
|
+
on-disk form stays the driver's job (`SqlDriver.temporalFilterValue`), so
|
|
1061
|
+
there is still exactly one source of truth for the storage convention.
|
|
1062
|
+
- Calendar boundaries follow `ExecutionContext.timezone`; one instant is pinned
|
|
1063
|
+
per filter tree, so a `>= {current_month_start}` / `< {next_month_start}` pair
|
|
1064
|
+
can never straddle a boundary.
|
|
1065
|
+
- `{current_org_id}` reads `ExecutionContext.tenantId`; `{current_user_id}` reads
|
|
1066
|
+
`userId`. A request carrying neither now **throws** instead of resolving to
|
|
1067
|
+
`null` — a null comparand degrades to `IS NULL` on most drivers and would hand
|
|
1068
|
+
back the rows the filter was written to exclude.
|
|
1069
|
+
- An unrecognised placeholder **throws**, carrying the near-miss fix
|
|
1070
|
+
(`{current_user}` → `{current_user_id}`, `{this_quarter_start}` →
|
|
1071
|
+
`{current_quarter_start}`). This matches what `objectstack build` already
|
|
1072
|
+
enforces. Consequence, previously implicit and now load-bearing: a filter value
|
|
1073
|
+
that is _entirely_ `{...}` is always read as a placeholder, so a literal value
|
|
1074
|
+
of that shape is not expressible — rename the value.
|
|
1075
|
+
|
|
1076
|
+
Also in this change: `notify` no longer sends the six-character string
|
|
1077
|
+
`"undefined"` as an audience member. `to: ['{record.owner.manager}']` walks
|
|
1078
|
+
`.manager` on a scalar foreign-key id, resolves to nothing, and `String(undefined)`
|
|
1079
|
+
turned that into a phantom recipient — the emit "succeeded", addressed nobody,
|
|
1080
|
+
and said nothing. Unresolved recipients are now dropped, and a node with no
|
|
1081
|
+
recipient left fails naming the offending template and pointing at the start
|
|
1082
|
+
node's `config.expand` (#3475), which does hydrate the relation.
|
|
1083
|
+
|
|
1084
|
+
- 518ca7a: fix(i18n): `GET /i18n/locales` reports the locales the app declared, not every locale a plugin happened to load (#7679)
|
|
1085
|
+
|
|
1086
|
+
`GET /api/v1/i18n/locales` answered with four locale descriptors — `en`,
|
|
1087
|
+
`zh-CN`, `ja-JP`, `es-ES` — on the showcase app, whose artifact declares
|
|
1088
|
+
`i18n.supportedLocales: ['en', 'zh-CN']`. The envelope was correct (#3636); the
|
|
1089
|
+
**set** was a superset.
|
|
1090
|
+
|
|
1091
|
+
Nothing was wrong with what had been _loaded_. Every platform plugin
|
|
1092
|
+
(`platform-objects`, `service-settings`, `service-storage`, `service-messaging`,
|
|
1093
|
+
`service-realtime`, `plugin-security`, `plugin-sharing`, `plugin-webhooks`)
|
|
1094
|
+
ships an `en/zh-CN/ja-JP/es-ES` bundle and pushes it at `kernel:ready`, which is
|
|
1095
|
+
what a platform should do. What was wrong is that the **loaded** set was
|
|
1096
|
+
reported as the **offered** set — two different facts owned by two different
|
|
1097
|
+
parties. So a locale picker built from this route, including the platform's own
|
|
1098
|
+
Settings > Localization select, offered `ja-JP` and `es-ES`: locales in which
|
|
1099
|
+
only `sys_*` objects are translated, guaranteeing a mixed-language session for
|
|
1100
|
+
everything the app itself owns.
|
|
1101
|
+
|
|
1102
|
+
**What changed.** `II18nService` gains an optional
|
|
1103
|
+
`setSupportedLocales(locales)`. `AppPlugin.loadTranslations` threads the
|
|
1104
|
+
artifact's `i18n.supportedLocales` into it exactly the way it already threads
|
|
1105
|
+
`defaultLocale`, and both providers of the `i18n` slot — `createMemoryI18n` in
|
|
1106
|
+
`@objectstack/core` and `FileI18nAdapter` in `@objectstack/service-i18n` —
|
|
1107
|
+
narrow what `getLocales()` reports to that declaration. The runtime app-plugin
|
|
1108
|
+
layer is the only place this can originate: `getLocales()` sees what is loaded,
|
|
1109
|
+
and the app's declaration is not visible below it.
|
|
1110
|
+
|
|
1111
|
+
The narrowing is applied as a filter at **read** time, never as a prune of what
|
|
1112
|
+
is stored, because the platform bundles arrive _after_ the app plugin has run.
|
|
1113
|
+
|
|
1114
|
+
**Only the reported set narrows.** Bundles stay loaded and stay servable:
|
|
1115
|
+
`GET /i18n/translations/ja-JP` still answers on a stack that no longer
|
|
1116
|
+
advertises `ja-JP`, and `t()` still resolves it. Unloading those bundles buys
|
|
1117
|
+
nothing — `sys_*` translations for an unadvertised locale cost nothing sitting
|
|
1118
|
+
in the map.
|
|
1119
|
+
|
|
1120
|
+
Two questions the fix had to settle, both behaviour in their own right:
|
|
1121
|
+
|
|
1122
|
+
- **An app that declares no `supportedLocales` is not narrowed.** Absent means
|
|
1123
|
+
"no narrowing", and it keeps reporting every loaded locale — the behaviour it
|
|
1124
|
+
has today. Every app written before this change declared nothing, so
|
|
1125
|
+
narrowing an undeclared app to zero (or to its default alone) would have
|
|
1126
|
+
emptied the picker on every stack whose author never opted in. An
|
|
1127
|
+
`i18n` block carrying only a `defaultLocale`, and a `supportedLocales: []`
|
|
1128
|
+
that declares no usable code, are both read the same way.
|
|
1129
|
+
- **A declared locale with no bundle behind it is reported, not dropped.** If an
|
|
1130
|
+
app declares a locale the platform plugins never shipped, it appears in the
|
|
1131
|
+
response as declared-but-unserved rather than being silently intersected away.
|
|
1132
|
+
The declaration is the app's statement of intent and the client is entitled to
|
|
1133
|
+
see it; a quietly shortened list hides the authoring gap from both ends.
|
|
1134
|
+
Reporting the declaration is also the only answer that does not depend on how
|
|
1135
|
+
many bundles had loaded by the time the route was called. Reads for such a
|
|
1136
|
+
locale degrade to the default/fallback exactly as a half-translated bundle's
|
|
1137
|
+
missing keys already do.
|
|
1138
|
+
|
|
1139
|
+
Reported locales now follow the **declared order** rather than the insertion
|
|
1140
|
+
order of whichever plugin loaded first, so a picker renders the ordering the app
|
|
1141
|
+
author wrote.
|
|
1142
|
+
|
|
1143
|
+
`setSupportedLocales` is optional on the contract, like `setDefaultLocale`: a
|
|
1144
|
+
third-party `II18nService` that does not implement it keeps its current
|
|
1145
|
+
behaviour instead of failing to boot.
|
|
1146
|
+
|
|
1147
|
+
- 4cca74c: fix(i18n)!: the `translation` metadata type speaks the same `objects.` shape everything else does (#3778)
|
|
1148
|
+
|
|
1149
|
+
A translation authored in the product saved successfully and then rendered
|
|
1150
|
+
nothing. Not a resolver gap — a contract split. The `translation` metadata type
|
|
1151
|
+
(`allowRuntimeCreate: true`, so Studio/the metadata API/an agent can author it)
|
|
1152
|
+
was registered against `AppTranslationBundleSchema`, an object-first shape keyed
|
|
1153
|
+
on `o.<object>`. Every resolver, `os i18n extract`, `os i18n check`, the objectui
|
|
1154
|
+
hooks, and all nine shipped bundles read `objects.<object>`. Nothing bridged the
|
|
1155
|
+
two, so the save path and the read path never met.
|
|
1156
|
+
|
|
1157
|
+
**Why converge instead of bridge.** A converter was the obvious fix and the
|
|
1158
|
+
wrong one: it would be throwaway code, and it would start producing _working_
|
|
1159
|
+
`o.`-shaped rows — closing the migration-free window that exists precisely
|
|
1160
|
+
because the feature never functioned. The retired shape's real-world footprint
|
|
1161
|
+
was zero: all three `*.translation.ts` files in the tree (platform-objects,
|
|
1162
|
+
CRM and todo examples) were already `objects.`-shaped, contradicting the type's
|
|
1163
|
+
own registered schema. Converging is a registration fix, not a migration.
|
|
1164
|
+
|
|
1165
|
+
**Breaking.** `AppTranslationBundleSchema`, `ObjectTranslationNodeSchema`, and
|
|
1166
|
+
their types are **deleted** — no deprecation cycle. Nothing worked end-to-end
|
|
1167
|
+
through them, so there is no functioning consumer to protect, and a
|
|
1168
|
+
deprecated-but-present schema is exactly the exemplar an AI agent copies into
|
|
1169
|
+
new code. The optional `II18nService.getAppBundle` / `loadAppBundle` methods go
|
|
1170
|
+
with them: zero implementers, so they advertised a capability the runtime never
|
|
1171
|
+
delivered.
|
|
1172
|
+
|
|
1173
|
+
**The replacement.** `TranslationItemSchema` — one locale of the same
|
|
1174
|
+
`TranslationData` groups a file bundle uses, plus the `locale` it translates,
|
|
1175
|
+
with a `defineTranslation()` factory. An item is one entry of a
|
|
1176
|
+
`TranslationBundle`; that is the whole type.
|
|
1177
|
+
|
|
1178
|
+
Three details are deliberate, all aimed at the failure being silent rather than
|
|
1179
|
+
loud:
|
|
1180
|
+
|
|
1181
|
+
- **`locale` is required**, not inferred from the item name. The sync skips an
|
|
1182
|
+
item whose locale it cannot resolve, and a skip is invisible to whoever — or
|
|
1183
|
+
whatever — authored it. (The name fallback still covers rows written before
|
|
1184
|
+
this.)
|
|
1185
|
+
- **Retired keys are rejected, not stripped.** Zod drops undeclared keys
|
|
1186
|
+
silently, which would reproduce this bug exactly: save succeeds, nothing
|
|
1187
|
+
renders. A pre-parse guard turns that silence into a 422 naming the group to
|
|
1188
|
+
use (`'o' … — use 'objects.<object_name>'`). It runs ahead of the parse so the
|
|
1189
|
+
retired keys stay out of the schema itself — the generated JSON Schema and the
|
|
1190
|
+
Studio editor never advertise a shape that cannot work.
|
|
1191
|
+
- **`ObjectTranslationData.label` is now optional.** Partial translation is the
|
|
1192
|
+
normal state and every resolver already treats each key as independent.
|
|
1193
|
+
Requiring it forced authors to restate the source label just to validate,
|
|
1194
|
+
filling bundles with fake translations that mask real coverage gaps.
|
|
1195
|
+
|
|
1196
|
+
Also in this change: the authored-translation sync warns (naming the row and the
|
|
1197
|
+
fix) when it meets a row still in the retired shape instead of loading it into
|
|
1198
|
+
nowhere, and no longer merges publish bookkeeping (`_lockReason`,
|
|
1199
|
+
`_packageVersion`, …) into the translation layer. `GET
|
|
1200
|
+
/i18n/labels/:object/:locale`'s fallback now reads the nested
|
|
1201
|
+
`objects.<obj>.fields.<field>.label` data it is actually given — it scanned for
|
|
1202
|
+
flat dotted `o.<obj>.fields.<field>` keys, a third dialect no producer ever
|
|
1203
|
+
wrote, so it always returned `{}`.
|
|
1204
|
+
|
|
1205
|
+
Migration: author every translation — file or runtime item — under `objects.`.
|
|
1206
|
+
`o` → `objects`, `app` → `apps`, `nav` → `apps.<app>.navigation.<id>.label`,
|
|
1207
|
+
`dashboard` → `dashboards`, `_globalOptions` →
|
|
1208
|
+
`objects.<obj>.fields.<field>.options`, `_meta.locale` → top-level `locale`,
|
|
1209
|
+
`_actions.confirmMessage` → `_actions.confirmText`. `reports`, `notifications`,
|
|
1210
|
+
`errors`, and `namespace` had no runtime consumer and have no replacement.
|
|
1211
|
+
|
|
1212
|
+
- 0f2fdcd: fix(core)!: a throwing `kernel:bootstrapped` / `kernel:listening` handler fails the boot on LiteKernel too (#5257)
|
|
1213
|
+
|
|
1214
|
+
**A failed `listen()` no longer yields a false "✅ Bootstrap complete".**
|
|
1215
|
+
|
|
1216
|
+
#5170 (PR #5258) unified `kernel:ready`: a handler that throws fails the boot on
|
|
1217
|
+
`ObjectKernel` and `LiteKernel` alike. It deliberately ruled that one hook only,
|
|
1218
|
+
leaving the other lifecycle hooks split — `ObjectKernel` propagates their
|
|
1219
|
+
failures (its `context.trigger` is a bare awaited loop that never catches) while
|
|
1220
|
+
`LiteKernel` routed them through the isolating dispatcher, logging
|
|
1221
|
+
`Hook handler failed: <name>` and carrying on. This closes the two boot-path
|
|
1222
|
+
hooks that were left: `kernel:bootstrapped` and `kernel:listening` now use the
|
|
1223
|
+
propagating dispatcher (`triggerHookOrThrow`) on `LiteKernel`, in the same shape
|
|
1224
|
+
#5258 established — the remaining handlers for that hook are skipped, the later
|
|
1225
|
+
boot hooks never fire, the original error reaches the caller **unwrapped**,
|
|
1226
|
+
`state` is left `'stopped'` rather than `'running'`, and the success line is
|
|
1227
|
+
never logged.
|
|
1228
|
+
|
|
1229
|
+
The concrete failure this removes: `HonoServerPlugin` opens its socket inside a
|
|
1230
|
+
`kernel:listening` handler — `await this.server.listen(port)`, with no try/catch
|
|
1231
|
+
of its own, deliberately. When that rejected on `LiteKernel` (EACCES on a
|
|
1232
|
+
privileged port, a failure inside the port-fallback logic itself, a serverless /
|
|
1233
|
+
edge host where `listen` is not available at all) the throw was swallowed,
|
|
1234
|
+
`bootstrap()` resolved normally, and the process printed
|
|
1235
|
+
`✅ Bootstrap complete` while **nothing was listening**. The same plugin code on
|
|
1236
|
+
`ObjectKernel` failed the boot. The health check that came next was the first
|
|
1237
|
+
thing to notice, and it had already been told startup succeeded. Plain "port is
|
|
1238
|
+
in use" was never affected — `server.listen` falls back to a random port
|
|
1239
|
+
internally — which is exactly why this stayed invisible.
|
|
1240
|
+
|
|
1241
|
+
`kernel:bootstrapped` carries reconcile and audit work (objectql's
|
|
1242
|
+
`announceOpenMigrationGates`, service-automation's node-type / trigger-binding
|
|
1243
|
+
audits, the sharing plugin's boot backfills); a swallowed failure there is a
|
|
1244
|
+
quieter version of the same lie — the audit silently does not run.
|
|
1245
|
+
|
|
1246
|
+
**`kernel:shutdown` keeps fail-soft dispatch**, now as an explicit per-hook
|
|
1247
|
+
judgement recorded in a comment at the dispatch site rather than an inherited
|
|
1248
|
+
default. On the teardown path there is no "refuse to proceed" left to buy, and
|
|
1249
|
+
the handlers queued behind a failing one — plus the reverse-order `destroy()`
|
|
1250
|
+
pass after them — are what flush buffers, close connections and release locks.
|
|
1251
|
+
Aborting that sequence would convert one bad handler into leaked resources and
|
|
1252
|
+
unflushed writes.
|
|
1253
|
+
|
|
1254
|
+
**Who is affected.** Hosts that boot through `LiteKernel` — vitest, serverless,
|
|
1255
|
+
edge (Workers) — and register a `kernel:bootstrapped` or `kernel:listening`
|
|
1256
|
+
handler that can throw. Such a host previously came up "successfully" with the
|
|
1257
|
+
work of that handler silently skipped; it now refuses to start and surfaces the
|
|
1258
|
+
original error. If a handler of yours performs best-effort work whose failure
|
|
1259
|
+
genuinely must not stop the boot, it needs its own `try/catch` — which is what
|
|
1260
|
+
the in-repo `kernel:bootstrapped` subscribers already do, per handler, with the
|
|
1261
|
+
reason written down. Nothing in this repo relied on the swallow: the core (426),
|
|
1262
|
+
client, runtime, http-conformance, connector-{rest,mcp,slack} and
|
|
1263
|
+
service-automation (665) suites pass unchanged.
|
|
1264
|
+
|
|
1265
|
+
Boot assertions still belong in `kernel:ready`: it is the earliest hook at which
|
|
1266
|
+
the service registry is finished filling.
|
|
1267
|
+
|
|
1268
|
+
- 8ffa8b9: fix(core)!: a throwing `kernel:ready` handler now fails the boot on **LiteKernel** too (#5170)
|
|
1269
|
+
|
|
1270
|
+
**Behaviour change — read this if you run `LiteKernel` (vitest harnesses,
|
|
1271
|
+
serverless functions, edge workers).** A `kernel:ready` handler that throws now
|
|
1272
|
+
**rejects `bootstrap()`** on `LiteKernel`, exactly as it always has on
|
|
1273
|
+
`ObjectKernel`. Before this change the throw was caught inside the kernel,
|
|
1274
|
+
written out as one `Hook handler failed: kernel:ready` error log, and the boot
|
|
1275
|
+
continued to "✅ Bootstrap complete".
|
|
1276
|
+
|
|
1277
|
+
**Why it mattered.** The two kernels ran the same hook through two different
|
|
1278
|
+
dispatchers: `ObjectKernel` used `context.trigger` (a bare awaited loop that
|
|
1279
|
+
never catches), `LiteKernel` used `triggerHook` (per-handler try/catch,
|
|
1280
|
+
"continue with other handlers even if one fails"). Same hook name, same plugin
|
|
1281
|
+
code, opposite failure semantics — which is `declared ≠ enforced` in the
|
|
1282
|
+
kernel's own lifecycle contract.
|
|
1283
|
+
|
|
1284
|
+
`kernel:ready` is the only correct moment for a plugin to assert that a
|
|
1285
|
+
precondition it _declared_ was actually delivered: the service registry is
|
|
1286
|
+
still filling during `init()`, so a boot gate has nowhere earlier to run. Every
|
|
1287
|
+
"declare it and we refuse to start if we cannot honour it" gate in this repo
|
|
1288
|
+
therefore lives there — and on `LiteKernel` those gates were being downgraded to
|
|
1289
|
+
a log line while the process came up and served traffic without the guarantee it
|
|
1290
|
+
had announced. `EmailServicePlugin`'s `queueDelivery: true` gate (#5160) is the
|
|
1291
|
+
worked example: on `ObjectKernel` the boot failed, on `LiteKernel` the server
|
|
1292
|
+
came up and quietly fell back to inline delivery. Serverless is exactly where
|
|
1293
|
+
"do not start misconfigured" matters most.
|
|
1294
|
+
|
|
1295
|
+
**Who is affected.** Any `LiteKernel` host whose `kernel:ready` handler throws
|
|
1296
|
+
on a healthy boot. That boot previously "succeeded"; it now fails loudly with
|
|
1297
|
+
the original error, and the kernel is left `stopped` rather than `running`. The
|
|
1298
|
+
failure was never silent — it was already an `ERROR` line in your logs — so
|
|
1299
|
+
check for `Hook handler failed: kernel:ready` in existing logs to find hosts
|
|
1300
|
+
that will now refuse to start. If the handler's work is genuinely optional,
|
|
1301
|
+
catch inside the handler and log there; the kernel no longer decides that for
|
|
1302
|
+
you. The full test surface in this repo that boots `LiteKernel` (core, client,
|
|
1303
|
+
runtime, http-conformance, the connectors, service-automation) passes unchanged
|
|
1304
|
+
— nothing was relying on the swallow.
|
|
1305
|
+
|
|
1306
|
+
Scope: **`kernel:ready` only.** `kernel:bootstrapped`, `kernel:listening` and
|
|
1307
|
+
`kernel:shutdown` keep `LiteKernel`'s isolating dispatch, pinned by a test.
|
|
1308
|
+
|
|
1309
|
+
- 9319586: feat(core,metadata,objectql): `IMetadataService.register` refuses ambiguous writes, and type stores key on the canonical type (#7378)
|
|
1310
|
+
|
|
1311
|
+
The maintainer's three-cell ruling of 2026-08-12 on #7378, implemented in every
|
|
1312
|
+
shipped `IMetadataService` implementation — `createMemoryMetadata`
|
|
1313
|
+
(`@objectstack/core`), `MetadataManager` (`@objectstack/metadata`) and
|
|
1314
|
+
`MetadataFacade` (`@objectstack/objectql`) — through one shared guard,
|
|
1315
|
+
`assertMetadataRegisterContract` / `canonicalMetadataServiceType`, newly
|
|
1316
|
+
exported from `@objectstack/core`:
|
|
1317
|
+
|
|
1318
|
+
- **A `data.name` that disagrees with the `name` argument is refused** with a
|
|
1319
|
+
locating `VALIDATION_ERROR` (status 400), before anything is stored. The
|
|
1320
|
+
previous behaviours resolved the disagreement silently in opposite
|
|
1321
|
+
directions per implementation (argument-wins on the Map-backed stores,
|
|
1322
|
+
document-wins on the pre-#7511 facade), either of which can file an item
|
|
1323
|
+
under a key the author never wrote. A document carrying no `name` of its own
|
|
1324
|
+
still registers under the argument — absence is not a disagreement.
|
|
1325
|
+
- **A non-object `data` (primitive, `null`, array) is refused** the same way.
|
|
1326
|
+
It was previously accepted-then-dropped by `MetadataFacade` (readable back
|
|
1327
|
+
through no member) and interim-fixed by boxing into `{ name, content }`; the
|
|
1328
|
+
ruling forbids both the drop and the coercion.
|
|
1329
|
+
- **Type stores are keyed on the canonical (singular) type**: `'objects'` and
|
|
1330
|
+
`'object'` now address ONE store on every implementation, in both the write
|
|
1331
|
+
and the read direction, converging with the platform's enforced
|
|
1332
|
+
plural→singular normalization (`PLURAL_TO_SINGULAR`, `canonicalMetaType`
|
|
1333
|
+
#4432, `check:meta-type-normalized`).
|
|
1334
|
+
|
|
1335
|
+
Callers that register with a matching (or absent) `data.name` and plain-object
|
|
1336
|
+
documents — every in-tree caller — are unaffected. A caller that relied on a
|
|
1337
|
+
mismatched `data.name` being silently resolved must pass the intended key as
|
|
1338
|
+
the argument and make `data.name` match it; a caller storing a bare value must
|
|
1339
|
+
wrap it in a document whose shape its type's schema accepts.
|
|
1340
|
+
|
|
1341
|
+
- 071d0dc: feat(runtime,cli,core): boot reconciliation and `os migrate resume` for the migration journal — an interrupted run can no longer go unnoticed (ADR-0119 D2, #4617)
|
|
1342
|
+
|
|
1343
|
+
Completes ADR-0119 D2. The runner and `sys_migration_journal` landed in #4668; this is the discovery channel that makes an interrupted run findable by someone who does not already know it happened.
|
|
1344
|
+
|
|
1345
|
+
**`MigrationRecoveryPlugin` (`@objectstack/runtime`)** — at `kernel:ready`, scans the journal for runs that started and never concluded, and warns per run: how many chunks committed, which have an **unknown** outcome (`chunk_started` with no `chunk_done`), whether a compensation was left half-finished, and the exact command that will act. It also owns the `migration-plans` registry service.
|
|
1346
|
+
|
|
1347
|
+
**`os migrate resume` (`@objectstack/cli`)** — lists interrupted runs (read-only, the default), or acts on one with `--run <id>`, under confirmation. Exits non-zero when a run ends `failed`, so a scripted recovery cannot move on from a migration that needs a human.
|
|
1348
|
+
|
|
1349
|
+
**`MigrationPlanRegistry` (`@objectstack/core`)** — where a resume finds the plan it has to re-run.
|
|
1350
|
+
|
|
1351
|
+
## Boot discovers, the CLI acts
|
|
1352
|
+
|
|
1353
|
+
This is the design decision, and it is deliberate rather than incidental.
|
|
1354
|
+
|
|
1355
|
+
Resuming is a large, irreversible, potentially hour-long write against production data. Doing that as an unrequested side effect of a process starting is the kind of behaviour an operator finds out about from a graph. It is also not always possible at boot: a resume needs the plan's live callbacks, and the package that owns them may not be loaded in whichever process happened to restart first.
|
|
1356
|
+
|
|
1357
|
+
So boot surfaces the run and names the command; the command acts, under explicit operator intent. ADR-0119 D2's per-plan `onCrash` policy still decides **what** acting means — resume forward from the first chunk lacking `chunk_done`, or unwind what committed — it just does not decide **when**, and "when" is the part a human should own.
|
|
1358
|
+
|
|
1359
|
+
Deferring is safe precisely because of the runner's re-entrancy: `started ∧ ¬done` is durable, so an interrupted run stays exactly as recoverable an hour later as it was at boot. Nothing decays while the operator decides.
|
|
1360
|
+
|
|
1361
|
+
## Why a plan registry exists at all
|
|
1362
|
+
|
|
1363
|
+
A journal cannot hold a plan. `forward` and `compensate` are functions and `load()` reads the live database, so none of it crosses a process boundary — which is why the journal records the plan **hash**, not the plan. Recovery therefore needs the plan handed back by the code that owns it, and `migration-plans` is that seam: between "the journal knows a run stopped at chunk 7" and "something in this process knows what chunk 7 was supposed to do".
|
|
1364
|
+
|
|
1365
|
+
A run whose plan no loaded package registers is **reported**, never silently skipped — the operator is told which plan id is missing. "Nothing to resume" and "the code that owns this run is not here" are different facts, and only one of them is safe to ignore.
|
|
1366
|
+
|
|
1367
|
+
## Degradation
|
|
1368
|
+
|
|
1369
|
+
No engine, or no `sys_migration_journal` registered (a lean kernel that never composed platform-objects) → the scan is skipped in **silence**: such a kernel has no interrupted runs to find, and a warning there would train operators to ignore this plugin's output, which is the one thing it cannot afford. A scan that **fails**, by contrast, is reported — "I could not check" and "there is nothing to find" are different answers.
|
|
1370
|
+
|
|
1371
|
+
11 new tests pin the split (boot writes nothing to the journal), the three states an operator must tell apart (clean / interrupted / half-unwound), and both degradation paths.
|
|
1372
|
+
|
|
1373
|
+
- d13004a: feat(core,runtime): plugin ordering is a declared, kernel-enforced contract (ADR-0116, #4131)
|
|
1374
|
+
|
|
1375
|
+
`kernel.use()` registration order was never a contract — the kernel resolves
|
|
1376
|
+
init/start order from the plugin dependency graph — but a plugin that needed a
|
|
1377
|
+
service at init _when its provider is composed_ while also booting _without_
|
|
1378
|
+
the provider had no way to declare that. `AppPlugin` was the standing example:
|
|
1379
|
+
it grabs `manifest`/`objectql` synchronously in `init()`, declared nothing
|
|
1380
|
+
(a hard dependency would break empty-env / metadata-only / mock-engine
|
|
1381
|
+
kernels), and so its correctness rode on which array slot each caller put it
|
|
1382
|
+
in. That convention failed the same way twice (`DefaultDatasourcePlugin`'s
|
|
1383
|
+
first cut; then #4085, disguised for months as "crashes when the artifact is
|
|
1384
|
+
missing").
|
|
1385
|
+
|
|
1386
|
+
The kernel `Plugin` contract gains three additive fields, enforced by both
|
|
1387
|
+
`ObjectKernel` and `LiteKernel` through one shared implementation
|
|
1388
|
+
(`plugin-order.ts` — the previously duplicated topological sort is unified
|
|
1389
|
+
there):
|
|
1390
|
+
|
|
1391
|
+
- **`optionalDependencies: string[]`** — order-if-present: hoisted ahead
|
|
1392
|
+
exactly like `dependencies` when composed (real topology edges, including
|
|
1393
|
+
cycle detection), silently skipped when absent.
|
|
1394
|
+
- **`requiresServices: string[]`** — services resolved synchronously during
|
|
1395
|
+
`init()` with no fallback. Validated **before Phase 1**: a required service
|
|
1396
|
+
whose only declared provider initializes later fails the boot with an error
|
|
1397
|
+
naming both plugins, both slots, and the fix — before any init side
|
|
1398
|
+
effects. Re-checked immediately before the plugin's own init, where a still-
|
|
1399
|
+
missing service becomes a named composition error exactly where the old
|
|
1400
|
+
bare `Service not found` crash fired.
|
|
1401
|
+
- **`providesServices: string[]`** — services a plugin's `init()`
|
|
1402
|
+
unconditionally registers; powers the validation and the diagnostics.
|
|
1403
|
+
|
|
1404
|
+
Plugins that declare nothing get the diagnosis too: a `getService` miss
|
|
1405
|
+
during Phase 1 now appends which plugin was initializing and — when a
|
|
1406
|
+
composed plugin declares the service — who provides it and how to declare the
|
|
1407
|
+
ordering. The `Service '<name>' not found` prefix and the factory-backed
|
|
1408
|
+
`is async - use await` message are unchanged.
|
|
1409
|
+
|
|
1410
|
+
First adopters: `AppPlugin` declares
|
|
1411
|
+
`optionalDependencies: ['com.objectstack.engine.objectql']` +
|
|
1412
|
+
`requiresServices: ['manifest']` (cleared on the empty-env no-op path), so
|
|
1413
|
+
the #4085 composition — AppPlugin registered before the engine — now boots
|
|
1414
|
+
correctly in every slot; `ObjectQLPlugin` declares
|
|
1415
|
+
`providesServices: ['objectql', 'data', 'manifest', 'lifecycle']` and
|
|
1416
|
+
`MetadataPlugin` declares `providesServices: ['metadata']`.
|
|
1417
|
+
|
|
1418
|
+
Everything is additive — plugins that declare nothing keep their exact
|
|
1419
|
+
ordering semantics; no existing declaration changes meaning.
|
|
1420
|
+
|
|
1421
|
+
- 28d1eb7: fix(core): the QA `contains` assertion fails loudly instead of silently passing on a non-array/non-string actual (#7256)
|
|
1422
|
+
|
|
1423
|
+
`TestRunner.assert`'s `case 'contains':` handled the two shapes it can evaluate —
|
|
1424
|
+
an array (membership) and a string (substring) — and had **no `else`**. Every
|
|
1425
|
+
other shape fell straight out of the switch throwing nothing, so the assertion
|
|
1426
|
+
reported **PASSED**. A scenario asserting
|
|
1427
|
+
`{ field: "body.data.items", operator: "contains", expectedValue: "acme" }`
|
|
1428
|
+
against a response that has no `body.data.items` at all reported ✅. The
|
|
1429
|
+
overwhelmingly common way to reach that branch is the one that matters most: a
|
|
1430
|
+
typo'd `field` path, or a response shape that moved under a suite nobody
|
|
1431
|
+
re-read. The assertion that was supposed to _be_ the test is the thing that
|
|
1432
|
+
silently disappears, and CI believes the green.
|
|
1433
|
+
|
|
1434
|
+
`contains` was the only path in this engine that could decide "no comparison
|
|
1435
|
+
applies here" and report success. Every other unhandled shape already fails
|
|
1436
|
+
loud — an operator with no branch throws `Unknown assertion operator`, an action
|
|
1437
|
+
type with no adapter branch throws `Unsupported action type in HttpAdapter`,
|
|
1438
|
+
and `equals`/`not_equals`/`is_null`/`not_null` all compare unconditionally. This
|
|
1439
|
+
closes the asymmetry rather than adding a new posture: an assertion the engine
|
|
1440
|
+
**cannot evaluate** is a **failed** assertion.
|
|
1441
|
+
|
|
1442
|
+
The message is written for the author who has to act on it, so it names the
|
|
1443
|
+
field, the operator and the runtime type of what the path actually resolved to
|
|
1444
|
+
(`null` and arrays get their own names, not `typeof`'s `object`), and then says
|
|
1445
|
+
which of the two things is wrong:
|
|
1446
|
+
|
|
1447
|
+
```
|
|
1448
|
+
Assertion failed: body.data.items cannot be evaluated by 'contains' — expected an
|
|
1449
|
+
array or a string at that path, got undefined. The path resolved to nothing — the
|
|
1450
|
+
field is absent from the result, or the path is misspelled. Use 'is_null' if
|
|
1451
|
+
asserting absence is what you meant.
|
|
1452
|
+
```
|
|
1453
|
+
|
|
1454
|
+
`undefined`/`null` point at the **fixture** (the path did not resolve, so the
|
|
1455
|
+
field path or the response shape it was written against is the suspect);
|
|
1456
|
+
a number, boolean or object points at the **assertion** (the path resolved
|
|
1457
|
+
fine and `contains` is the wrong operator for what it found).
|
|
1458
|
+
|
|
1459
|
+
**Behaviour change, and its measured blast radius.** Suites that today pass a
|
|
1460
|
+
`contains` against a non-array/non-string will start failing — which is the
|
|
1461
|
+
point; each such assertion was asserting nothing. The in-tree radius was
|
|
1462
|
+
measured on the loud build and is **zero**: `os test` is the runner's only
|
|
1463
|
+
consumer, and the repository contains no Quality Protocol suite documents at
|
|
1464
|
+
all (no `qa/*.test.json` anywhere; the three example apps run `vitest`, and
|
|
1465
|
+
`packages/qa/*` are vitest suites that never touch `TestRunner`). No CI workflow
|
|
1466
|
+
invokes `os test`. So no in-repo case was passing vacuously and none needed
|
|
1467
|
+
repair. Downstream suites are the ones that will see red, and every case they
|
|
1468
|
+
see is a test that was never running.
|
|
1469
|
+
|
|
1470
|
+
The two evaluable shapes are untouched in both directions: a matching array or
|
|
1471
|
+
string still passes, a non-matching one still fails with its existing message.
|
|
1472
|
+
`not_contains`, `gt`, `gte`, `lt`, `lte` and `error` are declared in
|
|
1473
|
+
`TestAssertionTypeSchema` and still have no branch in the runner — they were
|
|
1474
|
+
already refused loudly at `default:` rather than silently passed, so they do not
|
|
1475
|
+
carry this defect; that gap is recorded separately and is pinned here so a later
|
|
1476
|
+
implementation is a deliberate change rather than an accident.
|
|
1477
|
+
|
|
1478
|
+
- 1363084: feat(spec,objectql): `engine.transaction` 契约收紧第一批 —— `opts.require` fail-closed 与 `owned` 信号 (#5696)
|
|
1479
|
+
|
|
1480
|
+
`IObjectQLEngine.transaction` 的声明面(`packages/spec/src/contracts/objectql-engine.ts`,
|
|
1481
|
+
ADR-0119 D1)此前把「默认驱动之外的对象写在事务外」与「驱动没有 `beginTransaction`
|
|
1482
|
+
时静默降级」写成**声明语义**的一部分。#4619 把这两条降级变得可观测(PR #5724),本次
|
|
1483
|
+
把其中两条收紧为调用方可选的契约,并同步修订 TSDoc 的事实性偏差。
|
|
1484
|
+
|
|
1485
|
+
**新增(可选,默认行为完全不变):**
|
|
1486
|
+
|
|
1487
|
+
- `transaction(cb, base, { require: true })` —— 驱动没有 `beginTransaction` 时
|
|
1488
|
+
**抛 `TransactionUnsupportedError`(`code: 'ERR_TRANSACTION_UNSUPPORTED'`)**,
|
|
1489
|
+
而不是静默降级成「无事务、无回滚」。在回调运行**之前**拒绝,所以调用方收到错误时
|
|
1490
|
+
一行都还没写。这是把 `batchData` 的 atomic 门(ADR-0119 D4)泛化成通用能力:
|
|
1491
|
+
只为「开事务的唯一理由就是回滚」的调用方而设,不传 `require` 的行为一字未变
|
|
1492
|
+
(仍然降级 + warn-once)。
|
|
1493
|
+
- 回调的**第二个参数** `{ owned: boolean }` —— `true` 表示本次调用开启了事务并拥有
|
|
1494
|
+
提交/回滚,`false` 表示它 **join** 了外层已开的 ambient 事务(ADR-0067 D2),
|
|
1495
|
+
或者处在降级路径上(那里根本没有事务可拥有)。join 语义本身正确且保留;缺的是
|
|
1496
|
+
调用方**无从分辨**,而「整体一起回滚」这类担保只在 owned 时成立。单参数回调不受影响。
|
|
1497
|
+
|
|
1498
|
+
两点在 `ctx.api.transaction`(`ScopedContext.transaction`,沙箱 hook/action 体)上
|
|
1499
|
+
同样生效 —— 同一个原语的第二份实现不该变成第二种方言。
|
|
1500
|
+
|
|
1501
|
+
**契约文本修订:** transaction 的 TSDoc 原先写「路由到别处的对象在事务**外**写入」,
|
|
1502
|
+
实测不符 —— 引擎无条件把 ambient 事务句柄穿给了目标驱动,语句在**错误的连接**上执行
|
|
1503
|
+
(#5351 在真 SQL driver 上实测为 `no such table`)。TSDoc 已按实测改写,并声明了随后
|
|
1504
|
+
落地的两条语义:业务写跨驱动**响亮拒绝**、系统账本(`lifecycle.class` 为
|
|
1505
|
+
`audit`/`telemetry`/`event`)**移出事务执行**。
|
|
1506
|
+
|
|
1507
|
+
**类型面:** `@objectstack/core` 的 `EngineWithTransaction` 从「手抄签名」改为
|
|
1508
|
+
`transaction: IObjectQLEngine['transaction']`,窄接口可以窄,但不能与真签名漂移。
|
|
1509
|
+
新导出 `EngineTransactionOptions` / `EngineTransactionInfo`(spec `contracts` 命名空间,
|
|
1510
|
+
经 `@objectstack/core` 转出)。
|
|
1511
|
+
|
|
1512
|
+
升级须知:无破坏性变更。既有调用点全部保持原行为;要 fail-closed 的调用方显式传
|
|
1513
|
+
`{ require: true }`。
|
|
1514
|
+
|
|
1515
|
+
- e4c2dc8: Order temporal operands correctly when one side is a JS `Date` on the two
|
|
1516
|
+
type-blind filter backends (ADR-0053 D-A3 / #4191).
|
|
1517
|
+
|
|
1518
|
+
`utcInstantMs` joins `nextUtcCalendarDay` in `@objectstack/spec/data`
|
|
1519
|
+
(re-exported from `@objectstack/core`): it reads the UTC instant a temporal
|
|
1520
|
+
operand denotes, accepting only unambiguous spellings — a `Date`, epoch ms, a
|
|
1521
|
+
bare `YYYY-MM-DD`, and an ISO timestamp with or without an explicit zone (a
|
|
1522
|
+
zone-naive one being UTC, per D-B2) — and returning `null` for everything
|
|
1523
|
+
else, notably a bare wall clock, which denotes no instant.
|
|
1524
|
+
|
|
1525
|
+
Both type-blind evaluators now use it to compare a `Date` against wire text,
|
|
1526
|
+
which JS relational operators cannot do: `<` and friends coerce with hint
|
|
1527
|
+
`number`, so the `Date` becomes its epoch and the string becomes `NaN`.
|
|
1528
|
+
|
|
1529
|
+
- `formula`'s `matchesFilterCondition` (the RLS write-side `check`) dropped
|
|
1530
|
+
every `Date`-valued row in 10 of the 16 shared conformance cases. The
|
|
1531
|
+
post-image is the caller's raw write payload, so an SDK write of
|
|
1532
|
+
`new Date()` hit this directly, and fail-closed turned it into a **denied
|
|
1533
|
+
write**.
|
|
1534
|
+
- `service-analytics`' preview evaluator diverged on the same 10 cases in
|
|
1535
|
+
BOTH directions, because `String(new Date())` sorts after every `'2026-…'`
|
|
1536
|
+
comparand — a drafted chart both lost rows and gained ones, then changed
|
|
1537
|
+
its numbers at publish. Rows from a mongo-backed dataset arrive as BSON
|
|
1538
|
+
`Date`s, so this was reachable in normal use.
|
|
1539
|
+
|
|
1540
|
+
Comparisons that did not involve a `Date` are unchanged.
|
|
1541
|
+
|
|
1542
|
+
### Patch Changes
|
|
1543
|
+
|
|
1544
|
+
- 690ccf2: fix(objectql): a by-id `update()`/`delete()` against a nonexistent record answers 404 `RECORD_NOT_FOUND` instead of a 400 from further down the pipeline (#7867)
|
|
1545
|
+
|
|
1546
|
+
Nothing on the action-body write path ever asked whether the target row existed.
|
|
1547
|
+
`ctx.api.object(name).update({ id, … })` reached `ObjectQL.update()`'s by-id
|
|
1548
|
+
branch through `buildSandboxApi` → `ObjectRepository`, and that branch had **no
|
|
1549
|
+
existence gate at all**: `engine.update()` on a ghost id was a silent no-op that
|
|
1550
|
+
resolved `null`, so the write ran on into validation, the driver and the hook
|
|
1551
|
+
chain and died on whichever complained first.
|
|
1552
|
+
|
|
1553
|
+
**Which one it died on varied with the object's declarations**, which is why the
|
|
1554
|
+
defect read as several unrelated bugs:
|
|
1555
|
+
|
|
1556
|
+
- a **hooked** object → `400` `HookConditionError`, from an `afterUpdate`
|
|
1557
|
+
condition reading `previous` on a row nobody read;
|
|
1558
|
+
- an **unhooked** object → `400` `VALIDATION_FAILED` "X is required", because
|
|
1559
|
+
with no prior row a PATCH is validated as if it were a whole record.
|
|
1560
|
+
|
|
1561
|
+
The 400 class varied; the missing 404 was the constant. Measured on one showcase
|
|
1562
|
+
stack, same id, same object, same second: `POST /actions/showcase_task/
|
|
1563
|
+
showcase_mark_done/<ghost>` answered 400 while `PATCH /data/showcase_task/
|
|
1564
|
+
<ghost>` answered 404. Both answer **404 `RECORD_NOT_FOUND`** now.
|
|
1565
|
+
|
|
1566
|
+
`delete()` had the same shape and was the worse of the two: with no gate it
|
|
1567
|
+
reported success for a row that was never there, so a typo'd id, an
|
|
1568
|
+
already-deleted row and a real deletion were indistinguishable.
|
|
1569
|
+
|
|
1570
|
+
**This is not a `previous`-binding bug.** `if (priorRecord) hookContext.previous
|
|
1571
|
+
= …` is correct and is untouched — ADR-0058 Addendum II / #4649 require that an
|
|
1572
|
+
absent row leave `previous` UNBOUND rather than fabricated. It was behaving
|
|
1573
|
+
correctly on a path that should never have been entered, so the fix removes the
|
|
1574
|
+
producer rather than specializing what it produced.
|
|
1575
|
+
|
|
1576
|
+
**Where the gate went, and why there.** At the engine, in the by-id branches of
|
|
1577
|
+
`update()` and `delete()` — the one point all three action-body write faces
|
|
1578
|
+
funnel through (`ctx.api.object()`, its context-less repo-facade fallback, and
|
|
1579
|
+
`ctx.engine.update()`). A repository-level gate would have closed one of the
|
|
1580
|
+
three and made `ql.update(o, { id })` and `ctx.api.object(o).update({ id })`
|
|
1581
|
+
answer one ghost id two different ways. Two sibling paths already gated
|
|
1582
|
+
correctly — `protocol.updateData`/`deleteData` (#4435) and `callData`'s ObjectQL
|
|
1583
|
+
fallback (#5138) — and all three now throw the **same** `recordNotFoundError`,
|
|
1584
|
+
which moved to `@objectstack/core` so the engine can reach it without importing
|
|
1585
|
+
`@objectstack/metadata-protocol` (forbidden in the `/core` closure by ADR-0076
|
|
1586
|
+
D2's boundary ratchet). `@objectstack/metadata-protocol` re-exports it unchanged.
|
|
1587
|
+
|
|
1588
|
+
Existence is asked with a pre-write read, never off the write's own result:
|
|
1589
|
+
`IDataDriver.update` declares no not-found signal, and the engine's post-write
|
|
1590
|
+
readback is `null` for a second reason (a write that moves the row out of the
|
|
1591
|
+
caller's row scope), so reading either would answer 404 to a write that landed.
|
|
1592
|
+
|
|
1593
|
+
**Behaviour change worth knowing about — the by-id prior-row read is now
|
|
1594
|
+
unconditional.** #5284 (update) and #5929 (delete) had narrowed it to "does
|
|
1595
|
+
anything CONSUME the prior row?", skipping the read for objects with no hook, no
|
|
1596
|
+
prior-reading validation rule and no roll-up. Existence is a consumer that
|
|
1597
|
+
demand list never enumerated and the one consumer every by-id write has, and no
|
|
1598
|
+
cheaper question answers it — so the skip and the gate are mutually exclusive.
|
|
1599
|
+
The measured cost is small: #5929's own record enumerates the global hook
|
|
1600
|
+
registrants (plugin-sharing, service-storage, plugin-auth, plugin-audit), so on
|
|
1601
|
+
any kernel that loads them the demand was already true for every object and the
|
|
1602
|
+
narrowing skipped nothing. The read is genuinely new only for a bare
|
|
1603
|
+
`@objectstack/objectql/core` embedder — which is buying a 404 it did not have.
|
|
1604
|
+
|
|
1605
|
+
Three read-count pins measured the old skip and now measure the read, each
|
|
1606
|
+
recording what changed and why at its own site: #5284's and #5929's in
|
|
1607
|
+
`packages/objectql`, and #5860's `sys_job_queue` case in `@objectstack/plugin-audit`.
|
|
1608
|
+
The DISPATCH half all three are actually about — the per-object `hasHooksFor`
|
|
1609
|
+
question, the `excludeObjects` subtraction, and the retired
|
|
1610
|
+
`sys_fetch_previous_*` builtins — is untouched and still pinned.
|
|
1611
|
+
|
|
1612
|
+
One further case encoded the old silent no-op as correct: `@objectstack/plugin-auth`'s
|
|
1613
|
+
#5941 last-admin-guard test deleted a `sys_account` id that was never seeded and
|
|
1614
|
+
asserted it RESOLVED, to show the guard does not write-guard that object. It now
|
|
1615
|
+
deletes a REAL row — which states the same thing more strongly — and separately
|
|
1616
|
+
pins that a ghost id there is refused by the ENGINE rather than by the guard.
|
|
1617
|
+
|
|
1618
|
+
**Scope.** By-id only. A `multi: true` predicate write matching zero rows still
|
|
1619
|
+
resolves "0 rows affected" — the same line both sibling paths draw.
|
|
1620
|
+
|
|
1621
|
+
`@objectstack/runtime`: the sandbox error passthrough now also carries `status`
|
|
1622
|
+
alongside `code` and `fields`, so an error that names its own HTTP status keeps
|
|
1623
|
+
it across the QuickJS boundary. Without it the action surface answered the right
|
|
1624
|
+
diagnosis at the wrong status (`{ code: 'RECORD_NOT_FOUND', httpStatus: 400 }`);
|
|
1625
|
+
`domains/actions.ts` already honoured `.status` first — the number simply never
|
|
1626
|
+
arrived. A permission refusal thrown inside a body likewise keeps its 403 now
|
|
1627
|
+
instead of flattening to 400.
|
|
1628
|
+
|
|
1629
|
+
- 2af1988: fix(formula,spec,core): the RLS write-side `check` evaluator honours calendar-day upper bounds (ADR-0053 D-D)
|
|
1630
|
+
|
|
1631
|
+
`@objectstack/formula`'s `matchesFilterCondition` — the evaluator behind RLS
|
|
1632
|
+
write-side `check` policies (ADR-0058 D4) — compared a bare `YYYY-MM-DD` `$lte`
|
|
1633
|
+
bound literally. On a `datetime` post-image that meant a policy of the shape
|
|
1634
|
+
`{ signed_on: { $lte: '{today}' } }` **denied every write made after 00:00**:
|
|
1635
|
+
the write-side twin of the read-side data loss #3777 fixed, and the last of the
|
|
1636
|
+
platform's filter backends that disagreed about what a bare day means as a
|
|
1637
|
+
bound.
|
|
1638
|
+
|
|
1639
|
+
`$lte` and a `$between` max now evaluate half-open against the next calendar
|
|
1640
|
+
day, matching the SQL compiler, the memory and mongo drivers, and the analytics
|
|
1641
|
+
preview evaluator. Unchanged, per the same semantics table: full-ISO bounds keep
|
|
1642
|
+
exact-instant semantics, `$gte`/`$gt`/`$lt` keep their midnight anchoring, and a
|
|
1643
|
+
plain `YYYY-MM-DD` value compares identically (string ordering makes the two
|
|
1644
|
+
forms equivalent). The evaluator stays fail-closed on a null bound.
|
|
1645
|
+
|
|
1646
|
+
**Where the rule now lives.** `nextUtcCalendarDay` moved from
|
|
1647
|
+
`@objectstack/core` to `@objectstack/spec/data` — beside `date-macros.zod.ts`,
|
|
1648
|
+
whose vocabulary it interprets. `formula` cannot depend on `core`, and a second
|
|
1649
|
+
copy of the rule is exactly the divergence #3777 catalogued; `spec` is the one
|
|
1650
|
+
package all six consumers already depend on, so this adds no dependency edge.
|
|
1651
|
+
|
|
1652
|
+
No import changes are required: `@objectstack/core` re-exports the symbol, so
|
|
1653
|
+
existing `import { nextUtcCalendarDay } from '@objectstack/core'` keeps working.
|
|
1654
|
+
New code should prefer `@objectstack/spec/data`.
|
|
1655
|
+
|
|
1656
|
+
- 2e836de: chore(packaging): CHANGELOG.md ships in every npm tarball (#4261)
|
|
1657
|
+
|
|
1658
|
+
The AGENTS.md post-task checklist requires breaking changesets to carry their
|
|
1659
|
+
FROM → TO migration because "this text ships to consumers as `CHANGELOG.md`
|
|
1660
|
+
inside the npm package and is what an upgrading agent greps after the tombstone
|
|
1661
|
+
error." That delivery path was severed for 68 of the 69 publishable packages:
|
|
1662
|
+
npm packs `package.json` / `README*` / `LICENSE*` unconditionally but — unlike
|
|
1663
|
+
older npm versions — not `CHANGELOG.md`, and the canonical
|
|
1664
|
+
`"files": ["dist", "README.md"]` whitelist never named it. Measured on npm
|
|
1665
|
+
10.9.7: `npm pack --dry-run` on `@objectstack/types` shipped 3 files while its
|
|
1666
|
+
70KB `CHANGELOG.md` stayed behind. Only `@objectstack/spec` listed it
|
|
1667
|
+
explicitly.
|
|
1668
|
+
|
|
1669
|
+
The tombstone-error scenario is precisely the one where the repo is out of
|
|
1670
|
+
reach — the upgrading agent has `node_modules` and nothing else — so the
|
|
1671
|
+
migration text has to ride in the tarball. Every publishable package now
|
|
1672
|
+
declares `CHANGELOG.md` in `files`, and the canonical whitelist is
|
|
1673
|
+
`["dist", "README.md", "CHANGELOG.md"]`.
|
|
1674
|
+
|
|
1675
|
+
The other half is the gate: `check:published-files` gains a fifth invariant,
|
|
1676
|
+
COMPLETE — a whitelist that fails to cover `CHANGELOG.md` fails the
|
|
1677
|
+
always-required lint job, so the next package cannot silently sever the path
|
|
1678
|
+
again. `@objectstack/spec`'s per-package EXTRA_ENTRIES exemption dissolves
|
|
1679
|
+
into the canonical set.
|
|
1680
|
+
|
|
1681
|
+
Consumer-visible change: one more file per install (the package's changelog,
|
|
1682
|
+
e.g. 70.8KB for `@objectstack/types`), and `grep -r "removed key"
|
|
1683
|
+
node_modules/@objectstack/*/CHANGELOG.md` now finds the migration it was
|
|
1684
|
+
promised.
|
|
1685
|
+
|
|
1686
|
+
- b746aa0: fix(service-automation): connector 物化失败的软路径改用结构化 `meta`;顺带修好 `ObjectLogger.error` 丢弃契约第三参的缺陷 (#5575)
|
|
1687
|
+
|
|
1688
|
+
## service-automation:`fail(msg, cause)`
|
|
1689
|
+
|
|
1690
|
+
`reconcileDeclaredConnectors` 的报错器有两条路径(ADR-0097):冷启动 `throw`(fatal),
|
|
1691
|
+
`metadata:reloaded` 之后 —— Studio publish、`os dev` 重编译 —— 记日志并让旧 connector
|
|
1692
|
+
继续服务(soft)。其中两个调用点把**外来**的 `err.message` 插进那条日志 message:
|
|
1693
|
+
`resolveInstanceAuth` 失败处,以及 provider factory 抛错处。这两个 message 都不是我们
|
|
1694
|
+
自己的:credential resolver 由宿主提供
|
|
1695
|
+
(`AutomationServicePluginOptions.credentialResolver`),provider factory 更是 ADR-0097
|
|
1696
|
+
明确鼓励第三方去写的代码 —— 第一个用严格 Zod schema 校验 `providerConfig` 的 factory
|
|
1697
|
+
抛出的就是 `ZodError`,它的 `.message` 是 issue 数组的多行 JSON dump,第一行是一个 `[`。
|
|
1698
|
+
|
|
1699
|
+
`ObjectLogger` 每次调用只写一条 `<ts> <LEVEL> <msg>` 记录,带换行的 message 会溢出到
|
|
1700
|
+
不带等级头的后续物理行,于是运行时 stderr 的每一个按行工作的消费者 —— 文件 sink、
|
|
1701
|
+
`docker logs`/journald 送进日志采集、一次 `grep ERROR` —— 都会把那些续行读成无法归属的
|
|
1702
|
+
垃圾记录:一条诊断散成 N 个碎片。与 #5048 在 flow 绑定接缝上是同一类,也是同一条 #4632
|
|
1703
|
+
原则:被搅烂的诊断比没有诊断更贵。
|
|
1704
|
+
|
|
1705
|
+
改法与 PR #5572 同源:`fail(msg, cause?)` —— message 是不含换行的自足句子,cause 按路径
|
|
1706
|
+
分别渲染。soft 路径把 cause 交给 logger 的**结构化 meta**(`issues[]` / `error`);fatal
|
|
1707
|
+
路径把 cause 文本接在抛出的 message 后面(`… cause: <text>`),因为 throw 不是日志记录,
|
|
1708
|
+
内核失败通道原样打印,多行 ZodError dump 在终端里本来就好读 —— 同一个 cause,两种受众,
|
|
1709
|
+
刻意不共用一种形状。`#5048` 引入的内部模块随之从 `flow-bind-diagnostics.ts` 更名为
|
|
1710
|
+
`thrown-cause-diagnostics.ts`(`describeThrownForLog`),因为它从来不是 flow 专属的:
|
|
1711
|
+
主题是日志管线,不是 metadata 类型。被拒键名仍放在 `unrecognized` 而不是 Zod 原本的
|
|
1712
|
+
`keys`(`ObjectLogger` 的脱敏表按子串匹配,`keys` 含 `key`)。
|
|
1713
|
+
|
|
1714
|
+
**一处订正**:#5575 的 issue 正文把此处的危害归给了 `serve` 的启动诊断缓冲
|
|
1715
|
+
(`BootLogCapture`)。那个缓冲看不到这条路径 —— `ObjectLogger` 把 `warn` 送 stdout(启动
|
|
1716
|
+
静默窗口只包了 `process.stdout.write`),`error`/`fatal` 送 **stderr**,而且 soft 路径在
|
|
1717
|
+
`metadata:reloaded` 之后才跑,窗口早已恢复。危害是上面那串按行消费者,以及日志查询根本
|
|
1718
|
+
无法按字段过滤;机制写进了模块文档,连同 `warn`/`error` 下游不同这件事本身。
|
|
1719
|
+
|
|
1720
|
+
## core:`ObjectLogger.error`/`fatal` 兑现契约声明的 `meta`
|
|
1721
|
+
|
|
1722
|
+
`Logger` 契约声明 `error(message, error?: Error, meta?)`。`ObjectLogger` 按形状分派,
|
|
1723
|
+
所以 meta 也允许出现在 `error` 位 —— 这份宽容没问题;**丢掉一个自己声明的参数**有问题:
|
|
1724
|
+
`error === undefined` 时旧代码走 `write(level, message, errorOrMeta)`,第三个参数从未被
|
|
1725
|
+
读取。于是每一个按契约书写的 `logger.error(msg, undefined, { … })` 都只输出一条裸 message,
|
|
1726
|
+
事实全部静默消失 —— `metadata`、`metadata-protocol`、`client`、`core/security` 里约 15 处
|
|
1727
|
+
调用点今天就是这样(其中 `metadata/src/endpoint-matcher.ts` 送的正是一个 Zod issue 数组)。
|
|
1728
|
+
契约的另外两个实现(`@objectstack/observability` 的 `ConsoleLogger`/`JsonLogger`)都老老实实
|
|
1729
|
+
用了这个位置,所以是契约对、这一个实现错:declared ≠ enforced。
|
|
1730
|
+
|
|
1731
|
+
三种形状现在都被兑现,两个位置同时带值时以更靠后的 `meta` 为准。这一处修好之后,上述
|
|
1732
|
+
调用点的诊断自动恢复(`client` 的 `HTTP request failed` 记录重新带上
|
|
1733
|
+
`{method, url, status, error}`)。connector 接缝改用契约的第三参而非第二参,是刻意的:
|
|
1734
|
+
把原始 error 塞进第二位会让每条记录都附带完整堆栈,ZodError 还会附带整段多行 dump ——
|
|
1735
|
+
正是我们要消灭的无界形状。
|
|
1736
|
+
|
|
1737
|
+
- a227ed7: fix(objectql)!: one key for the empty group bucket — real `null`, on both aggregation paths (#3839)
|
|
1738
|
+
|
|
1739
|
+
A grouped row whose dimension value is empty now carries `null` for that
|
|
1740
|
+
dimension no matter which way the aggregate ran. Downstream code can test the
|
|
1741
|
+
empty bucket with a plain `value == null` again: charts render their own empty
|
|
1742
|
+
label, drill-through on that bucket builds `field = null` and returns the rows
|
|
1743
|
+
it should, and a dashboard no longer changes shape when the driver, the
|
|
1744
|
+
granularity or the reference timezone changes.
|
|
1745
|
+
|
|
1746
|
+
### What was wrong
|
|
1747
|
+
|
|
1748
|
+
`engine.aggregate` has two implementations of one feature. It pushes the
|
|
1749
|
+
aggregate down as SQL when the driver advertises every requested granularity and
|
|
1750
|
+
the reference timezone is UTC; otherwise it fetches rows and buckets them in JS.
|
|
1751
|
+
The two disagreed about how to spell "empty":
|
|
1752
|
+
|
|
1753
|
+
```
|
|
1754
|
+
--- same dataset, same query, one row with a NULL value ---
|
|
1755
|
+
pushed-down SQL : [{ "key": null, "type": "null", "total": 2 }, …]
|
|
1756
|
+
in-memory : [{ "key": "(null)", "type": "string", "total": 2 }, …]
|
|
1757
|
+
```
|
|
1758
|
+
|
|
1759
|
+
The measures were always right — only the key's type and literal differed —
|
|
1760
|
+
which is why this went unnoticed for so long: every total reconciled. But the
|
|
1761
|
+
engine picks a path per query, so the same data produced a different bucket key
|
|
1762
|
+
on SQLite-plus-UTC-plus-`month` than on `week` (which SQLite does not advertise),
|
|
1763
|
+
a non-UTC timezone, or `driver-rest` / `driver-memory` / a remote Turso, all of
|
|
1764
|
+
which bucket in memory unconditionally.
|
|
1765
|
+
|
|
1766
|
+
It was never date-specific either. A plain `groupBy: ['stage']` over a NULL
|
|
1767
|
+
column diverged the same way.
|
|
1768
|
+
|
|
1769
|
+
Consumers are written against `null` — they check `== null` and supply their own
|
|
1770
|
+
empty label ('—', '(empty)', a localized "Uncategorized"). The sentinel defeated
|
|
1771
|
+
every one of them: it rendered a raw English debug string in the UI, and a drill
|
|
1772
|
+
on the empty bucket compiled to `field = '(null)'` and matched nothing.
|
|
1773
|
+
|
|
1774
|
+
The in-memory path's comment justified the string as staying "consistent with
|
|
1775
|
+
the client `useReportData` hook". That hook was removed with ADR-0021, and the
|
|
1776
|
+
literal never appeared in it.
|
|
1777
|
+
|
|
1778
|
+
### What changed
|
|
1779
|
+
|
|
1780
|
+
- `applyInMemoryAggregation` and `bucketDateValue` (`@objectstack/objectql`) key
|
|
1781
|
+
the empty bucket as `null`. `bucketDateValue` now returns `string | null`. A
|
|
1782
|
+
null instant and an unparseable one still share one bucket, because SQL cannot
|
|
1783
|
+
tell them apart either (`strftime('%Y-%m', 'not-a-date')` is NULL).
|
|
1784
|
+
- The internal composite bucket id is JSON-encoded, so the empty bucket stays
|
|
1785
|
+
distinct from a row whose value is the literal string `"null"`.
|
|
1786
|
+
- `bucketKeyToCalendarRange` (`@objectstack/core`) accepts `string | null`. The
|
|
1787
|
+
empty bucket has no calendar span, so a drill on it opens the unscoped
|
|
1788
|
+
superset instead of an invented bound — unchanged behavior, honest signature.
|
|
1789
|
+
- The driver output contract in `@objectstack/spec` now states the rule: a row
|
|
1790
|
+
with no value keys as `null`, never a sentinel. Propagating NULL through the
|
|
1791
|
+
bucket expression is the whole of it; a driver only breaks it by adding a
|
|
1792
|
+
`COALESCE`.
|
|
1793
|
+
|
|
1794
|
+
### Gates
|
|
1795
|
+
|
|
1796
|
+
`checkDateBucketParity` (`@objectstack/verify`) deliberately carried no null
|
|
1797
|
+
instant, because the divergence would have failed it for a reason it was not
|
|
1798
|
+
about. Its fixture now has one, so the convergence is held in place — including
|
|
1799
|
+
for out-of-tree drivers that run the check against themselves.
|
|
1800
|
+
|
|
1801
|
+
Two fixes were needed to make that fixture meaningful:
|
|
1802
|
+
|
|
1803
|
+
- The check folded bucket labels through `String(value)`, which turns SQL NULL
|
|
1804
|
+
into `'null'` — a label a TEXT column can genuinely hold. A driver spelling
|
|
1805
|
+
"empty" as a string could compare equal to one returning real NULL. The empty
|
|
1806
|
+
bucket is now keyed out of band.
|
|
1807
|
+
- Label sets were compared with `JSON.stringify`, which is sensitive to key
|
|
1808
|
+
insertion order. Row order is not part of this contract and the two paths
|
|
1809
|
+
naturally differ (SQL sorts its groups; the in-memory path emits first-seen
|
|
1810
|
+
order), so a driver with entirely correct buckets could be reported as
|
|
1811
|
+
disagreeing — with an empty diff message, since nothing actually differed.
|
|
1812
|
+
The comparison is now order-insensitive.
|
|
1813
|
+
|
|
1814
|
+
A new dogfood check covers the non-date half against real drivers: same dataset,
|
|
1815
|
+
plain and date-bucketed `groupBy`, both paths, one key.
|
|
1816
|
+
|
|
1817
|
+
- b127c8b: fix(spec,core): a filter placeholder is recognised by INTENT — `{TODAY()}` refuses loudly instead of comparing as a literal (#5586)
|
|
1818
|
+
|
|
1819
|
+
`UnknownFilterTokenError` had a hole exactly where authors fall in. Recognition
|
|
1820
|
+
used the token-NAME grammar `/^\$?\{([a-zA-Z0-9_]+)\}$/`, so any placeholder
|
|
1821
|
+
carrying a **non-word character** classified as "not a placeholder at all" and
|
|
1822
|
+
was handed to the driver verbatim, to be compared as a literal string — the
|
|
1823
|
+
silent-wrong-result failure the diagnostic exists to abolish.
|
|
1824
|
+
|
|
1825
|
+
The failure was inverted against the author. Measured on 17.0.0-rc.2 against a
|
|
1826
|
+
four-row fixture:
|
|
1827
|
+
|
|
1828
|
+
| filter value | before | |
|
|
1829
|
+
| ------------------------ | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
1830
|
+
| `due_date < '{today}'` | 2 rows | correct — the two overdue rows |
|
|
1831
|
+
| `due_date < '{TODAY}'` | throws `UnknownFilterTokenError` | diagnostic working |
|
|
1832
|
+
| `due_date < '{TODAY()}'` | **4 rows** | diagnostic bypassed — literal string compare, and `'2026-…' < '{'` in lexicographic order swallowed a row due a week later |
|
|
1833
|
+
|
|
1834
|
+
So misspelling `{today}` as `{TODAY}` was reported by name, while misspelling it
|
|
1835
|
+
as `{TODAY()}` returned the wrong rows in silence — and the parenthesised,
|
|
1836
|
+
kebab-case, natural-language and dotted spellings (`{TODAY()}`,
|
|
1837
|
+
`{current-user-id}`, `{30 days ago}`, `{user.id}`) are precisely what an author
|
|
1838
|
+
migrating from another system's macro syntax writes first.
|
|
1839
|
+
|
|
1840
|
+
**Both directions of the behaviour change:**
|
|
1841
|
+
|
|
1842
|
+
- **Previously silent, now refuses loudly** — a filter value that is entirely
|
|
1843
|
+
brace-wrapped and outside the vocabulary now throws `UnknownFilterTokenError`
|
|
1844
|
+
(`code: FILTER_TOKEN_UNKNOWN`, `status: 400`) on the ObjectQL read and write
|
|
1845
|
+
paths and the analytics dataset executor, and is reported as
|
|
1846
|
+
`filter-token-unknown` by `objectstack build` / `validate` / `lint`. Before,
|
|
1847
|
+
it reached the data engine and compared as text.
|
|
1848
|
+
- **Unchanged** — `{today}` / `{current_user_id}` still resolve; `{TODAY}` still
|
|
1849
|
+
refuses with the same identity; a value that merely _contains_ braces
|
|
1850
|
+
(`'acme {x} deal'`), or is not ONE pair around the whole value (`{a}{b}`,
|
|
1851
|
+
`{{x}}`, `{}`), is still an ordinary literal and still reaches the driver
|
|
1852
|
+
untouched.
|
|
1853
|
+
|
|
1854
|
+
Recognition and vocabulary are now two named grammars rather than one:
|
|
1855
|
+
`FILTER_TOKEN_WRAPPED_RE` (`/^\$?\{([^{}]+)\}$/`) answers "did the author mean a
|
|
1856
|
+
placeholder", and `isContextToken` / `isDateMacroToken` answer "is it in the
|
|
1857
|
+
vocabulary". Wide in, strict out. No escape hatch for a literal `{…}` comparand
|
|
1858
|
+
ships with this: a repo-wide measurement across structured metadata, examples,
|
|
1859
|
+
seed data and fixtures found zero legitimate consumers comparing a
|
|
1860
|
+
brace-wrapped literal, and an escape syntax is a public micro-contract that can
|
|
1861
|
+
be added the day one shows up.
|
|
1862
|
+
|
|
1863
|
+
Flow templates are unaffected. `interpolateFilter` in
|
|
1864
|
+
`@objectstack/service-automation` already recognised the same wide shape and
|
|
1865
|
+
resolves `{record.id}` / `{TODAY() + 30}` from flow variables **before** the
|
|
1866
|
+
filter reaches ObjectQL; its hand-off to the engine is keyed on the token
|
|
1867
|
+
vocabulary (`isKnownFilterToken`), which this change does not touch.
|
|
1868
|
+
|
|
1869
|
+
- eb3e650: fix(core): 健康检查的超时守卫在 race 落定时被清除,周期性检查不再堆积孤儿定时器 (#4875)
|
|
1870
|
+
|
|
1871
|
+
`PluginHealthMonitor.performHealthCheck()` 里那条 race 的守卫由 `timeout()` armed 之后就被
|
|
1872
|
+
扔掉:插件的 `checkMethod` 赢下 race 之后,那根 `setTimeout` 既没 `clearTimeout` 也没
|
|
1873
|
+
`unref()`,带着 ref 一直挂满整个 `config.timeout`。这与 #4813 修掉的两处(内核 init/start
|
|
1874
|
+
守卫,PR #4874)是同一种漏法。
|
|
1875
|
+
|
|
1876
|
+
差别在于**健康检查是周期性的**:内核那两处是启动时一次性的固定份额(4 个插件 = 8 根),这里
|
|
1877
|
+
则是**每个插件每一轮各留一根**,`interval` 越密、`timeout` 越长,堆得越高 —— 一个
|
|
1878
|
+
`interval: 30s` / `timeout: 5s` 的插件在任意时刻都挂着若干根本该在毫秒级就回收的定时器。
|
|
1879
|
+
今天这条还没发作,只是因为 `startMonitoring()` 目前没有被内核启动流程调用;一旦健康监控被接进
|
|
1880
|
+
宿主,它就是 #4813 的放大版。
|
|
1881
|
+
|
|
1882
|
+
修法与 #4874 同形:`timeout()` 换成私有 helper `raceCheckTimeout()`,`try { await
|
|
1883
|
+
Promise.race(...) } finally { clearTimeout(guard) }`。
|
|
1884
|
+
|
|
1885
|
+
**为什么是 `clearTimeout` 而不是 `unref()`。** `unref()` 让定时器不再钉住事件循环的同时,
|
|
1886
|
+
也让它不再是一个守卫 —— 若检查永不 settle 且没有别的东西撑着事件循环,Node 会在定时器触发
|
|
1887
|
+
之前退出,超时被静默吞掉。守卫必须在 race 未决期间保持 ref'd、在落定那一刻被回收,这正是
|
|
1888
|
+
`finally { clearTimeout(guard) }` 表达的语义。回归测试因此是三条:守卫赢不了时不留 ref'd
|
|
1889
|
+
定时器、连跑多轮不累积(fake timers 下计数,能识破 `unref()` 式的假修复)、以及检查真的挂住时
|
|
1890
|
+
超时照常上报。
|
|
1891
|
+
|
|
1892
|
+
超时时长(`config.timeout`)一个都没动 —— 问题从来不在时长,而在没人回收。
|
|
1893
|
+
|
|
1894
|
+
- 45dc446: Every in-memory fallback and dev stub now self-describes with the standard `__serviceInfo` descriptor, classified by what it actually is (#4058 step 1).
|
|
1895
|
+
|
|
1896
|
+
ADR-0076 D12 gave services one way to say "I am not the real thing", but the producers never converged on it:
|
|
1897
|
+
|
|
1898
|
+
- The kernel's own fallbacks (`createMemoryCache` / `Queue` / `Job` / `I18n` / `Metadata`) carried `_fallback: true` — a marker **no** consumer recognized, `readServiceSelfInfo` included — so both discovery builders reported them as fully `available`.
|
|
1899
|
+
- `plugin-dev` marked all of its implementations with the same `_dev: true`, normalized to `status: 'stub', handlerReady: false`. That declared a working in-memory search index exactly as fake as an AI stub returning invented text.
|
|
1900
|
+
|
|
1901
|
+
Both now carry `__serviceInfo`, split by a rule that holds across the whole set:
|
|
1902
|
+
|
|
1903
|
+
- **`degraded`** — really does the work, with reduced capability: `cache`, `queue`, `job`, `file-storage`, `search`, `i18n`, `metadata`, `workflow`, `realtime`. Its answers are true answers; the `message` names what is missing (no persistence, no scheduling timer, no state-machine validation, …).
|
|
1904
|
+
- **`stub`** — the answer is fabricated: `ai`, `automation`, `notification`, `data`, `auth`, `security.permissions`, `security.rls`, `security.fieldMasker`. Never to be mistaken for a capability.
|
|
1905
|
+
|
|
1906
|
+
`handlerReady: false` is set independently wherever no HTTP handler serves the slot (`cache` / `queue` / `job` / `realtime`, and every `stub`).
|
|
1907
|
+
|
|
1908
|
+
Discovery output changes accordingly — a kernel fallback that used to report `status: 'available'` now reports `degraded` with an explanatory message. No routing, gating, or dispatch behavior changes: every dispatcher domain still resolves services exactly as before. Consumers reading `discovery.services.*` get the truth instead of a uniform claim.
|
|
1909
|
+
|
|
1910
|
+
For anything that duck-typed the old markers: `svc._fallback` / `svc._dev` → `readServiceSelfInfo(svc)` from `@objectstack/spec/api` (the legacy `_dev` key is still understood by that reader, so third-party stubs carrying it keep working).
|
|
1911
|
+
|
|
1912
|
+
- f985b3f: fix(spec,core,cloud-connection,metadata): one HTTP contract, one canonical slot name — and the dead shadow copy that helped cause the false exemption is deleted (#4251)
|
|
1913
|
+
|
|
1914
|
+
**`packages/core/src/contracts/` was a dead near-copy of the real contracts,
|
|
1915
|
+
and it is gone.** The directory (http-server.ts, data-engine.ts, logger.ts) had
|
|
1916
|
+
ZERO importers — no relative import, no subpath export, not a tsup entry;
|
|
1917
|
+
core's barrel has re-exported the `@objectstack/spec/contracts` versions all
|
|
1918
|
+
along ("Re-export contracts from @objectstack/spec for backward
|
|
1919
|
+
compatibility"). But the shadow had already **diverged** from the live
|
|
1920
|
+
contract (spec's `IHttpResponse` grew `write?`/`end?` and `IHttpRequest` grew
|
|
1921
|
+
`rawBody?`; the copy never did), so anyone who grepped their way into it read a
|
|
1922
|
+
stale contract that nothing enforces — the exact both-humans-and-AI failure
|
|
1923
|
+
mode behind the false `http.server` exemption (#4382). Deleting it is
|
|
1924
|
+
zero-risk by construction: nothing could reach it.
|
|
1925
|
+
|
|
1926
|
+
**`http.server` is the canonical slot name, and the ledger now says so.**
|
|
1927
|
+
`ServiceSlotContracts` gains `'http.server': IHttpServer` plus the deprecated
|
|
1928
|
+
`'http-server'` alias entry (same instance — hono-plugin and qa's node-plugin
|
|
1929
|
+
register both two lines apart; cloud's two server entrypoints do the same).
|
|
1930
|
+
Canonical is the only name present on EVERY provider path: runtime's
|
|
1931
|
+
`config.server` path registers no alias, so the three cloud-connection plugins
|
|
1932
|
+
that read the alias alone (marketplace-proxy, runtime-config,
|
|
1933
|
+
marketplace-install-local) found an empty slot there — a live miss, now fixed:
|
|
1934
|
+
all readers go canonical-first with the alias as a fallback that dies with the
|
|
1935
|
+
alias registrations. The registrations themselves are untouched this release;
|
|
1936
|
+
both sites now carry the deprecation note.
|
|
1937
|
+
|
|
1938
|
+
**`getRawApp?(): any` joins `IHttpServer`** — the deliberate framework-handle
|
|
1939
|
+
escape, declared once. Four consumers were each declaring it locally
|
|
1940
|
+
(cloud-connection ×2, metadata's HMR routes, cloud's serverless node-server);
|
|
1941
|
+
those local `RawAppHost`/`HttpServerWithRawApp` types are deleted. The `any`
|
|
1942
|
+
return is deliberate and documented at the single declaration: the handle's
|
|
1943
|
+
real type belongs to the framework, and naming it would give the contract a
|
|
1944
|
+
framework dependency. Adapters are not required to expose it; consumers
|
|
1945
|
+
feature-detect.
|
|
1946
|
+
|
|
1947
|
+
**`IMetadataService.bulkRegister`/`bulkUnregister` declare the write options
|
|
1948
|
+
their implementation has always accepted.** `bulkRegister`'s contract options
|
|
1949
|
+
dropped the `MetadataWriteOptions` half its implementation intersects in
|
|
1950
|
+
(`notify` is destructured on the method's first line); `bulkUnregister`
|
|
1951
|
+
declared no options at all while the manager takes them. Same shape as the
|
|
1952
|
+
`IDataEngine` read-methods gap from B2: a caller typed to the contract could
|
|
1953
|
+
not reach the channel without erasing the lookup. Both additive; no implementor
|
|
1954
|
+
or caller breaks.
|
|
1955
|
+
|
|
1956
|
+
Slot-lookup baseline ratchets 168 → 167 (marketplace-install-local's lookup
|
|
1957
|
+
typed while touched).
|
|
1958
|
+
|
|
1959
|
+
- d6d1a50: refactor(core): one implementation per hook-dispatch flavour, plus a paired-pin gate (#5282)
|
|
1960
|
+
|
|
1961
|
+
`ObjectKernel` does not extend `ObjectKernelBase` — it is a standalone
|
|
1962
|
+
production kernel with its own `hooks` map, and only `LiteKernel` extends the
|
|
1963
|
+
base. Lifecycle-hook dispatch therefore existed **twice**, with no shared code
|
|
1964
|
+
path: the base's `triggerHook` (isolating) / `triggerHookOrThrow` (propagating) /
|
|
1965
|
+
`context.trigger` on one side, and `ObjectKernel`'s private
|
|
1966
|
+
`triggerShutdownHookIsolating` / `context.trigger` on the other. The two
|
|
1967
|
+
isolating loops printed the same `Hook handler failed: kernel:shutdown` line
|
|
1968
|
+
because someone typed it twice.
|
|
1969
|
+
|
|
1970
|
+
That seam produced three consecutive bugs, each the same shape — one hook name
|
|
1971
|
+
meaning opposite things on the two kernels: `kernel:ready` (#5170),
|
|
1972
|
+
`kernel:bootstrapped` / `kernel:listening` (#5257, where a swallowed
|
|
1973
|
+
`server.listen()` failure let a process print "✅ Bootstrap complete" with
|
|
1974
|
+
nothing listening), and `kernel:shutdown` in the other direction (#5274, where
|
|
1975
|
+
one bad handler skipped every `destroy()`).
|
|
1976
|
+
|
|
1977
|
+
**No behaviour change.** The two dispatch flavours move verbatim into an
|
|
1978
|
+
internal module, `packages/core/src/hook-dispatch.ts`, which both kernels now
|
|
1979
|
+
call:
|
|
1980
|
+
|
|
1981
|
+
- `dispatchHookIsolating` — a failing handler is logged as
|
|
1982
|
+
`Hook handler failed: <name>` and the remaining handlers still run.
|
|
1983
|
+
- `dispatchHookPropagating` — the first failure escapes unwrapped and the
|
|
1984
|
+
handlers behind it are skipped.
|
|
1985
|
+
|
|
1986
|
+
Every call path keeps the flavour, the log wording and the trace line it had
|
|
1987
|
+
before, including the one asymmetry inside the propagating flavour:
|
|
1988
|
+
`PluginContext.trigger` has never emitted the `Triggering hook: <name>` trace on
|
|
1989
|
+
either kernel, so it still does not. The kernels' two `hooks` maps are
|
|
1990
|
+
deliberately **not** unified, and `ObjectKernel` deliberately does **not** gain a
|
|
1991
|
+
base class — both were considered and ruled out of scope.
|
|
1992
|
+
|
|
1993
|
+
How "no behaviour change" was proved: the paired kernel pins from #5170 / #5257 /
|
|
1994
|
+
#5274 pass untouched, and deleting the shared dispatcher's error log now turns
|
|
1995
|
+
**both** kernels' test files red from a single edit — a property the hand-mirrored
|
|
1996
|
+
copies could not have (editing `ObjectKernel`'s private loop could never turn
|
|
1997
|
+
`lite-kernel.test.ts` red).
|
|
1998
|
+
|
|
1999
|
+
Shared dispatch cannot cover the residual two-maps seam, so the pairing of the
|
|
2000
|
+
tests is now a gate rather than a convention: `pnpm check:kernel-hook-pairs`
|
|
2001
|
+
(`scripts/check-kernel-hook-pairs.mjs`, wired into the ESLint job) requires every
|
|
2002
|
+
`kernel:*` hook dispatched in `packages/core/src` to be named in a test title in
|
|
2003
|
+
**both** `kernel.test.ts` and `lite-kernel.test.ts`, and fails naming the hook
|
|
2004
|
+
and the side that lacks it. A fifth lifecycle hook can no longer arrive paired on
|
|
2005
|
+
one kernel only.
|
|
2006
|
+
|
|
2007
|
+
Also pinned, deliberately unchanged: `kernel:shutdown` has two dispatch paths
|
|
2008
|
+
with different flavours on both kernels — the kernel's own teardown isolates,
|
|
2009
|
+
while a plugin calling `ctx.trigger('kernel:shutdown')` by hand propagates.
|
|
2010
|
+
Nothing in the repo triggers it by hand today, so this is dormant; it is now a
|
|
2011
|
+
documented fact with a named test on each side rather than a surprise found at
|
|
2012
|
+
teardown.
|
|
2013
|
+
|
|
2014
|
+
- 674ac99: fix(core): one throwing `kernel:shutdown` handler no longer skips every plugin `destroy()` and kills the process under a false "Shutdown timed out" (#5274)
|
|
2015
|
+
|
|
2016
|
+
**On `ObjectKernel`, a single bad shutdown subscriber used to end the entire teardown
|
|
2017
|
+
and `process.exit(1)` the host — reporting a timeout that never happened.**
|
|
2018
|
+
|
|
2019
|
+
`performShutdown()` dispatched `kernel:shutdown` through `context.trigger` (a bare
|
|
2020
|
+
awaited loop that never catches), so the first handler that threw propagated out to
|
|
2021
|
+
`shutdown()`'s `Promise.race` catch. That catch was written for the timeout race alone
|
|
2022
|
+
and treated every exception as one, producing three consequences at once:
|
|
2023
|
+
|
|
2024
|
+
1. the remaining `kernel:shutdown` handlers never ran;
|
|
2025
|
+
2. **every** plugin's `destroy()` was skipped — the reverse-order destroy pass sits
|
|
2026
|
+
after the trigger in `performShutdown()`, so it was never reached;
|
|
2027
|
+
3. the process was killed by `process.exit(1)` under the log line
|
|
2028
|
+
`Shutdown timed out — forcing exit`, while nothing had timed out — sending whoever
|
|
2029
|
+
read it to the `shutdownTimeout` config for a handler bug.
|
|
2030
|
+
|
|
2031
|
+
Two changes, matching the reasoning #5257 recorded at `LiteKernel`'s shutdown dispatch
|
|
2032
|
+
site:
|
|
2033
|
+
|
|
2034
|
+
- **`kernel:shutdown` now dispatches ISOLATING on `ObjectKernel` too.** A handler that
|
|
2035
|
+
throws is logged as `Hook handler failed: kernel:shutdown` and the remaining handlers
|
|
2036
|
+
still run, followed by the reverse-order `destroy()` pass and the `onShutdown()`
|
|
2037
|
+
handlers — both of which already isolated per plugin and per handler. What is queued
|
|
2038
|
+
behind a failing shutdown handler is the cleanup that flushes buffers, closes
|
|
2039
|
+
connections and releases locks, so one bad handler must not amplify into leaks and
|
|
2040
|
+
unflushed writes. The BOOT-path hooks are untouched: `kernel:ready`,
|
|
2041
|
+
`kernel:bootstrapped` and `kernel:listening` still propagate and still fail the boot
|
|
2042
|
+
(#5170, #5257).
|
|
2043
|
+
- **The timeout catch now handles only a genuine timeout**, discriminated by identity on
|
|
2044
|
+
the timer's own rejection — not by message, not by type, so nothing a plugin throws
|
|
2045
|
+
can impersonate it. A genuine `shutdownTimeout` overrun is **unchanged**: it still
|
|
2046
|
+
logs `Shutdown timed out — forcing exit` and still calls `process.exit(1)`, because
|
|
2047
|
+
teardown really is hung and the process would otherwise hold what it failed to
|
|
2048
|
+
release. Any other exception is logged at `error` and follows the normal path —
|
|
2049
|
+
`state = 'stopped'`, return — with no `process.exit`, leaving an embedding host
|
|
2050
|
+
(cloud auth-proxy, CLI, a test runner) its own chance to finish cleanly.
|
|
2051
|
+
|
|
2052
|
+
`shutdown()` still never rejects, so no existing caller changes. Telling the two paths
|
|
2053
|
+
apart is the point of the fix, and both are pinned by named tests.
|
|
2054
|
+
|
|
2055
|
+
- 833b512: fix(core): 插件 init/start 的超时守卫定时器在 race 结束时被清除,进程不再空转 `startupTimeout` (#4813)
|
|
2056
|
+
|
|
2057
|
+
`ObjectKernel.initPluginWithTimeout()` / `startPluginWithTimeout()` 各自 `setTimeout` armed
|
|
2058
|
+
一根超时守卫,然后**把它扔了**:插件赢下 race 之后,那根定时器既没 `clearTimeout` 也没
|
|
2059
|
+
`unref()`,带着 ref 一直挂到 `startupTimeout` 走完。于是每个进程在活干完之后还要空转整整
|
|
2060
|
+
一个 `startupTimeout` —— `ObjectQLPlugin` 是 120 秒。
|
|
2061
|
+
|
|
2062
|
+
实测(`examples/app-crm`,同一条 `migrate recorded-by --json`,同一个构建链,唯一差别是本
|
|
2063
|
+
改动):
|
|
2064
|
+
|
|
2065
|
+
| | 墙钟 |
|
|
2066
|
+
| :----- | :----- |
|
|
2067
|
+
| 修复前 | 122.4s |
|
|
2068
|
+
| 修复后 | 3.1s |
|
|
2069
|
+
|
|
2070
|
+
JSON 与 `✅ Graceful shutdown complete` 两次都在 ~3 秒出现 —— 后面那 119 秒纯粹是 8 根
|
|
2071
|
+
孤儿定时器(4 个 init + 4 个 start)钉着事件循环。`os serve` 里同样漏,只是那里进程本来
|
|
2072
|
+
就长命,看不出来。
|
|
2073
|
+
|
|
2074
|
+
**为什么是 `clearTimeout` 而不是 `unref()`。** 隔壁 `shutdown()` 的守卫用的是 `unref()`,
|
|
2075
|
+
但那个写法在这里是错的,而且不是风格问题:`unref()` 让定时器不再钉住事件循环,**同时也
|
|
2076
|
+
让它不再是一个守卫** —— 若 hook 永不 settle 且没有别的东西撑着事件循环,Node 会在定时器
|
|
2077
|
+
触发之前直接退出,超时被**静默吞掉**,谁也不会收到那个 error。守卫必须在 race 未决期间
|
|
2078
|
+
保持 ref'd,在 race 落定的那一刻被回收,这正是 `finally { clearTimeout(guard) }` 表达的
|
|
2079
|
+
语义。两个守卫合并为一个私有 helper `raceStartupTimeout()`,措辞与理由写在它的 doc
|
|
2080
|
+
comment 里。
|
|
2081
|
+
|
|
2082
|
+
`startupTimeout` 的取值一个都没动 —— 慢启动的插件需要那个上限,问题从来不在时长,而在
|
|
2083
|
+
没人回收。
|
|
2084
|
+
|
|
2085
|
+
- 7777e8f: fix(spec)!: retire the never-built typed-event system; the lifecycle registry now lists the events that actually fire (#4212 follow-up)
|
|
2086
|
+
|
|
2087
|
+
The lifecycle-event surface promised a typed-event system that was never
|
|
2088
|
+
built, in three layers. `kernel/plugin-lifecycle-events.zod.ts` shipped ten
|
|
2089
|
+
payload schemas (`PluginRegisteredEvent`, `PluginErrorEvent`,
|
|
2090
|
+
`HookTriggeredEvent`, `KernelReadyEvent`, …) and a 21-name
|
|
2091
|
+
`PluginLifecycleEventType` enum — zero consumers for every export, and the
|
|
2092
|
+
enum was wrong in both directions: 17 names nothing fires, 10 real events
|
|
2093
|
+
missing. `contracts/plugin-lifecycle-events.ts` declared the same 17 dead
|
|
2094
|
+
names in `IPluginLifecycleEvents` next to 5 real ones, plus an
|
|
2095
|
+
`ITypedEventEmitter` interface nothing implements. All of it read as a
|
|
2096
|
+
promise; anyone who coded against it (hooking `plugin:started`, awaiting
|
|
2097
|
+
`plugin:error`) registered a handler that could never fire, with no error
|
|
2098
|
+
saying so — the same silent-drop shape as the #4212 lifecycle-hook family.
|
|
2099
|
+
|
|
2100
|
+
Removed, with zero consumers verified repo-wide:
|
|
2101
|
+
|
|
2102
|
+
- `kernel/plugin-lifecycle-events.zod.ts` and every export: `EventPhase`,
|
|
2103
|
+
`PluginEventBase`, `PluginRegisteredEvent`, `PluginLifecyclePhaseEvent`,
|
|
2104
|
+
`PluginErrorEvent`, `ServiceRegisteredEvent`, `ServiceUnregisteredEvent`,
|
|
2105
|
+
`HookRegisteredEvent`, `HookTriggeredEvent`, `KernelEventBase`,
|
|
2106
|
+
`KernelReadyEvent`, `KernelShutdownEvent`, `PluginLifecycleEventType`
|
|
2107
|
+
(schemas and inferred types).
|
|
2108
|
+
- `ITypedEventEmitter` from `contracts/plugin-lifecycle-events.ts`.
|
|
2109
|
+
- The 17 never-fired names from `IPluginLifecycleEvents`.
|
|
2110
|
+
|
|
2111
|
+
`IPluginLifecycleEvents` is now the registry of the **14 events with a real
|
|
2112
|
+
emitter** — `kernel:{ready,bootstrapped,listening,shutdown}`, `app:seeded`,
|
|
2113
|
+
`metadata:reloaded` (payload `metadata` now optional, matching the documented
|
|
2114
|
+
contract), `external.schema.drift`, `ai:routes`, `auth:configure`, and the
|
|
2115
|
+
`{service}:ready` convention family (`mcp`, `automation`, `analytics`,
|
|
2116
|
+
`external-datasource`, `datasource-admin`) — each payload as observed at its
|
|
2117
|
+
fire site. A new `LifecycleEventName` union types
|
|
2118
|
+
`PluginContext.hook`/`trigger` in `@objectstack/core` as
|
|
2119
|
+
`LifecycleEventName | (string & {})`: known names autocomplete, custom
|
|
2120
|
+
cross-plugin names stay legal, existing callers compile unchanged. A pinning
|
|
2121
|
+
test asserts two-way equality between the interface keys and the fire-site
|
|
2122
|
+
inventory.
|
|
2123
|
+
|
|
2124
|
+
FROM → TO:
|
|
2125
|
+
|
|
2126
|
+
- `PluginLifecycleEventType` → `LifecycleEventName` (the union of names that
|
|
2127
|
+
fire). There is no runtime enum; the bus is open by design.
|
|
2128
|
+
- Event payload schemas (`KernelReadyEvent`, `PluginErrorEvent`, …) → the
|
|
2129
|
+
payload tuples on `IPluginLifecycleEvents`. No wire format existed or
|
|
2130
|
+
exists; payloads are in-process arguments.
|
|
2131
|
+
- `ITypedEventEmitter` → `PluginContext.hook`/`trigger` (the emitter that
|
|
2132
|
+
actually exists).
|
|
2133
|
+
- Handlers for the 17 dead names → delete them; they never ran. For plugin
|
|
2134
|
+
phase observation use the boot report (ADR-0084); for per-plugin errors the
|
|
2135
|
+
kernel throws/logs at the failing phase.
|
|
2136
|
+
|
|
2137
|
+
Plain deletion rather than `retiredKey()` tombstones, per the #4233
|
|
2138
|
+
precedent: these keys were never authorable — they described runtime event
|
|
2139
|
+
payload records no config author can write, so the silent-strip class the
|
|
2140
|
+
authorable-surface ratchet guards against is vacuous. Its baseline entries
|
|
2141
|
+
and the `json-schema.manifest.json` keys are dropped deliberately in this PR.
|
|
2142
|
+
No ADR-0087 conversion: no stack metadata names these types; there is nothing
|
|
2143
|
+
for `os migrate meta` to rewrite.
|
|
2144
|
+
|
|
2145
|
+
- 46365ab: fix(core): `ObjectLogger` 的脱敏表按**词边界**匹配,不再按子串吃掉 `keys`/`tokens` 这类普通字段 (#5573)
|
|
2146
|
+
|
|
2147
|
+
`redactSensitive` 此前的判定是 `key.toLowerCase().includes(pattern)` —— 只要字段名
|
|
2148
|
+
**含有** `password`/`token`/`secret`/`key` 子串,整个值就被换成 `***REDACTED***`。
|
|
2149
|
+
于是 `keys`、`keyword`、`keywords`、`keyboard`、`monkey`、`tokens`、`tokenizer`、
|
|
2150
|
+
`secretary` 全部中招:读者不但丢了事实,还被告知"这里挡住了一个秘密",比字段直接
|
|
2151
|
+
缺失更误导。仓库里已经有活的命中 —— `dispatcher-plugin.ts` 为了躲开脱敏器特意把
|
|
2152
|
+
`key` 改名成 `keyedBy`,而 `'keyedby'.includes('key')` 依然为真,那条限流日志的
|
|
2153
|
+
`keyedBy` 一直是 `***REDACTED***`。
|
|
2154
|
+
|
|
2155
|
+
匹配语义 FROM → TO:
|
|
2156
|
+
|
|
2157
|
+
| | FROM(子串 `includes`) | TO(词边界) |
|
|
2158
|
+
| :----------------------------------------------------- | :-------------------- | :------------------- |
|
|
2159
|
+
| `apiKey` / `api_key` / `API_KEY` / `x-api-key` | 脱敏 | 脱敏(不变) |
|
|
2160
|
+
| `apikey` / `APIKEY`(全小写连写) | 脱敏 | 脱敏(不变,见下) |
|
|
2161
|
+
| `apiKeys` / `refresh_tokens`(复合词里的复数) | 脱敏 | 脱敏(不变) |
|
|
2162
|
+
| `keys` / `tokens` / `keyword` / `monkey` / `secretary` | **脱敏** | **不脱敏** |
|
|
2163
|
+
| `keyedBy` / `tokenizerName` | **脱敏** | **不脱敏** |
|
|
2164
|
+
| `passwords` / `secrets`(裸复数) | **脱敏** | **不脱敏** |
|
|
2165
|
+
| `api_key` 字段 + `redact: ['apiKey']` 配置 | **不脱敏** | **脱敏**(跨拼法命中) |
|
|
2166
|
+
|
|
2167
|
+
字段名按 camelCase / snake_case / kebab-case / 字母-数字边界分词后逐词比对。默认脱敏表
|
|
2168
|
+
(`['password','token','secret','key']`)本身**没有变**,`packages/spec` 的 schema 默认值
|
|
2169
|
+
也没有变 —— 变的只是这张表怎么用。
|
|
2170
|
+
|
|
2171
|
+
两个边角是显式取舍,不是遗漏:
|
|
2172
|
+
|
|
2173
|
+
- **全小写连写**没有词边界可分,`apikey` 分词后只有一个词。不能用"以 `key` 结尾"救,
|
|
2174
|
+
因为 `monkey`/`turkey`/`whiskey` 也以它结尾 —— 那正是本单要去掉的误报。所以连写只在
|
|
2175
|
+
前缀是一张显式限定词表(`api`/`access`/`refresh`/`client`/`private`/`session`/…)里的
|
|
2176
|
+
词时才算命中;表外的连写(`foobarkey`)不脱敏,按仓库命名惯例写成 `fooBarKey` /
|
|
2177
|
+
`foo_bar_key` 即可通用命中。只认**后缀**连写,所以 `secretary`、`keyword` 保持干净。
|
|
2178
|
+
- **裸复数**是集合或计数而不是秘密(`keys` 来自 Zod 的 `unrecognized_keys` issue,
|
|
2179
|
+
`tokens` 来自 LLM 用量),按维护者裁决不脱敏;复数**出现在复合词里**时仍然是秘密
|
|
2180
|
+
(`apiKeys: ['sk-…']`),照常脱敏。确实要脱敏裸复数的 host,写
|
|
2181
|
+
`redact: [..., 'passwords']` 显式加回。
|
|
2182
|
+
|
|
2183
|
+
**影响面**:host 侧自定义 `redact` 配置的匹配行为随之收紧 —— 依赖子串宽匹配"顺手"挡住
|
|
2184
|
+
某个字段的部署,需要把该字段名(或它的词)显式写进 `redact`。反向的收益是同一个词现在
|
|
2185
|
+
跨拼法命中:配 `redact: ['apiKey']` 也会挡住 `api_key` 和 `apikey`。
|
|
2186
|
+
|
|
2187
|
+
- c5adfe1: fix: 节点执行与热重载 shutdown 的超时守卫在 race 落定时被清除,不再留下孤儿定时器 (#4952)
|
|
2188
|
+
|
|
2189
|
+
#4813(PR #4874,内核 init/start)与 #4875(PR #4950,周期性健康检查)修掉的是同一种漏法:
|
|
2190
|
+
守卫 armed 之后就被扔掉 —— 被守护的一方赢下 race 之后,那根 `setTimeout` 既没 `clearTimeout`
|
|
2191
|
+
也没 `unref()`,带着 ref 一直把事件循环钉满整个超时预算。本次清仓剩下的两处生产实例:
|
|
2192
|
+
|
|
2193
|
+
- **`AutomationEngine.executeWithTimeout()`**(`service-automation`)—— 三处里量级最大的一处:
|
|
2194
|
+
**每个声明了 `timeoutMs` 的流程节点各一根**,孤儿数随流程节点数 × 触发频率线性增长;一次性进程
|
|
2195
|
+
(`os` CLI 跑到 flow 的路径)干完活之后还会被最长的那根守卫按住到超时才退出。
|
|
2196
|
+
- **`HotReloadManager.reloadPlugin()`**(`core`)—— 插件 `destroy()` 的 shutdown 守卫,与 #4813
|
|
2197
|
+
修掉的两处一字不差:一次毫秒级完成的热重载,照样把循环钉满 `shutdownTimeout`。
|
|
2198
|
+
|
|
2199
|
+
两处修法与 #4874 / #4950 同形,不新造变体:私有 helper +
|
|
2200
|
+
`try { return await Promise.race([...]) } finally { clearTimeout(guard) }`。`hot-reload.ts` 的
|
|
2201
|
+
helper 把入参放宽到 `T | PromiseLike<T>`(Plugin 契约允许同步 `destroy()`);`engine.ts` 的不放宽
|
|
2202
|
+
(`NodeExecutor.execute` 声明返回 `Promise`)。
|
|
2203
|
+
|
|
2204
|
+
**为什么是 `clearTimeout` 而不是 `unref()`。** `unref()` 让定时器不再钉住事件循环的同时,也让它
|
|
2205
|
+
不再是一个守卫 —— 若被守护的一方永不 settle 且没有别的东西撑着事件循环,Node 会在定时器触发之前
|
|
2206
|
+
退出,超时被静默吞掉。守卫必须在 race 未决期间保持 ref'd、在落定那一刻被回收,这正是
|
|
2207
|
+
`finally { clearTimeout(guard) }` 表达的语义。两处的回归测试各自沿用 #4950 的双向写法:
|
|
2208
|
+
真实定时器下不留 ref'd 定时器、fake timers 下连跑多轮不累积(计数能看见 `unref()` 过的定时器,
|
|
2209
|
+
因此识破 `unref()` 式的假修复)、以及被守护方真的挂住时超时照常上报。
|
|
2210
|
+
|
|
2211
|
+
超时时长(`timeoutMs` / `shutdownTimeout`)一个都没动 —— 问题从来不在时长,而在没人回收。
|
|
2212
|
+
|
|
2213
|
+
- 7ce02eb: feat(spec,objectql): `IObjectQLEngine` — the `objectql` slot's contract exists, the class `implements` it, and the seven consumer-local stand-ins are deleted (#4251 B3)
|
|
2214
|
+
|
|
2215
|
+
ObjectQL registers one instance under two names, and the ledger can finally say
|
|
2216
|
+
what each name means: `data` stays `IDataEngine` (the data plane), `objectql`
|
|
2217
|
+
now resolves to **`IObjectQLEngine`** — the full engine: schema access
|
|
2218
|
+
(`getSchema` / `getObject` / `registry`), actions (`registerAction` /
|
|
2219
|
+
`removeActionsByPackage` / `executeAction`), the hook/middleware seams
|
|
2220
|
+
(`registerHook` / `unregisterHooksByPackage` / `registerFunction` /
|
|
2221
|
+
`registerMiddleware` / `bindHooks`), the first-wins default runners and hook
|
|
2222
|
+
metrics, boot wiring (`registerDriver` / `setDatasourceMapping` /
|
|
2223
|
+
`registerApp`), and the ops probes (`checkDriversHealth` /
|
|
2224
|
+
`wasDatastoreCreatedFromEmpty` / `invalidateDataMigrationFlags`). The ledger
|
|
2225
|
+
test pins the new relation: `objectql` strictly widens `data`, deliberately no
|
|
2226
|
+
longer equal.
|
|
2227
|
+
|
|
2228
|
+
**Why now, and why `implements` is the point.** The honest state for two
|
|
2229
|
+
batches was recorded on `DomainHandlerContext.getObjectQL`: ObjectQL is wider
|
|
2230
|
+
than `IDataEngine`, the wider part had no contract, and typing it `IDataEngine`
|
|
2231
|
+
would be "the more comfortable-looking lie". The interim discipline — each
|
|
2232
|
+
consumer declares the narrow slice it uses — produced seven local surfaces
|
|
2233
|
+
(`AppEngineSurface`, `EngineRegistrySurface`, `EngineExtensionSurface`,
|
|
2234
|
+
`SecurityEngineSurface`, `FreshDatastoreEngine`, the dispatcher's inline
|
|
2235
|
+
`checkDriversHealth` slice, the `getObjectQL: any` itself). Each was honest and
|
|
2236
|
+
each was an UNCHECKED claim: `getService<Surface>('objectql')` is an assertion,
|
|
2237
|
+
so an engine rename would have broken every consumer at runtime with zero
|
|
2238
|
+
compile errors. `ObjectQL implements IObjectQLEngine` converts all of them into
|
|
2239
|
+
one compiler-verified claim. All seven stand-ins are deleted; consumers import
|
|
2240
|
+
the one declaration. `getObjectQL` is typed `Promise<IObjectQLEngine | null>`
|
|
2241
|
+
end to end, closing the oldest documented `any` in the dispatcher.
|
|
2242
|
+
|
|
2243
|
+
**Evidence bar unchanged.** Every declared member has a cross-package consumer
|
|
2244
|
+
reaching it through the slot; engine members without one (e.g. `triggerHooks`,
|
|
2245
|
+
cross-package only in tests) stay off until a caller appears. The registry view
|
|
2246
|
+
(`EngineSchemaRegistryView`) declares exactly the eight members consumers use.
|
|
2247
|
+
|
|
2248
|
+
**`_registry` never leaves the engine package now.** plugin-security's
|
|
2249
|
+
declared-metadata readers (`readDeclared`, permission-set projection, suggested
|
|
2250
|
+
audience bindings) reached ObjectQL's private `_registry` field through `any` —
|
|
2251
|
+
the same private reach `/me/apps` had in B2, five more times. All migrated to
|
|
2252
|
+
the public `registry` getter the contract declares, test doubles included.
|
|
2253
|
+
|
|
2254
|
+
**`IMetadataService` gains `subscribe?` / `loadMany?`** — implemented by
|
|
2255
|
+
`MetadataManager` beside `watch` all along, reached through the slot only via
|
|
2256
|
+
`any` by ObjectQLPlugin's metadata bridge (the re-sync keeping runtime-authored
|
|
2257
|
+
hooks/actions live). With them declared, the bridge's six `metadata` lookups
|
|
2258
|
+
and metadata-protocol's `objectql` lookup carry contract types, and both files
|
|
2259
|
+
leave the grandfather list entirely: baseline **167 → 159 sites, 36 → 34
|
|
2260
|
+
files**.
|
|
2261
|
+
|
|
2262
|
+
- d0d5205: refactor(core,plugin-audit,service-storage,plugin-reports): give the `__` operation-private-key convention a single owner (#7284)
|
|
2263
|
+
|
|
2264
|
+
`withoutOperationPrivateKeys` — the rule that a consumer forwarding a caller's
|
|
2265
|
+
execution envelope to a question about a DIFFERENT object must first drop the
|
|
2266
|
+
`__`-prefixed keys plugin-security stamped for the operation in flight — had been
|
|
2267
|
+
hand-copied into three packages: `plugin-audit`'s comment access hooks (#7141),
|
|
2268
|
+
`service-storage`'s attachment access hooks (#7145) and `plugin-reports`' report
|
|
2269
|
+
service (#7204). Each carried its own `OPERATION_PRIVATE_KEY_PREFIX` and its own
|
|
2270
|
+
doc block, and the prose had already diverged while the code still agreed — the
|
|
2271
|
+
shape that makes a later divergence in behaviour hard to notice.
|
|
2272
|
+
|
|
2273
|
+
The helper now lives once, in `@objectstack/core`
|
|
2274
|
+
(`security/operation-private-keys.ts`), exported from the package root. Core is
|
|
2275
|
+
the only candidate all three consumers already depend on: `plugin-security` is
|
|
2276
|
+
the producer of the convention and the most honest owner, but none of the three
|
|
2277
|
+
depends on it and a string-prefix filter does not justify three new dependency
|
|
2278
|
+
edges onto a plugin; `@objectstack/spec` is fenced off by Prime Directive #2. The
|
|
2279
|
+
new home sits beside `assemble-execution-context.ts`, which owns the other end of
|
|
2280
|
+
the same lifecycle — that file is where an `ExecutionContext` is built at a
|
|
2281
|
+
transport entry point, this one is where it is stripped back down before being
|
|
2282
|
+
forwarded.
|
|
2283
|
+
|
|
2284
|
+
The full reasoning moved with the code rather than being thinned: which keys the
|
|
2285
|
+
middleware stamps and why each is a widening input, why they are dropped by
|
|
2286
|
+
PREFIX and never by a name list, and why the fresh copy is load-bearing in both
|
|
2287
|
+
directions. Each consumer keeps only its own local half — which object _its_
|
|
2288
|
+
gates actually ask about — and points at the shared home.
|
|
2289
|
+
|
|
2290
|
+
No behaviour change: the three copies were byte-equivalent, and all three
|
|
2291
|
+
packages' suites pass unchanged. Two new pins at the home cover it — the rule's
|
|
2292
|
+
own behaviour, which no package-level test had ever asserted directly, and a
|
|
2293
|
+
repository-shape pin that turns red if a fourth file declares its own copy.
|
|
2294
|
+
|
|
2295
|
+
- be7360c: chore(plugins,services): declare `providesServices` on the 20 remaining init-time service providers (ADR-0116 follow-up, #4131)
|
|
2296
|
+
|
|
2297
|
+
ADR-0116 gave the kernel a declared ordering contract, but only
|
|
2298
|
+
`ObjectQLPlugin` and `MetadataPlugin` had declared what their `init()`
|
|
2299
|
+
registers. The pre-Phase-1 ordering check can only _name a provider_ for
|
|
2300
|
+
services someone declared, so its coverage was two plugins wide.
|
|
2301
|
+
|
|
2302
|
+
An audit of every plugin's `init()` body (brace-matched, comments stripped,
|
|
2303
|
+
each call classified by whether it sits inside a `try`/`if`) found 20 plugins
|
|
2304
|
+
that register a service on every path without declaring it. All 20 now
|
|
2305
|
+
declare `providesServices`. Purely additive: no ordering changes, no new
|
|
2306
|
+
failure modes — a `providesServices` entry only lets the kernel say _who_
|
|
2307
|
+
provides a service when it reports a misordering, and enriches the Phase-1
|
|
2308
|
+
`getService` miss diagnostic.
|
|
2309
|
+
|
|
2310
|
+
Three needed a closer read before declaring, because they register the same
|
|
2311
|
+
service from several branches (`cache`, `queue`, `job`): each early-return
|
|
2312
|
+
branch plus the fallback registers it, so every path does — the declaration
|
|
2313
|
+
is honest. ADR-0116's rule that a _conditionally_ registered service must
|
|
2314
|
+
never be declared is unchanged and was applied throughout.
|
|
2315
|
+
|
|
2316
|
+
The same audit found 12 plugins that hard-resolve a service during `init()`
|
|
2317
|
+
(11 of them `manifest`) without declaring `requiresServices`. None is a live
|
|
2318
|
+
exposure — every one already declares a hard `dependencies` entry on the
|
|
2319
|
+
provider, so the kernel orders them correctly today. Those are tracked
|
|
2320
|
+
separately: with a hard dependency in place, `requiresServices` mostly
|
|
2321
|
+
restates what the kernel already enforces, and its real value is on
|
|
2322
|
+
_soft_-dependency consumers, of which `AppPlugin` is currently the only one.
|
|
2323
|
+
|
|
2324
|
+
- 06770c0: fix(core,cli): `os test`'s record action types reach the served route, and a zero-match glob states its posture (#7848)
|
|
2325
|
+
|
|
2326
|
+
Two defects on the same surface, both measured on a booted showcase while
|
|
2327
|
+
authoring the `qa` platform-checklist item.
|
|
2328
|
+
|
|
2329
|
+
## 5 of the 8 declared action types could not reach a stock server
|
|
2330
|
+
|
|
2331
|
+
`HttpTestAdapter` built `${baseUrl}/api/data/:object`. A stock server serves
|
|
2332
|
+
`{apiPath}/data/:object` with `apiPath` = `/api/v1`, so every record-shaped
|
|
2333
|
+
member of `TestActionTypeSchema` was one version segment short and answered
|
|
2334
|
+
`HTTP Error 404: {"error":"Not found"}` — `create_record`, `read_record`,
|
|
2335
|
+
`update_record`, `delete_record` and `query_records`. `update_record` was wrong
|
|
2336
|
+
twice: it issued `PUT` where the route is `PATCH`, and there is no `PUT`
|
|
2337
|
+
sibling to fall back on. Only `api_call` and `wait` executed, which is why the
|
|
2338
|
+
gap survived — everything the Quality Protocol had been used for so far was
|
|
2339
|
+
expressible through `api_call`.
|
|
2340
|
+
|
|
2341
|
+
All five now address the route the server registers, and `update_record` uses
|
|
2342
|
+
`PATCH` with `id` peeled off the body (the body is the field patch, not a
|
|
2343
|
+
column write). The prefix is no longer written down: it is derived from the two
|
|
2344
|
+
schemas `RestServer` itself resolves from — `RestApiConfigSchema`
|
|
2345
|
+
(`apiPath ?? {basePath}/{version}`) and `CrudEndpointsConfigSchema.dataPrefix`
|
|
2346
|
+
— so the adapter's default cannot drift from the declaration again. Defaults
|
|
2347
|
+
only: a deployment that overrides `api.apiPath` or `crud.dataPrefix` is still
|
|
2348
|
+
out of reach for the record action types, and `api_call` remains the escape
|
|
2349
|
+
hatch there.
|
|
2350
|
+
|
|
2351
|
+
`run_script` still has no adapter branch and still throws by name; nothing here
|
|
2352
|
+
implements it.
|
|
2353
|
+
|
|
2354
|
+
## A run that loaded no suite reported success silently
|
|
2355
|
+
|
|
2356
|
+
`os test 'qa/nothing-matches-*.test.json'` exited **0** after executing nothing,
|
|
2357
|
+
so a CI step whose glob stopped matching (a renamed directory, a moved suite)
|
|
2358
|
+
reported success forever.
|
|
2359
|
+
|
|
2360
|
+
The default exit status is deliberately unchanged — a repository that
|
|
2361
|
+
legitimately ships no suites must not begin failing CI. What changes is that the
|
|
2362
|
+
posture is now **declared** rather than accidental:
|
|
2363
|
+
|
|
2364
|
+
- `os test --help` states it: a pattern matching no suite prints
|
|
2365
|
+
`Found 0 test suites.` and exits 0;
|
|
2366
|
+
- **new flag `--fail-on-empty`** opts into the strict reading and exits 1 on an
|
|
2367
|
+
empty match;
|
|
2368
|
+
- `Found N test suites.` is emitted on **every** run, `Found 0 test suites.`
|
|
2369
|
+
included. It was previously printed only when the count was positive — absent
|
|
2370
|
+
from exactly the run where a caller needs it to tell "every suite passed" from
|
|
2371
|
+
"there were no suites".
|
|
2372
|
+
|
|
2373
|
+
Both exit-code arms now carry explicit assertions over a real child process.
|
|
2374
|
+
|
|
2375
|
+
- 857a6cf: fix(cli,core,metadata,runtime): `os serve` boots with no compiled artifact — the platform does not need an application to start (#4085)
|
|
2376
|
+
|
|
2377
|
+
The artifact (`dist/objectstack.json`) defines an **application**. ObjectStack is
|
|
2378
|
+
a development platform, so it has to start without one — but `os serve
|
|
2379
|
+
objectstack.config.ts` died during boot whenever the artifact was absent:
|
|
2380
|
+
|
|
2381
|
+
```
|
|
2382
|
+
Loading objectstack.config.ts...
|
|
2383
|
+
[StandaloneStack] artifact read FAILED: path='…/dist/objectstack.json' error=ENOENT…
|
|
2384
|
+
|
|
2385
|
+
✗ Service 'manifest' is async - use await
|
|
2386
|
+
```
|
|
2387
|
+
|
|
2388
|
+
Exit 1 — on a **known-good app** (`examples/app-todo` fails the same way with
|
|
2389
|
+
only its `dist/objectstack.json` moved aside), and on every freshly authored
|
|
2390
|
+
project between `os init` and its first `os compile`. The message named neither
|
|
2391
|
+
the missing artifact nor a fix, so it read as an internal kernel fault.
|
|
2392
|
+
|
|
2393
|
+
Three separate faults, each of which alone was enough to refuse the boot:
|
|
2394
|
+
|
|
2395
|
+
- **`serve` registered the config-derived `AppPlugin` before the stack's own
|
|
2396
|
+
`plugins[]`.** Registration order _is_ the kernel's init/start order, and that
|
|
2397
|
+
slot sits ahead of `ObjectQLPlugin` (which registers `manifest`/`objectql`) and
|
|
2398
|
+
`DefaultDatasourcePlugin` (which connects the database the app seeds through).
|
|
2399
|
+
The wrap is now **appended** to `plugins[]`, the same slot
|
|
2400
|
+
`createStandaloneStack` gives its artifact-derived `AppPlugin` — so config-boot
|
|
2401
|
+
and artifact-boot share one plugin order. The artifact path never hit this,
|
|
2402
|
+
which is exactly what made a plugin-**order** bug look artifact-related.
|
|
2403
|
+
|
|
2404
|
+
- **`ctx.getService()` reported a never-registered service as "is async".**
|
|
2405
|
+
`PluginLoader.getService` is an `async` method, so its return value is _always_
|
|
2406
|
+
a Promise and its internal "not found" rejection can never surface
|
|
2407
|
+
synchronously — the kernel read the answer off that Promise and told every
|
|
2408
|
+
caller to `await` a service that did not exist, while the `not found` branch
|
|
2409
|
+
below it was unreachable. It now decides from the registry: absent ⇒
|
|
2410
|
+
`[Kernel] Service 'x' not found`, registered-but-uninstantiated ⇒ the unchanged
|
|
2411
|
+
`Service 'x' is async - use await`. The same crash now reads
|
|
2412
|
+
`[Kernel] Service 'manifest' not found`, which points at the layer that is
|
|
2413
|
+
actually wrong.
|
|
2414
|
+
|
|
2415
|
+
- **`MetadataPlugin` treated an absent `local-file` artifact as fatal.**
|
|
2416
|
+
`createStandaloneStack` always points it at `dist/objectstack.json`, so a stack
|
|
2417
|
+
with no app at all could not boot. A **missing** local artifact is now "nothing
|
|
2418
|
+
compiled yet": it logs, starts empty, and leaves the artifact watcher armed, so
|
|
2419
|
+
a later `os compile` hydrates the running server. The tolerance is
|
|
2420
|
+
ENOENT-only — a malformed or unreadable artifact stays fatal — and
|
|
2421
|
+
`bootstrap: 'artifact-only'` (sealed runtime, where the artifact _is_ the
|
|
2422
|
+
deployment) keeps failing loudly rather than silently serving an empty runtime.
|
|
2423
|
+
|
|
2424
|
+
`[StandaloneStack] artifact read FAILED … ENOENT` is likewise no longer shouted
|
|
2425
|
+
at callers for whom "no artifact" is a healthy state; a present-but-unusable
|
|
2426
|
+
artifact keeps the loud warning.
|
|
2427
|
+
|
|
2428
|
+
Pinned by an e2e pair that drives the real `os serve` with **no `os compile`
|
|
2429
|
+
anywhere**: an app defined only by `objectstack.config.ts` (asserting its object
|
|
2430
|
+
is in the started plugin set, not merely that boot survived) and a bare
|
|
2431
|
+
`export default {}` platform. The #4012 fixture drops the `os compile` this bug
|
|
2432
|
+
had forced on it.
|
|
2433
|
+
|
|
2434
|
+
- d92c72d: fix(lint,runtime,core): the slot-lookup guard sees the split-declaration form — the shape that made the ratchet look cleaner the more it was used (#4251)
|
|
2435
|
+
|
|
2436
|
+
The three selectors from #4321 all key off the erasure and the lookup being in
|
|
2437
|
+
ONE expression. Split them and every selector misses:
|
|
2438
|
+
|
|
2439
|
+
```ts
|
|
2440
|
+
let ql: any;
|
|
2441
|
+
try {
|
|
2442
|
+
ql = ctx.getService("objectql");
|
|
2443
|
+
} catch {
|
|
2444
|
+
/* optional */
|
|
2445
|
+
}
|
|
2446
|
+
```
|
|
2447
|
+
|
|
2448
|
+
Selector 1 needs the call inside the declarator (this declarator has no init),
|
|
2449
|
+
selector 2 needs `as`, selector 3 needs a type argument. The contract is erased
|
|
2450
|
+
exactly as in `const ql: any = ctx.getService(…)`.
|
|
2451
|
+
|
|
2452
|
+
**Why this could not wait for the batches.** The baseline's monotonicity check
|
|
2453
|
+
means a file that leaves the grandfather list can never be re-added. So every
|
|
2454
|
+
batch converted more of this shape from "grandfathered" into "lint covers this
|
|
2455
|
+
file and says nothing" — B2 alone moved `plugin-security/security-plugin.ts`
|
|
2456
|
+
into that state. A ratchet that reports a cleaner number the more you sweep is
|
|
2457
|
+
the #4342 failure wearing different clothes, and the fix only gets more
|
|
2458
|
+
expensive per batch shipped.
|
|
2459
|
+
|
|
2460
|
+
**It is a rule, not a fourth selector, and that is the whole finding.** esquery
|
|
2461
|
+
can match `AssignmentExpression:has(CallExpression[…])`, but it cannot tell
|
|
2462
|
+
which declaration the assigned identifier resolves to — so it would equally
|
|
2463
|
+
flag the correctly-typed form this work line exists to produce (`let
|
|
2464
|
+
i18nService: II18nService | undefined; i18nService = …`, 8 such sites today in
|
|
2465
|
+
runtime/app-plugin.ts, service-automation and metadata-protocol). Resolving the
|
|
2466
|
+
identifier needs SCOPE analysis. That is cheap and needs no type information, so
|
|
2467
|
+
this stays out of the typed-lint pass the KNOWN RESIDUAL still waits on — but it
|
|
2468
|
+
is a rule, and the earlier "just one more selector" estimate was wrong.
|
|
2469
|
+
|
|
2470
|
+
Verified against exactly that: the rule flags all 16 real sites and none of the
|
|
2471
|
+
8 correctly-typed lookalikes.
|
|
2472
|
+
|
|
2473
|
+
**Scale.** The baseline goes 140 → **169 sites** with the file count unchanged
|
|
2474
|
+
at 37: 29 sites were already inside grandfathered files and simply invisible.
|
|
2475
|
+
16 more could NOT be grandfathered (12 in files earlier batches had cleared, 3
|
|
2476
|
+
in files never listed, 1 the regex sweep had missed) and are typed here —
|
|
2477
|
+
`runtime/app-plugin.ts` ×5, `core/fallbacks/authored-translation-sync.ts` ×2,
|
|
2478
|
+
`plugin-security/security-plugin.ts` ×2, `cloud-connection/{runtime-config,
|
|
2479
|
+
marketplace-proxy}-plugin.ts` ×3, `platform-objects/src/plugin.ts` ×2,
|
|
2480
|
+
`runtime/http-dispatcher.ts`, `runtime/domains/ai.ts`. No baseline key was
|
|
2481
|
+
added; the key set still only shrinks.
|
|
2482
|
+
|
|
2483
|
+
Contracts where they exist (`IAIService`, `IJobService`, `IMetadataService`,
|
|
2484
|
+
`II18nService`, `IDataEngine`, `IHttpServer`), named local surfaces where they
|
|
2485
|
+
do not — `AppEngineSurface`, `SecurityEngineSurface`, `RawAppHost`,
|
|
2486
|
+
`EnvRegistrySurface`, `FreshDatastoreEngine`, `AuthoredTranslationSink`. Two of
|
|
2487
|
+
those record something worth naming: `IHttpServer` has no `getRawApp()` (the
|
|
2488
|
+
contract is framework-agnostic and the raw app is Hono's own handle), and
|
|
2489
|
+
ObjectQL's `_defaultBodyRunner` / `_defaultActionRunner` have no public reader
|
|
2490
|
+
at all — the engine attaches them via `(this as any)` and publishes nothing,
|
|
2491
|
+
while `getHookMetricsRecorder()` exists for exactly that question about the
|
|
2492
|
+
metrics recorder. Declared rather than laundered through `any`, and filed.
|
|
2493
|
+
|
|
2494
|
+
- ee264b2: fix(rest): refuse an unknown `?status` on `/security/suggested-bindings` instead of answering an empty list (#7678)
|
|
2495
|
+
|
|
2496
|
+
`GET /api/v1/security/suggested-bindings?status=garbage` returned **200 with an
|
|
2497
|
+
empty list**. That is worse than an error: an empty list is a plausible,
|
|
2498
|
+
actionable-looking answer, so the response reads as _"there are no suggestions"_
|
|
2499
|
+
rather than _"your filter was not a status"_. An admin checking whether a package
|
|
2500
|
+
still has pending audience-binding suggestions got a clean, wrong all-clear.
|
|
2501
|
+
|
|
2502
|
+
The route (`registerSecurityEndpoints`) forwarded `req.query.status` straight into
|
|
2503
|
+
`listAudienceBindingSuggestions`, whose contract — `AudienceBindingSuggestionFilter`
|
|
2504
|
+
— declares exactly three values (`pending`, `confirmed`, `dismissed`). Anything
|
|
2505
|
+
else was not an injection (the `where` clause is structured, never interpolated),
|
|
2506
|
+
it simply matched no row.
|
|
2507
|
+
|
|
2508
|
+
**The rule already existed; only one of its two seams had it.** The runtime
|
|
2509
|
+
dispatcher's `/security` domain has refused unknown statuses since the filter was
|
|
2510
|
+
first tightened, carrying a comment describing precisely the empty-list arm above.
|
|
2511
|
+
The live REST route is a second seam onto the same service call and never got it —
|
|
2512
|
+
a dispatcher-vs-REST divergence pointing the opposite way from the earlier `/meta`
|
|
2513
|
+
cases, where routes existed on the dispatcher but were never mounted on REST.
|
|
2514
|
+
|
|
2515
|
+
So this is a **convergence, not a second implementation**. The vocabulary, the
|
|
2516
|
+
predicate and the refusal wording move to `@objectstack/core`'s security barrel
|
|
2517
|
+
(`isAudienceBindingSuggestionStatus`, alongside `shouldDenyAnonymous` and the other
|
|
2518
|
+
decisions shared by every HTTP seam), and both callers import it. The accepted
|
|
2519
|
+
values stay keyed _by_ the contract type, so adding a status to
|
|
2520
|
+
`AudienceBindingSuggestionFilter` leaves a key missing and fails to compile rather
|
|
2521
|
+
than silently drifting.
|
|
2522
|
+
|
|
2523
|
+
An unknown `?status` is now refused with **400** and the ADR-0112 envelope
|
|
2524
|
+
(`{ error: { code: 'VALIDATION_ERROR', message } }`) — matching the repeated-query-
|
|
2525
|
+
parameter guard already on this route — and the service is not called at all. The
|
|
2526
|
+
vocabulary is case-sensitive, so `?status=PENDING` is refused like any other
|
|
2527
|
+
non-status.
|
|
2528
|
+
|
|
2529
|
+
Unchanged: every declared status still returns its list, omitting `?status`
|
|
2530
|
+
entirely still returns the unfiltered list, `?packageId` is untouched, and the
|
|
2531
|
+
dispatcher seam answers exactly as it did before.
|
|
2532
|
+
|
|
2533
|
+
- 3556b67: fix(security): the MCP stdio bridge stops echoing `internal: true` columns from a write, and the write-response guarantee is guarded as a PROPERTY rather than per-class (#8497)
|
|
2534
|
+
|
|
2535
|
+
**A live leak, found by widening a guard.** #7823 relocated the `internal: true`
|
|
2536
|
+
write-response strip to the generic-data-path ingress and gated the relocation on
|
|
2537
|
+
a tripwire that enumerates every `*Data` face on the protocol class. The card that
|
|
2538
|
+
produced this change observed that the guard's coverage — *"every `*Data`face on
|
|
2539
|
+
one class"* — is narrower than the property that needs holding — *"no response body
|
|
2540
|
+
an external caller receives from a write carries an`internal: true`value"* — and
|
|
2541
|
+
that`@objectstack/rest`'s cross-object batch (a direct `ql.update`) was the
|
|
2542
|
+
standing proof the two are not the same set.
|
|
2543
|
+
|
|
2544
|
+
Widening the guard to the property immediately found a second direct mouth that
|
|
2545
|
+
was **not** covered, and it was leaking. `@objectstack/mcp`'s stdio bridge
|
|
2546
|
+
(`stdio-data-bridge.ts`) is engine-only by construction — the long-lived stdio
|
|
2547
|
+
host cannot reuse the runtime's request-shaped `callData` builder — and its
|
|
2548
|
+
`create` arm handed `engine.insert`'s result straight back to the MCP caller.
|
|
2549
|
+
Since #7823 the engine deliberately keeps its write results whole, so the flagged
|
|
2550
|
+
column rode the tool response verbatim. Measured before the fix:
|
|
2551
|
+
|
|
2552
|
+
```
|
|
2553
|
+
{"object":"vault","id":"r1","record":{"name":"row","id":"r1","vault_secret":"<the stored secret>"}}
|
|
2554
|
+
```
|
|
2555
|
+
|
|
2556
|
+
The file's own header had listed its protocol-layer divergences as _"deliberate,
|
|
2557
|
+
filed, not security"_. One limb of that list **was** security, and the header now
|
|
2558
|
+
says so.
|
|
2559
|
+
|
|
2560
|
+
**What changed**
|
|
2561
|
+
|
|
2562
|
+
- `@objectstack/mcp` — the stdio bridge's `create` runs its response record
|
|
2563
|
+
through the shared strip. `update` does too: that arm discards the engine's
|
|
2564
|
+
write result and echoes the read-path row plus the caller's own patch, so no
|
|
2565
|
+
_stored_ value could reach it, but a caller who puts an `internal: true` key in
|
|
2566
|
+
`data` would otherwise get it echoed back — their own bytes used as an oracle
|
|
2567
|
+
for a column the flag says is never returned. Read verbs are untouched (the
|
|
2568
|
+
engine's read-path strip is unchanged).
|
|
2569
|
+
- `@objectstack/core` — the strip helper
|
|
2570
|
+
(`omitInternalFieldsFromWriteResponse` / `collectInternalWriteResponseFields`)
|
|
2571
|
+
moved here from `@objectstack/metadata-protocol`. It shipped beside the protocol
|
|
2572
|
+
class when that class was its only caller, but the generic write mouths are not
|
|
2573
|
+
all on it: `rest` and `mcp` both reach the engine directly and **neither depends
|
|
2574
|
+
on `@objectstack/metadata-protocol`**, so the old home forced each new mouth to
|
|
2575
|
+
choose between a duck-typed reach through a protocol instance and a private
|
|
2576
|
+
restatement of a security-relevant rule. `core` is the floor all three already
|
|
2577
|
+
depend on, and already hosts this class of shared write-path helper
|
|
2578
|
+
(`bulk-write.ts`). No behaviour change and no API change:
|
|
2579
|
+
`@objectstack/metadata-protocol` re-exports both names unchanged.
|
|
2580
|
+
|
|
2581
|
+
**What guards it now.** Two new tripwires join the shipped one — which is **not**
|
|
2582
|
+
replaced: its runtime prototype walk and its `leakyData` negative control are
|
|
2583
|
+
untouched. Each is a runtime enumeration no author can dodge by adding code
|
|
2584
|
+
without touching it, and each fails on a surface it has no disposition for:
|
|
2585
|
+
|
|
2586
|
+
- `metadata-protocol` — walks the protocol class for `*Data` faces (unchanged);
|
|
2587
|
+
- `rest` — walks `RestServer.getRoutes()` for HTTP write routes, drives the ten
|
|
2588
|
+
data-plane ones (including `POST /batch`, the direct-`ql.update` mouth) against
|
|
2589
|
+
a fixture whose stored rows carry a flagged sentinel, and deep-scans each
|
|
2590
|
+
response body;
|
|
2591
|
+
- `mcp` — walks the `McpDataBridge` faces the factory actually returns.
|
|
2592
|
+
|
|
2593
|
+
Every driven case also asserts a control value is present, so a refusal or an
|
|
2594
|
+
empty body cannot satisfy "no sentinel" by returning nothing.
|
|
2595
|
+
|
|
2596
|
+
Reverse-verified in both directions, the discipline #7823's own fix used: deleting
|
|
2597
|
+
the strip from the REST batch arm turned the REST tripwire red on exactly that
|
|
2598
|
+
route; adding a _second_ unstripped direct engine mouth turned it red again;
|
|
2599
|
+
removing the new MCP strip turned the MCP tripwire red; every restore was proven
|
|
2600
|
+
byte-identical with `git hash-object`.
|
|
2601
|
+
|
|
2602
|
+
- Updated dependencies [50616d9]
|
|
2603
|
+
- Updated dependencies [430dcc2]
|
|
2604
|
+
- Updated dependencies [6a67d7a]
|
|
2605
|
+
- Updated dependencies [333a374]
|
|
2606
|
+
- Updated dependencies [9fe9c1d]
|
|
2607
|
+
- Updated dependencies [3d5c090]
|
|
2608
|
+
- Updated dependencies [e5bd768]
|
|
2609
|
+
- Updated dependencies [08b5a3d]
|
|
2610
|
+
- Updated dependencies [e027b3e]
|
|
2611
|
+
- Updated dependencies [e6ac4bd]
|
|
2612
|
+
- Updated dependencies [c2429b0]
|
|
2613
|
+
- Updated dependencies [445a0c2]
|
|
2614
|
+
- Updated dependencies [d99aeb3]
|
|
2615
|
+
- Updated dependencies [f6609e6]
|
|
2616
|
+
- Updated dependencies [4727eb8]
|
|
2617
|
+
- Updated dependencies [a70358a]
|
|
2618
|
+
- Updated dependencies [0ecc656]
|
|
2619
|
+
- Updated dependencies [06772eb]
|
|
2620
|
+
- Updated dependencies [d4e0809]
|
|
2621
|
+
- Updated dependencies [80334c7]
|
|
2622
|
+
- Updated dependencies [f63cd09]
|
|
2623
|
+
- Updated dependencies [97e7e3c]
|
|
2624
|
+
- Updated dependencies [ce5242c]
|
|
2625
|
+
- Updated dependencies [a7163ea]
|
|
2626
|
+
- Updated dependencies [e6e9379]
|
|
2627
|
+
- Updated dependencies [5823d59]
|
|
2628
|
+
- Updated dependencies [3140f9c]
|
|
2629
|
+
- Updated dependencies [9500ba4]
|
|
2630
|
+
- Updated dependencies [fa3d0cf]
|
|
2631
|
+
- Updated dependencies [af5a224]
|
|
2632
|
+
- Updated dependencies [71f76e1]
|
|
2633
|
+
- Updated dependencies [37b1346]
|
|
2634
|
+
- Updated dependencies [99736a0]
|
|
2635
|
+
- Updated dependencies [fe67e34]
|
|
2636
|
+
- Updated dependencies [fdb4f50]
|
|
2637
|
+
- Updated dependencies [270650f]
|
|
2638
|
+
- Updated dependencies [3aef718]
|
|
2639
|
+
- Updated dependencies [1bd5652]
|
|
2640
|
+
- Updated dependencies [14252d3]
|
|
2641
|
+
- Updated dependencies [7fb436c]
|
|
2642
|
+
- Updated dependencies [879ea13]
|
|
2643
|
+
- Updated dependencies [8828b9e]
|
|
2644
|
+
- Updated dependencies [1ea6bce]
|
|
2645
|
+
- Updated dependencies [c1dcacd]
|
|
2646
|
+
- Updated dependencies [ad303ed]
|
|
2647
|
+
- Updated dependencies [32ccb23]
|
|
2648
|
+
- Updated dependencies [f5a4ef0]
|
|
2649
|
+
- Updated dependencies [2d3e255]
|
|
2650
|
+
- Updated dependencies [a8940e4]
|
|
2651
|
+
- Updated dependencies [7d7521f]
|
|
2652
|
+
- Updated dependencies [5dc4d02]
|
|
2653
|
+
- Updated dependencies [f724f69]
|
|
2654
|
+
- Updated dependencies [98877c9]
|
|
2655
|
+
- Updated dependencies [98877c9]
|
|
2656
|
+
- Updated dependencies [53068c1]
|
|
2657
|
+
- Updated dependencies [ee58392]
|
|
2658
|
+
- Updated dependencies [f16e54e]
|
|
2659
|
+
- Updated dependencies [06be54e]
|
|
2660
|
+
- Updated dependencies [28ad90e]
|
|
2661
|
+
- Updated dependencies [76d74ec]
|
|
2662
|
+
- Updated dependencies [201b31f]
|
|
2663
|
+
- Updated dependencies [e6b1b69]
|
|
2664
|
+
- Updated dependencies [259459d]
|
|
2665
|
+
- Updated dependencies [3f7f14e]
|
|
2666
|
+
- Updated dependencies [e2616e0]
|
|
2667
|
+
- Updated dependencies [6fdc5c6]
|
|
2668
|
+
- Updated dependencies [8b9d71e]
|
|
2669
|
+
- Updated dependencies [05154a1]
|
|
2670
|
+
- Updated dependencies [33f5e23]
|
|
2671
|
+
- Updated dependencies [259af21]
|
|
2672
|
+
- Updated dependencies [f8644c7]
|
|
2673
|
+
- Updated dependencies [306ca50]
|
|
2674
|
+
- Updated dependencies [978fed2]
|
|
2675
|
+
- Updated dependencies [cfc293f]
|
|
2676
|
+
- Updated dependencies [587fc91]
|
|
2677
|
+
- Updated dependencies [de70b42]
|
|
2678
|
+
- Updated dependencies [9b6fe7c]
|
|
2679
|
+
- Updated dependencies [fb3d99b]
|
|
2680
|
+
- Updated dependencies [1986594]
|
|
2681
|
+
- Updated dependencies [6968885]
|
|
2682
|
+
- Updated dependencies [eaed61f]
|
|
2683
|
+
- Updated dependencies [cdfbee2]
|
|
2684
|
+
- Updated dependencies [ad4af62]
|
|
2685
|
+
- Updated dependencies [debe2f6]
|
|
2686
|
+
- Updated dependencies [d44dbfa]
|
|
2687
|
+
- Updated dependencies [29c6c9d]
|
|
2688
|
+
- Updated dependencies [d21c001]
|
|
2689
|
+
- Updated dependencies [ad047d2]
|
|
2690
|
+
- Updated dependencies [8c711fb]
|
|
2691
|
+
- Updated dependencies [f1cc3a3]
|
|
2692
|
+
- Updated dependencies [09e4547]
|
|
2693
|
+
- Updated dependencies [97b0798]
|
|
2694
|
+
- Updated dependencies [474fe39]
|
|
2695
|
+
- Updated dependencies [0bc685a]
|
|
2696
|
+
- Updated dependencies [b949059]
|
|
2697
|
+
- Updated dependencies [2826d1e]
|
|
2698
|
+
- Updated dependencies [be1c52c]
|
|
2699
|
+
- Updated dependencies [c5ff96d]
|
|
2700
|
+
- Updated dependencies [5a84d41]
|
|
2701
|
+
- Updated dependencies [84e7be9]
|
|
2702
|
+
- Updated dependencies [91f4c78]
|
|
2703
|
+
- Updated dependencies [ddc2527]
|
|
2704
|
+
- Updated dependencies [820eff9]
|
|
2705
|
+
- Updated dependencies [a6c3f38]
|
|
2706
|
+
- Updated dependencies [debc23a]
|
|
2707
|
+
- Updated dependencies [0f8ad09]
|
|
2708
|
+
- Updated dependencies [553a47f]
|
|
2709
|
+
- Updated dependencies [43a7a8d]
|
|
2710
|
+
- Updated dependencies [a98085f]
|
|
2711
|
+
- Updated dependencies [20b1a9e]
|
|
2712
|
+
- Updated dependencies [344a22a]
|
|
2713
|
+
- Updated dependencies [4827e91]
|
|
2714
|
+
- Updated dependencies [8d895ff]
|
|
2715
|
+
- Updated dependencies [86f7a20]
|
|
2716
|
+
- Updated dependencies [a3a884d]
|
|
2717
|
+
- Updated dependencies [cfed092]
|
|
2718
|
+
- Updated dependencies [203a449]
|
|
2719
|
+
- Updated dependencies [8f9689f]
|
|
2720
|
+
- Updated dependencies [73f69dc]
|
|
2721
|
+
- Updated dependencies [04c56aa]
|
|
2722
|
+
- Updated dependencies [f6472d7]
|
|
2723
|
+
- Updated dependencies [57a3bb3]
|
|
2724
|
+
- Updated dependencies [b3efeb7]
|
|
2725
|
+
- Updated dependencies [ddd075a]
|
|
2726
|
+
- Updated dependencies [88154be]
|
|
2727
|
+
- Updated dependencies [e8dc61e]
|
|
2728
|
+
- Updated dependencies [9c82146]
|
|
2729
|
+
- Updated dependencies [5f9a987]
|
|
2730
|
+
- Updated dependencies [744b8f5]
|
|
2731
|
+
- Updated dependencies [ac37fc6]
|
|
2732
|
+
- Updated dependencies [2f3e793]
|
|
2733
|
+
- Updated dependencies [4820f55]
|
|
2734
|
+
- Updated dependencies [462d9c4]
|
|
2735
|
+
- Updated dependencies [78caf51]
|
|
2736
|
+
- Updated dependencies [7d21581]
|
|
2737
|
+
- Updated dependencies [37785ed]
|
|
2738
|
+
- Updated dependencies [62a789b]
|
|
2739
|
+
- Updated dependencies [2e284b2]
|
|
2740
|
+
- Updated dependencies [d8e8d9c]
|
|
2741
|
+
- Updated dependencies [789ad63]
|
|
2742
|
+
- Updated dependencies [f2445c9]
|
|
2743
|
+
- Updated dependencies [94e749b]
|
|
2744
|
+
- Updated dependencies [ea1d916]
|
|
2745
|
+
- Updated dependencies [2af1988]
|
|
2746
|
+
- Updated dependencies [1b49eaf]
|
|
2747
|
+
- Updated dependencies [ae31a19]
|
|
2748
|
+
- Updated dependencies [e0f300b]
|
|
2749
|
+
- Updated dependencies [0161c7f]
|
|
2750
|
+
- Updated dependencies [e900015]
|
|
2751
|
+
- Updated dependencies [db02d47]
|
|
2752
|
+
- Updated dependencies [b5bdf48]
|
|
2753
|
+
- Updated dependencies [23338c3]
|
|
2754
|
+
- Updated dependencies [12a19a8]
|
|
2755
|
+
- Updated dependencies [5b843fb]
|
|
2756
|
+
- Updated dependencies [62b6a2f]
|
|
2757
|
+
- Updated dependencies [7e5af5c]
|
|
2758
|
+
- Updated dependencies [5b4780b]
|
|
2759
|
+
- Updated dependencies [a933452]
|
|
2760
|
+
- Updated dependencies [9d1d9c7]
|
|
2761
|
+
- Updated dependencies [8140915]
|
|
2762
|
+
- Updated dependencies [a019e52]
|
|
2763
|
+
- Updated dependencies [e8f8f6c]
|
|
2764
|
+
- Updated dependencies [41dcda3]
|
|
2765
|
+
- Updated dependencies [7b48cf9]
|
|
2766
|
+
- Updated dependencies [b5404f4]
|
|
2767
|
+
- Updated dependencies [64fc6d5]
|
|
2768
|
+
- Updated dependencies [b4487aa]
|
|
2769
|
+
- Updated dependencies [1007379]
|
|
2770
|
+
- Updated dependencies [65ca83a]
|
|
2771
|
+
- Updated dependencies [0bfdf46]
|
|
2772
|
+
- Updated dependencies [947d4f9]
|
|
2773
|
+
- Updated dependencies [f764691]
|
|
2774
|
+
- Updated dependencies [e120a5a]
|
|
2775
|
+
- Updated dependencies [e5bd2f6]
|
|
2776
|
+
- Updated dependencies [e650d67]
|
|
2777
|
+
- Updated dependencies [04476e7]
|
|
2778
|
+
- Updated dependencies [67bf2e2]
|
|
2779
|
+
- Updated dependencies [eaaf03c]
|
|
2780
|
+
- Updated dependencies [d17df80]
|
|
2781
|
+
- Updated dependencies [7d0e7b5]
|
|
2782
|
+
- Updated dependencies [c6d1cb4]
|
|
2783
|
+
- Updated dependencies [6513c17]
|
|
2784
|
+
- Updated dependencies [36030ff]
|
|
2785
|
+
- Updated dependencies [79228cd]
|
|
2786
|
+
- Updated dependencies [6117f7b]
|
|
2787
|
+
- Updated dependencies [e533b0b]
|
|
2788
|
+
- Updated dependencies [cdf4d9a]
|
|
2789
|
+
- Updated dependencies [aee1806]
|
|
2790
|
+
- Updated dependencies [c13350b]
|
|
2791
|
+
- Updated dependencies [c13350b]
|
|
2792
|
+
- Updated dependencies [2c1988c]
|
|
2793
|
+
- Updated dependencies [9ca2d85]
|
|
2794
|
+
- Updated dependencies [c13350b]
|
|
2795
|
+
- Updated dependencies [891d345]
|
|
2796
|
+
- Updated dependencies [c8124e5]
|
|
2797
|
+
- Updated dependencies [a52e2ef]
|
|
2798
|
+
- Updated dependencies [5293114]
|
|
2799
|
+
- Updated dependencies [376a061]
|
|
2800
|
+
- Updated dependencies [c142ced]
|
|
2801
|
+
- Updated dependencies [211abdb]
|
|
2802
|
+
- Updated dependencies [b3363e9]
|
|
2803
|
+
- Updated dependencies [eda599e]
|
|
2804
|
+
- Updated dependencies [a1a4140]
|
|
2805
|
+
- Updated dependencies [7c7e246]
|
|
2806
|
+
- Updated dependencies [2ef1807]
|
|
2807
|
+
- Updated dependencies [f35cdc5]
|
|
2808
|
+
- Updated dependencies [d03fe25]
|
|
2809
|
+
- Updated dependencies [217e2e6]
|
|
2810
|
+
- Updated dependencies [2672f85]
|
|
2811
|
+
- Updated dependencies [20bc357]
|
|
2812
|
+
- Updated dependencies [11066f6]
|
|
2813
|
+
- Updated dependencies [916af17]
|
|
2814
|
+
- Updated dependencies [84c86fb]
|
|
2815
|
+
- Updated dependencies [2a2a9fb]
|
|
2816
|
+
- Updated dependencies [86a71d1]
|
|
2817
|
+
- Updated dependencies [c001422]
|
|
2818
|
+
- Updated dependencies [77022a9]
|
|
2819
|
+
- Updated dependencies [d5c75e2]
|
|
2820
|
+
- Updated dependencies [03d26f7]
|
|
2821
|
+
- Updated dependencies [5966c2a]
|
|
2822
|
+
- Updated dependencies [2382580]
|
|
2823
|
+
- Updated dependencies [9ea2bc5]
|
|
2824
|
+
- Updated dependencies [a2e157c]
|
|
2825
|
+
- Updated dependencies [95c4227]
|
|
2826
|
+
- Updated dependencies [2a61116]
|
|
2827
|
+
- Updated dependencies [52760bf]
|
|
2828
|
+
- Updated dependencies [5543020]
|
|
2829
|
+
- Updated dependencies [880d343]
|
|
2830
|
+
- Updated dependencies [6e82972]
|
|
2831
|
+
- Updated dependencies [d4df105]
|
|
2832
|
+
- Updated dependencies [4615a18]
|
|
2833
|
+
- Updated dependencies [f505689]
|
|
2834
|
+
- Updated dependencies [d9fa683]
|
|
2835
|
+
- Updated dependencies [606d577]
|
|
2836
|
+
- Updated dependencies [4384921]
|
|
2837
|
+
- Updated dependencies [e2798fa]
|
|
2838
|
+
- Updated dependencies [3c628ce]
|
|
2839
|
+
- Updated dependencies [c2d9098]
|
|
2840
|
+
- Updated dependencies [0fd8556]
|
|
2841
|
+
- Updated dependencies [3c7bcc0]
|
|
2842
|
+
- Updated dependencies [4b6cac7]
|
|
2843
|
+
- Updated dependencies [7631964]
|
|
2844
|
+
- Updated dependencies [ac471a0]
|
|
2845
|
+
- Updated dependencies [60ae58e]
|
|
2846
|
+
- Updated dependencies [7f62706]
|
|
2847
|
+
- Updated dependencies [667fa44]
|
|
2848
|
+
- Updated dependencies [37e38d1]
|
|
2849
|
+
- Updated dependencies [e906126]
|
|
2850
|
+
- Updated dependencies [ce92674]
|
|
2851
|
+
- Updated dependencies [08363a0]
|
|
2852
|
+
- Updated dependencies [444de5b]
|
|
2853
|
+
- Updated dependencies [a227ed7]
|
|
2854
|
+
- Updated dependencies [7cb922e]
|
|
2855
|
+
- Updated dependencies [1d22114]
|
|
2856
|
+
- Updated dependencies [1eb13a0]
|
|
2857
|
+
- Updated dependencies [c52e608]
|
|
2858
|
+
- Updated dependencies [9613396]
|
|
2859
|
+
- Updated dependencies [3f7b4ff]
|
|
2860
|
+
- Updated dependencies [74155c7]
|
|
2861
|
+
- Updated dependencies [b5f9397]
|
|
2862
|
+
- Updated dependencies [ed77493]
|
|
2863
|
+
- Updated dependencies [6908830]
|
|
2864
|
+
- Updated dependencies [8b06bba]
|
|
2865
|
+
- Updated dependencies [58a03d2]
|
|
2866
|
+
- Updated dependencies [2bacd1a]
|
|
2867
|
+
- Updated dependencies [e47b342]
|
|
2868
|
+
- Updated dependencies [4c54037]
|
|
2869
|
+
- Updated dependencies [dc530b4]
|
|
2870
|
+
- Updated dependencies [9f601e8]
|
|
2871
|
+
- Updated dependencies [6a9dec6]
|
|
2872
|
+
- Updated dependencies [0f7157b]
|
|
2873
|
+
- Updated dependencies [4dc1c7d]
|
|
2874
|
+
- Updated dependencies [d9bef45]
|
|
2875
|
+
- Updated dependencies [4dfd002]
|
|
2876
|
+
- Updated dependencies [f549a0d]
|
|
2877
|
+
- Updated dependencies [51c5227]
|
|
2878
|
+
- Updated dependencies [82da264]
|
|
2879
|
+
- Updated dependencies [77be690]
|
|
2880
|
+
- Updated dependencies [4ed7ed4]
|
|
2881
|
+
- Updated dependencies [9b9b70f]
|
|
2882
|
+
- Updated dependencies [f5a9bc2]
|
|
2883
|
+
- Updated dependencies [e59786e]
|
|
2884
|
+
- Updated dependencies [2fa4ca1]
|
|
2885
|
+
- Updated dependencies [bcf1112]
|
|
2886
|
+
- Updated dependencies [baeb4f0]
|
|
2887
|
+
- Updated dependencies [29488cc]
|
|
2888
|
+
- Updated dependencies [881a3cc]
|
|
2889
|
+
- Updated dependencies [f5a2320]
|
|
2890
|
+
- Updated dependencies [ad6317b]
|
|
2891
|
+
- Updated dependencies [811c30c]
|
|
2892
|
+
- Updated dependencies [a4a85c8]
|
|
2893
|
+
- Updated dependencies [859cb83]
|
|
2894
|
+
- Updated dependencies [07a4e26]
|
|
2895
|
+
- Updated dependencies [9774b78]
|
|
2896
|
+
- Updated dependencies [8a88885]
|
|
2897
|
+
- Updated dependencies [deb538f]
|
|
2898
|
+
- Updated dependencies [b49ccfd]
|
|
2899
|
+
- Updated dependencies [5b89711]
|
|
2900
|
+
- Updated dependencies [85d95e7]
|
|
2901
|
+
- Updated dependencies [08cd163]
|
|
2902
|
+
- Updated dependencies [0c8a22f]
|
|
2903
|
+
- Updated dependencies [5f7669e]
|
|
2904
|
+
- Updated dependencies [becbe53]
|
|
2905
|
+
- Updated dependencies [b127c8b]
|
|
2906
|
+
- Updated dependencies [763931e]
|
|
2907
|
+
- Updated dependencies [ec975f1]
|
|
2908
|
+
- Updated dependencies [168f60f]
|
|
2909
|
+
- Updated dependencies [b07d829]
|
|
2910
|
+
- Updated dependencies [de9af8a]
|
|
2911
|
+
- Updated dependencies [eb4204b]
|
|
2912
|
+
- Updated dependencies [a80302a]
|
|
2913
|
+
- Updated dependencies [a648e96]
|
|
2914
|
+
- Updated dependencies [a47ac06]
|
|
2915
|
+
- Updated dependencies [e4c61a7]
|
|
2916
|
+
- Updated dependencies [cc60165]
|
|
2917
|
+
- Updated dependencies [474f131]
|
|
2918
|
+
- Updated dependencies [081aa6f]
|
|
2919
|
+
- Updated dependencies [91f4c78]
|
|
2920
|
+
- Updated dependencies [050cd82]
|
|
2921
|
+
- Updated dependencies [4d552af]
|
|
2922
|
+
- Updated dependencies [44d677c]
|
|
2923
|
+
- Updated dependencies [c32944d]
|
|
2924
|
+
- Updated dependencies [1dd780f]
|
|
2925
|
+
- Updated dependencies [e8d0c21]
|
|
2926
|
+
- Updated dependencies [244ca86]
|
|
2927
|
+
- Updated dependencies [546ab3c]
|
|
2928
|
+
- Updated dependencies [c4df271]
|
|
2929
|
+
- Updated dependencies [c8d6f6e]
|
|
2930
|
+
- Updated dependencies [0b51bb6]
|
|
2931
|
+
- Updated dependencies [d9971d3]
|
|
2932
|
+
- Updated dependencies [7dc1067]
|
|
2933
|
+
- Updated dependencies [4f13be2]
|
|
2934
|
+
- Updated dependencies [a41ba5c]
|
|
2935
|
+
- Updated dependencies [189854c]
|
|
2936
|
+
- Updated dependencies [0e3a226]
|
|
2937
|
+
- Updated dependencies [92a67f2]
|
|
2938
|
+
- Updated dependencies [9136327]
|
|
2939
|
+
- Updated dependencies [bf0ae99]
|
|
2940
|
+
- Updated dependencies [abeb375]
|
|
2941
|
+
- Updated dependencies [cb3b6cd]
|
|
2942
|
+
- Updated dependencies [73b7234]
|
|
2943
|
+
- Updated dependencies [d2b97c3]
|
|
2944
|
+
- Updated dependencies [61cc079]
|
|
2945
|
+
- Updated dependencies [0e96e46]
|
|
2946
|
+
- Updated dependencies [c1d44f7]
|
|
2947
|
+
- Updated dependencies [59b794f]
|
|
2948
|
+
- Updated dependencies [ef4efa8]
|
|
2949
|
+
- Updated dependencies [cbb6a5c]
|
|
2950
|
+
- Updated dependencies [fc3a36a]
|
|
2951
|
+
- Updated dependencies [ab9fb5c]
|
|
2952
|
+
- Updated dependencies [69787f0]
|
|
2953
|
+
- Updated dependencies [5d022a1]
|
|
2954
|
+
- Updated dependencies [042b9ee]
|
|
2955
|
+
- Updated dependencies [f985b3f]
|
|
2956
|
+
- Updated dependencies [795b6e1]
|
|
2957
|
+
- Updated dependencies [d52d4fe]
|
|
2958
|
+
- Updated dependencies [742cebb]
|
|
2959
|
+
- Updated dependencies [175d789]
|
|
2960
|
+
- Updated dependencies [f549a0d]
|
|
2961
|
+
- Updated dependencies [427344c]
|
|
2962
|
+
- Updated dependencies [8af76ae]
|
|
2963
|
+
- Updated dependencies [1d4756e]
|
|
2964
|
+
- Updated dependencies [720c5ad]
|
|
2965
|
+
- Updated dependencies [a8d1e24]
|
|
2966
|
+
- Updated dependencies [b85cc54]
|
|
2967
|
+
- Updated dependencies [a36db28]
|
|
2968
|
+
- Updated dependencies [7a8476f]
|
|
2969
|
+
- Updated dependencies [518ca7a]
|
|
2970
|
+
- Updated dependencies [41642b0]
|
|
2971
|
+
- Updated dependencies [4cca74c]
|
|
2972
|
+
- Updated dependencies [88ef03e]
|
|
2973
|
+
- Updated dependencies [9a4932a]
|
|
2974
|
+
- Updated dependencies [3f8817a]
|
|
2975
|
+
- Updated dependencies [a2443e3]
|
|
2976
|
+
- Updated dependencies [e1554b1]
|
|
2977
|
+
- Updated dependencies [9e2caf3]
|
|
2978
|
+
- Updated dependencies [4856789]
|
|
2979
|
+
- Updated dependencies [81ce41a]
|
|
2980
|
+
- Updated dependencies [85e1e4e]
|
|
2981
|
+
- Updated dependencies [c3f4916]
|
|
2982
|
+
- Updated dependencies [55dbbba]
|
|
2983
|
+
- Updated dependencies [33e0385]
|
|
2984
|
+
- Updated dependencies [dac6a08]
|
|
2985
|
+
- Updated dependencies [72c3c86]
|
|
2986
|
+
- Updated dependencies [2d8dba3]
|
|
2987
|
+
- Updated dependencies [7f1a635]
|
|
2988
|
+
- Updated dependencies [2205363]
|
|
2989
|
+
- Updated dependencies [09fe58d]
|
|
2990
|
+
- Updated dependencies [f9fc874]
|
|
2991
|
+
- Updated dependencies [d62f8eb]
|
|
2992
|
+
- Updated dependencies [d0a5ceb]
|
|
2993
|
+
- Updated dependencies [a7586cd]
|
|
2994
|
+
- Updated dependencies [4c5e80e]
|
|
2995
|
+
- Updated dependencies [4b5702a]
|
|
2996
|
+
- Updated dependencies [011b386]
|
|
2997
|
+
- Updated dependencies [e18a162]
|
|
2998
|
+
- Updated dependencies [394b7a1]
|
|
2999
|
+
- Updated dependencies [ce92674]
|
|
3000
|
+
- Updated dependencies [cf2c9b7]
|
|
3001
|
+
- Updated dependencies [d127ff0]
|
|
3002
|
+
- Updated dependencies [36d90fc]
|
|
3003
|
+
- Updated dependencies [7777e8f]
|
|
3004
|
+
- Updated dependencies [9b86cf6]
|
|
3005
|
+
- Updated dependencies [d063a96]
|
|
3006
|
+
- Updated dependencies [8825a06]
|
|
3007
|
+
- Updated dependencies [5087ac6]
|
|
3008
|
+
- Updated dependencies [677b591]
|
|
3009
|
+
- Updated dependencies [cf7c694]
|
|
3010
|
+
- Updated dependencies [ddd0f06]
|
|
3011
|
+
- Updated dependencies [d77d1b7]
|
|
3012
|
+
- Updated dependencies [0f9faa2]
|
|
3013
|
+
- Updated dependencies [2d1ddf0]
|
|
3014
|
+
- Updated dependencies [354b00f]
|
|
3015
|
+
- Updated dependencies [3de535b]
|
|
3016
|
+
- Updated dependencies [fe2e15a]
|
|
3017
|
+
- Updated dependencies [5b79a34]
|
|
3018
|
+
- Updated dependencies [502564d]
|
|
3019
|
+
- Updated dependencies [603cab8]
|
|
3020
|
+
- Updated dependencies [c757854]
|
|
3021
|
+
- Updated dependencies [471839d]
|
|
3022
|
+
- Updated dependencies [507b92a]
|
|
3023
|
+
- Updated dependencies [b508244]
|
|
3024
|
+
- Updated dependencies [df95346]
|
|
3025
|
+
- Updated dependencies [3dede58]
|
|
3026
|
+
- Updated dependencies [c6b6bb4]
|
|
3027
|
+
- Updated dependencies [594508e]
|
|
3028
|
+
- Updated dependencies [7cf42fe]
|
|
3029
|
+
- Updated dependencies [5966c2a]
|
|
3030
|
+
- Updated dependencies [0045682]
|
|
3031
|
+
- Updated dependencies [7309c81]
|
|
3032
|
+
- Updated dependencies [2f59da0]
|
|
3033
|
+
- Updated dependencies [d56012f]
|
|
3034
|
+
- Updated dependencies [f78dd83]
|
|
3035
|
+
- Updated dependencies [a2cd18a]
|
|
3036
|
+
- Updated dependencies [9051802]
|
|
3037
|
+
- Updated dependencies [20bc1ec]
|
|
3038
|
+
- Updated dependencies [1c625ca]
|
|
3039
|
+
- Updated dependencies [2f8328c]
|
|
3040
|
+
- Updated dependencies [2a6c279]
|
|
3041
|
+
- Updated dependencies [8c8f0df]
|
|
3042
|
+
- Updated dependencies [8ad609c]
|
|
3043
|
+
- Updated dependencies [bbee302]
|
|
3044
|
+
- Updated dependencies [90c2b15]
|
|
3045
|
+
- Updated dependencies [4638aaa]
|
|
3046
|
+
- Updated dependencies [0222d3c]
|
|
3047
|
+
- Updated dependencies [08863dd]
|
|
3048
|
+
- Updated dependencies [f293d45]
|
|
3049
|
+
- Updated dependencies [56664f5]
|
|
3050
|
+
- Updated dependencies [71f205d]
|
|
3051
|
+
- Updated dependencies [f067930]
|
|
3052
|
+
- Updated dependencies [414395b]
|
|
3053
|
+
- Updated dependencies [42eeb7d]
|
|
3054
|
+
- Updated dependencies [31cbe90]
|
|
3055
|
+
- Updated dependencies [6b7129a]
|
|
3056
|
+
- Updated dependencies [97ace2a]
|
|
3057
|
+
- Updated dependencies [26e1029]
|
|
3058
|
+
- Updated dependencies [0a936ea]
|
|
3059
|
+
- Updated dependencies [90bbf25]
|
|
3060
|
+
- Updated dependencies [023c00b]
|
|
3061
|
+
- Updated dependencies [eb91eba]
|
|
3062
|
+
- Updated dependencies [42da73d]
|
|
3063
|
+
- Updated dependencies [01e124d]
|
|
3064
|
+
- Updated dependencies [ef7b5ef]
|
|
3065
|
+
- Updated dependencies [9514767]
|
|
3066
|
+
- Updated dependencies [8f20201]
|
|
3067
|
+
- Updated dependencies [155507e]
|
|
3068
|
+
- Updated dependencies [643b7c7]
|
|
3069
|
+
- Updated dependencies [7bba90b]
|
|
3070
|
+
- Updated dependencies [8813b90]
|
|
3071
|
+
- Updated dependencies [108ba8d]
|
|
3072
|
+
- Updated dependencies [2a5f04a]
|
|
3073
|
+
- Updated dependencies [4f740b0]
|
|
3074
|
+
- Updated dependencies [7ce02eb]
|
|
3075
|
+
- Updated dependencies [b4ad984]
|
|
3076
|
+
- Updated dependencies [a9f32df]
|
|
3077
|
+
- Updated dependencies [aeb9b27]
|
|
3078
|
+
- Updated dependencies [7d27da0]
|
|
3079
|
+
- Updated dependencies [1a15893]
|
|
3080
|
+
- Updated dependencies [b70e534]
|
|
3081
|
+
- Updated dependencies [7e05d8e]
|
|
3082
|
+
- Updated dependencies [8f1851e]
|
|
3083
|
+
- Updated dependencies [61ea810]
|
|
3084
|
+
- Updated dependencies [2233a85]
|
|
3085
|
+
- Updated dependencies [67452d1]
|
|
3086
|
+
- Updated dependencies [089767f]
|
|
3087
|
+
- Updated dependencies [a13827e]
|
|
3088
|
+
- Updated dependencies [66d99ec]
|
|
3089
|
+
- Updated dependencies [cb43296]
|
|
3090
|
+
- Updated dependencies [b61afc1]
|
|
3091
|
+
- Updated dependencies [79021fc]
|
|
3092
|
+
- Updated dependencies [7733604]
|
|
3093
|
+
- Updated dependencies [40e420f]
|
|
3094
|
+
- Updated dependencies [62dd69a]
|
|
3095
|
+
- Updated dependencies [d13004a]
|
|
3096
|
+
- Updated dependencies [e15e679]
|
|
3097
|
+
- Updated dependencies [2ab1257]
|
|
3098
|
+
- Updated dependencies [0fc6219]
|
|
3099
|
+
- Updated dependencies [061406d]
|
|
3100
|
+
- Updated dependencies [e4c8b6c]
|
|
3101
|
+
- Updated dependencies [acb10f6]
|
|
3102
|
+
- Updated dependencies [605e190]
|
|
3103
|
+
- Updated dependencies [c6c59f1]
|
|
3104
|
+
- Updated dependencies [b0e78a8]
|
|
3105
|
+
- Updated dependencies [f31cc8d]
|
|
3106
|
+
- Updated dependencies [f343dc4]
|
|
3107
|
+
- Updated dependencies [8269e32]
|
|
3108
|
+
- Updated dependencies [74f7339]
|
|
3109
|
+
- Updated dependencies [a6c35a2]
|
|
3110
|
+
- Updated dependencies [c2f1002]
|
|
3111
|
+
- Updated dependencies [4cc4fb7]
|
|
3112
|
+
- Updated dependencies [97b6658]
|
|
3113
|
+
- Updated dependencies [2c26040]
|
|
3114
|
+
- Updated dependencies [f758cec]
|
|
3115
|
+
- Updated dependencies [5b47ab5]
|
|
3116
|
+
- Updated dependencies [b09d8d9]
|
|
3117
|
+
- Updated dependencies [b09d8d9]
|
|
3118
|
+
- Updated dependencies [8675db6]
|
|
3119
|
+
- Updated dependencies [b09d8d9]
|
|
3120
|
+
- Updated dependencies [27358d5]
|
|
3121
|
+
- Updated dependencies [1c3da1f]
|
|
3122
|
+
- Updated dependencies [c1f344b]
|
|
3123
|
+
- Updated dependencies [3eb1b2b]
|
|
3124
|
+
- Updated dependencies [9c93465]
|
|
3125
|
+
- Updated dependencies [a34fd2e]
|
|
3126
|
+
- Updated dependencies [ebb209c]
|
|
3127
|
+
- Updated dependencies [76bcb83]
|
|
3128
|
+
- Updated dependencies [59b85c0]
|
|
3129
|
+
- Updated dependencies [889ae47]
|
|
3130
|
+
- Updated dependencies [4f4c3fb]
|
|
3131
|
+
- Updated dependencies [78f0be8]
|
|
3132
|
+
- Updated dependencies [6e357ed]
|
|
3133
|
+
- Updated dependencies [d6938bf]
|
|
3134
|
+
- Updated dependencies [35f7fb4]
|
|
3135
|
+
- Updated dependencies [0410522]
|
|
3136
|
+
- Updated dependencies [63b33e6]
|
|
3137
|
+
- Updated dependencies [f163028]
|
|
3138
|
+
- Updated dependencies [814db6d]
|
|
3139
|
+
- Updated dependencies [a5302c7]
|
|
3140
|
+
- Updated dependencies [31e0be9]
|
|
3141
|
+
- Updated dependencies [4bfd455]
|
|
3142
|
+
- Updated dependencies [ffd2ce2]
|
|
3143
|
+
- Updated dependencies [2a44c1d]
|
|
3144
|
+
- Updated dependencies [7084313]
|
|
3145
|
+
- Updated dependencies [f07808c]
|
|
3146
|
+
- Updated dependencies [7ffc3d3]
|
|
3147
|
+
- Updated dependencies [88346ba]
|
|
3148
|
+
- Updated dependencies [4631592]
|
|
3149
|
+
- Updated dependencies [62f8017]
|
|
3150
|
+
- Updated dependencies [32ff033]
|
|
3151
|
+
- Updated dependencies [a831df1]
|
|
3152
|
+
- Updated dependencies [f752ee3]
|
|
3153
|
+
- Updated dependencies [a1b61e0]
|
|
3154
|
+
- Updated dependencies [cd6b9f2]
|
|
3155
|
+
- Updated dependencies [2cb6d3c]
|
|
3156
|
+
- Updated dependencies [af2a095]
|
|
3157
|
+
- Updated dependencies [5ac93d4]
|
|
3158
|
+
- Updated dependencies [695cfbd]
|
|
3159
|
+
- Updated dependencies [0e043d8]
|
|
3160
|
+
- Updated dependencies [93f267f]
|
|
3161
|
+
- Updated dependencies [7445149]
|
|
3162
|
+
- Updated dependencies [ec796d5]
|
|
3163
|
+
- Updated dependencies [071d0dc]
|
|
3164
|
+
- Updated dependencies [0024abf]
|
|
3165
|
+
- Updated dependencies [8dd98bf]
|
|
3166
|
+
- Updated dependencies [e87fea1]
|
|
3167
|
+
- Updated dependencies [c65e529]
|
|
3168
|
+
- Updated dependencies [0848bea]
|
|
3169
|
+
- Updated dependencies [d51bed2]
|
|
3170
|
+
- Updated dependencies [dadd1ad]
|
|
3171
|
+
- Updated dependencies [acbf364]
|
|
3172
|
+
- Updated dependencies [3ca34c1]
|
|
3173
|
+
- Updated dependencies [7adc841]
|
|
3174
|
+
- Updated dependencies [239c3a3]
|
|
3175
|
+
- Updated dependencies [b8b3c64]
|
|
3176
|
+
- Updated dependencies [2f2e63c]
|
|
3177
|
+
- Updated dependencies [4845f85]
|
|
3178
|
+
- Updated dependencies [486d526]
|
|
3179
|
+
- Updated dependencies [94a0bbc]
|
|
3180
|
+
- Updated dependencies [d6bfb3d]
|
|
3181
|
+
- Updated dependencies [8a9c079]
|
|
3182
|
+
- Updated dependencies [7b005b4]
|
|
3183
|
+
- Updated dependencies [cc3555e]
|
|
3184
|
+
- Updated dependencies [a2266a6]
|
|
3185
|
+
- Updated dependencies [d25a0ec]
|
|
3186
|
+
- Updated dependencies [89d7b35]
|
|
3187
|
+
- Updated dependencies [94f7b6a]
|
|
3188
|
+
- Updated dependencies [5c94f83]
|
|
3189
|
+
- Updated dependencies [ea936f3]
|
|
3190
|
+
- Updated dependencies [0c0fbd9]
|
|
3191
|
+
- Updated dependencies [667b83e]
|
|
3192
|
+
- Updated dependencies [f3141d8]
|
|
3193
|
+
- Updated dependencies [7687f7b]
|
|
3194
|
+
- Updated dependencies [5a84d41]
|
|
3195
|
+
- Updated dependencies [fd3013a]
|
|
3196
|
+
- Updated dependencies [85ec26d]
|
|
3197
|
+
- Updated dependencies [73e576f]
|
|
3198
|
+
- Updated dependencies [f6476fc]
|
|
3199
|
+
- Updated dependencies [69ac82c]
|
|
3200
|
+
- Updated dependencies [4ac12ef]
|
|
3201
|
+
- Updated dependencies [833ed84]
|
|
3202
|
+
- Updated dependencies [a18abf3]
|
|
3203
|
+
- Updated dependencies [c6a4eeb]
|
|
3204
|
+
- Updated dependencies [1659072]
|
|
3205
|
+
- Updated dependencies [f450ae7]
|
|
3206
|
+
- Updated dependencies [abceb0d]
|
|
3207
|
+
- Updated dependencies [627b188]
|
|
3208
|
+
- Updated dependencies [8d4eae7]
|
|
3209
|
+
- Updated dependencies [c5a5996]
|
|
3210
|
+
- Updated dependencies [0c302a7]
|
|
3211
|
+
- Updated dependencies [b88f5e8]
|
|
3212
|
+
- Updated dependencies [65a3a84]
|
|
3213
|
+
- Updated dependencies [6633337]
|
|
3214
|
+
- Updated dependencies [21676eb]
|
|
3215
|
+
- Updated dependencies [e9cb9ab]
|
|
3216
|
+
- Updated dependencies [42cc219]
|
|
3217
|
+
- Updated dependencies [d7e0b42]
|
|
3218
|
+
- Updated dependencies [3510e4a]
|
|
3219
|
+
- Updated dependencies [f00d8d4]
|
|
3220
|
+
- Updated dependencies [5326b36]
|
|
3221
|
+
- Updated dependencies [aa4b90d]
|
|
3222
|
+
- Updated dependencies [ccd9397]
|
|
3223
|
+
- Updated dependencies [503be86]
|
|
3224
|
+
- Updated dependencies [54299ca]
|
|
3225
|
+
- Updated dependencies [ae490ef]
|
|
3226
|
+
- Updated dependencies [e124711]
|
|
3227
|
+
- Updated dependencies [dc61def]
|
|
3228
|
+
- Updated dependencies [bca935b]
|
|
3229
|
+
- Updated dependencies [c54c822]
|
|
3230
|
+
- Updated dependencies [8dcc0f5]
|
|
3231
|
+
- Updated dependencies [75b9e51]
|
|
3232
|
+
- Updated dependencies [f61c8cf]
|
|
3233
|
+
- Updated dependencies [e3ef52b]
|
|
3234
|
+
- Updated dependencies [0a2f233]
|
|
3235
|
+
- Updated dependencies [8621cdd]
|
|
3236
|
+
- Updated dependencies [251e888]
|
|
3237
|
+
- Updated dependencies [07f1822]
|
|
3238
|
+
- Updated dependencies [e336549]
|
|
3239
|
+
- Updated dependencies [3bb9340]
|
|
3240
|
+
- Updated dependencies [1e604c4]
|
|
3241
|
+
- Updated dependencies [04fab5e]
|
|
3242
|
+
- Updated dependencies [183b4c4]
|
|
3243
|
+
- Updated dependencies [7f713b6]
|
|
3244
|
+
- Updated dependencies [d40f43a]
|
|
3245
|
+
- Updated dependencies [2fdb36e]
|
|
3246
|
+
- Updated dependencies [6f23667]
|
|
3247
|
+
- Updated dependencies [cde1975]
|
|
3248
|
+
- Updated dependencies [0bc685a]
|
|
3249
|
+
- Updated dependencies [20526f5]
|
|
3250
|
+
- Updated dependencies [efedd28]
|
|
3251
|
+
- Updated dependencies [5d21a48]
|
|
3252
|
+
- Updated dependencies [5278e11]
|
|
3253
|
+
- Updated dependencies [c5eef1d]
|
|
3254
|
+
- Updated dependencies [e5e7ee0]
|
|
3255
|
+
- Updated dependencies [23dba62]
|
|
3256
|
+
- Updated dependencies [e0f300b]
|
|
3257
|
+
- Updated dependencies [761a0ba]
|
|
3258
|
+
- Updated dependencies [c960170]
|
|
3259
|
+
- Updated dependencies [19365b7]
|
|
3260
|
+
- Updated dependencies [ba98e26]
|
|
3261
|
+
- Updated dependencies [b7ed26d]
|
|
3262
|
+
- Updated dependencies [a2ebea2]
|
|
3263
|
+
- Updated dependencies [800bdb0]
|
|
3264
|
+
- Updated dependencies [9d4dfc4]
|
|
3265
|
+
- Updated dependencies [1059965]
|
|
3266
|
+
- Updated dependencies [def5919]
|
|
3267
|
+
- Updated dependencies [60b672e]
|
|
3268
|
+
- Updated dependencies [6b441a8]
|
|
3269
|
+
- Updated dependencies [ce0cfe9]
|
|
3270
|
+
- Updated dependencies [04f1182]
|
|
3271
|
+
- Updated dependencies [be87153]
|
|
3272
|
+
- Updated dependencies [dd0f681]
|
|
3273
|
+
- Updated dependencies [60f0dd8]
|
|
3274
|
+
- Updated dependencies [a87c5cd]
|
|
3275
|
+
- Updated dependencies [a47f338]
|
|
3276
|
+
- Updated dependencies [b3a3d83]
|
|
3277
|
+
- Updated dependencies [7a55913]
|
|
3278
|
+
- Updated dependencies [35accbf]
|
|
3279
|
+
- Updated dependencies [6038de7]
|
|
3280
|
+
- Updated dependencies [fc5f536]
|
|
3281
|
+
- Updated dependencies [5647006]
|
|
3282
|
+
- Updated dependencies [e654bfd]
|
|
3283
|
+
- Updated dependencies [01a7337]
|
|
3284
|
+
- Updated dependencies [b45c71e]
|
|
3285
|
+
- Updated dependencies [f8cfbb4]
|
|
3286
|
+
- Updated dependencies [6e6c872]
|
|
3287
|
+
- Updated dependencies [2598216]
|
|
3288
|
+
- Updated dependencies [11949fc]
|
|
3289
|
+
- Updated dependencies [2c7e62d]
|
|
3290
|
+
- Updated dependencies [eb95d97]
|
|
3291
|
+
- Updated dependencies [b098b0e]
|
|
3292
|
+
- Updated dependencies [4d00b13]
|
|
3293
|
+
- Updated dependencies [1363084]
|
|
3294
|
+
- Updated dependencies [fa5758e]
|
|
3295
|
+
- Updated dependencies [38f7e4f]
|
|
3296
|
+
- Updated dependencies [eb7613c]
|
|
3297
|
+
- Updated dependencies [c57f3cf]
|
|
3298
|
+
- Updated dependencies [ecc9110]
|
|
3299
|
+
- Updated dependencies [e4c2dc8]
|
|
3300
|
+
- Updated dependencies [97faca3]
|
|
3301
|
+
- Updated dependencies [57bab76]
|
|
3302
|
+
- Updated dependencies [c89d18c]
|
|
3303
|
+
- Updated dependencies [1bd2795]
|
|
3304
|
+
- Updated dependencies [f7bd4e2]
|
|
3305
|
+
- Updated dependencies [361bd5b]
|
|
3306
|
+
- Updated dependencies [aac90a5]
|
|
3307
|
+
- Updated dependencies [3da3da5]
|
|
3308
|
+
- Updated dependencies [1e6ab15]
|
|
3309
|
+
- Updated dependencies [b90086a]
|
|
3310
|
+
- Updated dependencies [8186a70]
|
|
3311
|
+
- Updated dependencies [a329cca]
|
|
3312
|
+
- Updated dependencies [c87ef70]
|
|
3313
|
+
- Updated dependencies [3cb0618]
|
|
3314
|
+
- Updated dependencies [32a0874]
|
|
3315
|
+
- Updated dependencies [6eec18c]
|
|
3316
|
+
- Updated dependencies [4d7bebf]
|
|
3317
|
+
- Updated dependencies [821ac7a]
|
|
3318
|
+
- Updated dependencies [8f81731]
|
|
3319
|
+
- Updated dependencies [7055c22]
|
|
3320
|
+
- Updated dependencies [785a748]
|
|
3321
|
+
- Updated dependencies [3af0354]
|
|
3322
|
+
- Updated dependencies [866ff16]
|
|
3323
|
+
- Updated dependencies [5a85e67]
|
|
3324
|
+
- Updated dependencies [8b50cb3]
|
|
3325
|
+
- Updated dependencies [a0fdc56]
|
|
3326
|
+
- Updated dependencies [b95577a]
|
|
3327
|
+
- Updated dependencies [0dcbc11]
|
|
3328
|
+
- Updated dependencies [d88f3e9]
|
|
3329
|
+
- Updated dependencies [ad5fe25]
|
|
3330
|
+
- Updated dependencies [c183a12]
|
|
3331
|
+
- Updated dependencies [83c161f]
|
|
3332
|
+
- Updated dependencies [d8c4957]
|
|
3333
|
+
- Updated dependencies [b9f930b]
|
|
3334
|
+
- Updated dependencies [f24cb83]
|
|
3335
|
+
- Updated dependencies [5dbbb92]
|
|
3336
|
+
- Updated dependencies [ea90179]
|
|
3337
|
+
- Updated dependencies [1818998]
|
|
3338
|
+
- Updated dependencies [ce92674]
|
|
3339
|
+
- Updated dependencies [5ef0b5b]
|
|
3340
|
+
- Updated dependencies [8c2db68]
|
|
3341
|
+
- Updated dependencies [22b5e54]
|
|
3342
|
+
- Updated dependencies [0166bd5]
|
|
3343
|
+
- Updated dependencies [8064b07]
|
|
3344
|
+
- Updated dependencies [09ee21c]
|
|
3345
|
+
- Updated dependencies [4a56dbd]
|
|
3346
|
+
- Updated dependencies [289d04a]
|
|
3347
|
+
- Updated dependencies [f549a0d]
|
|
3348
|
+
- Updated dependencies [48fbacb]
|
|
3349
|
+
- Updated dependencies [06df4fa]
|
|
3350
|
+
- Updated dependencies [3fc2e48]
|
|
3351
|
+
- Updated dependencies [c9b809f]
|
|
3352
|
+
- Updated dependencies [e8f435c]
|
|
3353
|
+
- Updated dependencies [32386f8]
|
|
3354
|
+
- Updated dependencies [9b702dc]
|
|
3355
|
+
- Updated dependencies [ab16331]
|
|
3356
|
+
- Updated dependencies [41610f6]
|
|
3357
|
+
- Updated dependencies [69f1dfd]
|
|
3358
|
+
- Updated dependencies [bbe05de]
|
|
3359
|
+
- Updated dependencies [355e951]
|
|
3360
|
+
- Updated dependencies [a1dd1e4]
|
|
3361
|
+
- Updated dependencies [dadb43f]
|
|
3362
|
+
- @objectstack/spec@17.0.0
|
|
3363
|
+
|
|
3
3364
|
## 17.0.0-rc.6
|
|
4
3365
|
|
|
5
3366
|
### Minor Changes
|