@objectstack/core 17.2.0 → 17.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1450 -0
- package/dist/index.cjs +862 -302
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1195 -115
- package/dist/index.d.ts +1195 -115
- package/dist/index.js +822 -291
- package/dist/index.js.map +1 -1
- package/package.json +8 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,1455 @@
|
|
|
1
1
|
# @objectstack/core
|
|
2
2
|
|
|
3
|
+
## 17.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 655b106: fix(metadata): register a `packages[]` artifact per package at the metadata door so every object has one owner across every door (#14599)
|
|
8
|
+
|
|
9
|
+
A release artifact carrying `packages[]` (ADR-0130 D4) was read at the metadata
|
|
10
|
+
door as if it carried one package: `MetadataPlugin._parseAndRegisterArtifact`
|
|
11
|
+
iterated the **flattened top level** and stamped every item with the artifact's
|
|
12
|
+
own `manifest.id`. For an artifact composed with `composeStacks(…, { manifest:
|
|
13
|
+
'preserve' })` that id is one arbitrary member's — `selectManifest`'s `'last'`
|
|
14
|
+
pick — so a two-package artifact registered the **module's** object under the
|
|
15
|
+
**App** package's identity, while the ObjectQL load path, reading the same
|
|
16
|
+
artifact's `packages[]`, owned it under the module's.
|
|
17
|
+
|
|
18
|
+
The platform then held two answers to "who owns this object", and which one a
|
|
19
|
+
consumer saw depended on the door it went through. Measured on a real boot of
|
|
20
|
+
`examples/app-multi-package`:
|
|
21
|
+
|
|
22
|
+
- `GET /api/v1/meta/object` served `crm_order` **twice** — the list merge keys
|
|
23
|
+
slots by `${packageId}${name}`, so the two differently-attributed copies
|
|
24
|
+
landed in two slots;
|
|
25
|
+
- `GET /api/v1/meta/object?package=<the App package>` returned the **module's**
|
|
26
|
+
object, because the App-stamped copy was re-ingested into the registry as that
|
|
27
|
+
package's contribution;
|
|
28
|
+
- the layers door named the App package while the item door and
|
|
29
|
+
`GET /api/v1/packages` named the module;
|
|
30
|
+
- Studio's Data pillar for the App package listed the module's object — ADR-0130
|
|
31
|
+
Consequences §1.3a ("Studio's scope is the package") did not hold.
|
|
32
|
+
|
|
33
|
+
**The door now reads both shapes, and attributes every item to the body it was
|
|
34
|
+
found in.** `packages` present → each assembled package body's collections are
|
|
35
|
+
registered stamped with **that body's** id; `packages` absent → the single
|
|
36
|
+
`manifest` branch runs exactly as before (D7). The owner is read off the body an
|
|
37
|
+
item was found in — never reverse-derived by matching a top-level item's name
|
|
38
|
+
against a name-to-package index, which would be the second metadata-identity
|
|
39
|
+
resolution path #14512's triage rejected by name.
|
|
40
|
+
|
|
41
|
+
**Ordering and the entry gate are reused, not re-derived (D5).** The door calls
|
|
42
|
+
the same `resolveArtifactPackageOrder` the ObjectQL load path calls, so the two
|
|
43
|
+
readers of one `packages[]` cannot disagree about the registration order **or**
|
|
44
|
+
about which artifacts are loadable at all.
|
|
45
|
+
|
|
46
|
+
⚠️ **`resolveArtifactPackageOrder` / `artifactPackageId` moved to
|
|
47
|
+
`@objectstack/core`** — hence the `minor` there. They were in
|
|
48
|
+
`@objectstack/objectql`, which **depends on** `@objectstack/metadata`, so the
|
|
49
|
+
metadata door could not import them from where they lived; `@objectstack/core`
|
|
50
|
+
already owns `resolvePluginOrder` and is already a dependency of both readers,
|
|
51
|
+
so hosting them there adds **no edge** to the package graph. `@objectstack/objectql`
|
|
52
|
+
re-exports both under their existing names — its published surface is unchanged,
|
|
53
|
+
which is why it is graded `patch`. `@objectstack/runtime` is `patch` for the
|
|
54
|
+
dispatcher error vocabulary's `file:` anchors, repointed at the new path.
|
|
55
|
+
|
|
56
|
+
**Single-package artifacts are byte-for-byte unaffected (D7)**, measured rather
|
|
57
|
+
than asserted: the whole `manager.register` sequence for a single-`manifest`
|
|
58
|
+
artifact — every call, in order, with the id and version each item was stamped
|
|
59
|
+
with — is pinned as a literal in
|
|
60
|
+
`packages/metadata/src/plugin-artifact-packages-attribution.test.ts` and was
|
|
61
|
+
recorded identically on both legs of the ablation. A real boot of
|
|
62
|
+
`examples/app-todo` answers every door identically before and after.
|
|
63
|
+
|
|
64
|
+
**Nothing a booted instance can see today disappears.** Every live
|
|
65
|
+
`ARTIFACT_FIELD_TO_TYPE` key is a member of `AssembledPackageBodySchema`
|
|
66
|
+
(measured, not assumed), so iterating bodies loses no collection; and because
|
|
67
|
+
`packages` composes by `concat`, an artifact whose top level carries a
|
|
68
|
+
definition no package body repeats keeps it — registered once, attributed to the
|
|
69
|
+
artifact's own identity, and logged, because it means the artifact's two halves
|
|
70
|
+
disagree about what it ships.
|
|
71
|
+
|
|
72
|
+
⛔ The **producer** half is untouched: `composeStacks` and `os build` keep
|
|
73
|
+
emitting the flattened top level alongside `packages[]`. Whether they should is
|
|
74
|
+
#14512's decision, not this door's.
|
|
75
|
+
- 4bd6faa: feat(engine,core,cluster): the authorization-cache invalidation substrate — an engine-seam write epoch, the `authz.invalidated` channel, and a non-optional boot-time posture statement (#11968)
|
|
76
|
+
|
|
77
|
+
The substrate step (§10.3) of the accepted #11633 cross-request caching design
|
|
78
|
+
(maintainer acceptance 2026-08-25, Fork 2 → B). It ships the invalidation
|
|
79
|
+
machinery once, before the grants cache (#11967) that will consume it, so that
|
|
80
|
+
leg does not carry it. **Nothing here caches anything.**
|
|
81
|
+
|
|
82
|
+
- **`ObjectQL.writeEpoch`** — a monotonic counter advanced by the engine
|
|
83
|
+
middleware seam on every `insert` / `update` / `delete`, ahead of the whole
|
|
84
|
+
chain (and so ahead of any `isSystem` bypass a middleware applies). It
|
|
85
|
+
generalises the private counter `@objectstack/plugin-security` has carried
|
|
86
|
+
since #10757: the mechanism was always the engine's, and hoisting it lets a
|
|
87
|
+
second consumer share **one** signal instead of minting a parallel one that
|
|
88
|
+
watches a different set of writes. A seam rather than a list of call sites,
|
|
89
|
+
because a forgotten call site fails as silent over-permission and writing
|
|
90
|
+
through the engine is the only way to write at all — including better-auth's
|
|
91
|
+
own adapter.
|
|
92
|
+
- **`authz.invalidated`** — one new channel on the existing `IPubSub`, bridged
|
|
93
|
+
in the shape `MetadataClusterBridgePlugin` already uses. ⭐ **The TTL a
|
|
94
|
+
consuming cache carries is the correctness contract; this channel is not.** No
|
|
95
|
+
shipped driver delivers better than at-most-once (`cluster.mdx` §4.2), so a
|
|
96
|
+
missed message is *expected*, the bridge stays out of the write path (a
|
|
97
|
+
publish failure is logged and swallowed, never awaited by the writer), and the
|
|
98
|
+
channel only moves the *typical* convergence from one TTL to one network hop.
|
|
99
|
+
That statement lives in the code at the channel, where a consumer reads it.
|
|
100
|
+
- **The boot-time posture statement** — non-optional by the ruling. Whenever a
|
|
101
|
+
grants cache is enabled (`OS_AUTHZ_GRANTS_CACHE_TTL_MS` > 0) and there is no
|
|
102
|
+
cross-node invalidation bus, the deployment is told so at `warn`, every boot,
|
|
103
|
+
naming the window it accepted and the remedy. It is a statement, not a
|
|
104
|
+
refusal: a TTL-bounded per-process cache is a legitimate configuration. It is
|
|
105
|
+
said out loud because a silently-absent invalidation bridge is how a security
|
|
106
|
+
control gets disabled with nobody noticing (#4785). The in-process `memory`
|
|
107
|
+
driver counts as **no** bus — a cluster service exists on the shipped default
|
|
108
|
+
while fanning out to nobody, which is the case a "is a cluster service
|
|
109
|
+
registered?" check answers `yes` to and is wrong about.
|
|
110
|
+
|
|
111
|
+
**Runtime behaviour is unchanged.** With no cache consumer the epoch has zero
|
|
112
|
+
subscribers, so nothing is published and nothing is invalidated; with the
|
|
113
|
+
shipped default TTL of `0` the bridge attaches nothing and logs nothing above
|
|
114
|
+
`debug`. The one composition change worth naming: `Runtime` now registers
|
|
115
|
+
`AuthzClusterBridgePlugin` **unconditionally**, including under `cluster: false`
|
|
116
|
+
— that is not an oversight, it is the loudest case the posture check has, and
|
|
117
|
+
skipping it there would put the statement's absence exactly where the missing
|
|
118
|
+
bus is.
|
|
119
|
+
|
|
120
|
+
`@objectstack/plugin-security` is a `patch`: its permission-set memo now reads
|
|
121
|
+
the engine's epoch when the wired engine exposes one and keeps its private
|
|
122
|
+
counter otherwise (test doubles, embeddings). The covered set of writes is
|
|
123
|
+
identical — the plugin's own middleware was already global — and it is now
|
|
124
|
+
identical *by construction* rather than by two files agreeing on which
|
|
125
|
+
operations count.
|
|
126
|
+
- 86cbe37: feat(core): cross-request authorization grants cache — leg B of #11633 (#11971)
|
|
127
|
+
|
|
128
|
+
`resolveUserAuthzGrants` can now cache its resolved envelope across requests,
|
|
129
|
+
governed by `OS_AUTHZ_GRANTS_CACHE_TTL_MS`. **The default is `0` — the cache is
|
|
130
|
+
OFF and the shipped behaviour is unchanged** (Fork 4 of the accepted #11633
|
|
131
|
+
design): a deployment that enables it accepts the configured staleness window
|
|
132
|
+
explicitly, and the boot-time posture statement says so out loud when no
|
|
133
|
+
cross-node invalidation bus is attached.
|
|
134
|
+
|
|
135
|
+
With the cache on:
|
|
136
|
+
|
|
137
|
+
- **Coarse write-invalidation (Fork 1A).** Any engine write to a watched
|
|
138
|
+
authorization object (`sys_member`, `sys_user_position`,
|
|
139
|
+
`sys_user_permission_set`, `sys_position`, `sys_position_permission_set`,
|
|
140
|
+
`sys_permission_set`, `sys_user`) retires every entry on the writing node —
|
|
141
|
+
a grant/revoke/role change is observed by the very next request there, by
|
|
142
|
+
invalidation and not by TTL. `metadata.changed` and peer-node
|
|
143
|
+
`authz.invalidated` hints retire wholesale via the engine write epoch.
|
|
144
|
+
`sys_session` is deliberately not watched (its once-a-minute
|
|
145
|
+
`last_activity_at` cadence would turn the cache into a non-cache).
|
|
146
|
+
- **Expiry-boundary rule.** Entries expire at `min(ttl, nextBoundary)`, where
|
|
147
|
+
`nextBoundary` is the earliest upcoming ADR-0091 `valid_from`/`valid_until`
|
|
148
|
+
among the rows consulted — a validity window flipping is a permission change
|
|
149
|
+
with no write anywhere, so the timer is the only mechanism for that class.
|
|
150
|
+
- **Ruled bypass list.** The permission explainer
|
|
151
|
+
(`plugin-security` `buildContextForUser`) and `runAs:'user'` automation runs
|
|
152
|
+
(`service-automation`) always resolve fresh, and never populate the cache.
|
|
153
|
+
- The TTL remains the correctness contract; the `authz.invalidated` bus only
|
|
154
|
+
narrows the typical cross-node window (no shipped driver exceeds
|
|
155
|
+
at-most-once delivery).
|
|
156
|
+
- 6a180e4: fix(core,rest,services)!: a permission-store read failure now fails LOUD instead of resolving as an authenticated caller holding zero capabilities (#13279)
|
|
157
|
+
|
|
158
|
+
**BREAKING** runtime behaviour change on the shared authorization resolver,
|
|
159
|
+
shipped as `minor` under the repo's launch-window convention.
|
|
160
|
+
|
|
161
|
+
`resolveAuthzContext`'s per-read helper `tryFind` answered a THROWN read exactly
|
|
162
|
+
the way it answered an EMPTY one: `[]`. So an outage of the permission store
|
|
163
|
+
resolved as a well-formed context for an authenticated principal holding no
|
|
164
|
+
capabilities, and the package-management door answered
|
|
165
|
+
`403 FORBIDDEN` — "Reading packages requires the `studio.access` or
|
|
166
|
+
`setup.access` capability." That answer was measured byte-identical
|
|
167
|
+
(`JSON.stringify` equal, against a control that separates two answers which do
|
|
168
|
+
differ) to what a caller who genuinely holds nothing receives. An administrator
|
|
169
|
+
was told they lack a capability, during an outage of the store that holds the
|
|
170
|
+
capability.
|
|
171
|
+
|
|
172
|
+
Maintainer ruling 2026-08-30, verbatim 「第一批其余同意」: `tryFind` 区分「无行」
|
|
173
|
+
与「读失败」,读失败 fail-loud —— 权限库不可达时不再解析为「已认证零能力」,而是
|
|
174
|
+
响亮拒绝(与真实能力拒绝的 403 可区分)。
|
|
175
|
+
|
|
176
|
+
Second maintainer ruling the same day (第 5 场总监席决裁批 #9, verbatim 「同意」),
|
|
177
|
+
after implementing the first one showed that "the read failed" is two facts:
|
|
178
|
+
采**选项 A** —— 把 `isMissingTableError` 从 `@objectstack/metadata` 迁至
|
|
179
|
+
`@objectstack/types`(core 已依赖),metadata 保留 re-export 兼容;`tryFind` 仅对
|
|
180
|
+
**未被判定为「表未 provision」**的读失败抛 `AuthzStoreUnavailableError`。
|
|
181
|
+
|
|
182
|
+
**What changed.** A permission-store read that is issued and throws now raises
|
|
183
|
+
`AuthzStoreUnavailableError`, which carries the EXISTING ADR-0112 wire code
|
|
184
|
+
`SERVICE_UNAVAILABLE` and status `503`. No code is added to the closed wire
|
|
185
|
+
vocabulary and no response envelope gains or loses a key — only which declared
|
|
186
|
+
code an outage selects. Doors that map thrown errors through
|
|
187
|
+
`resolveThrownHttpError` answer 503 with no per-door change.
|
|
188
|
+
|
|
189
|
+
**What did NOT change**, and is pinned:
|
|
190
|
+
|
|
191
|
+
- A reachable, genuinely EMPTY store (reads return no rows) still resolves to
|
|
192
|
+
zero capabilities.
|
|
193
|
+
- A genuine capability denial still answers `403 FORBIDDEN` with its message.
|
|
194
|
+
- An ABSENT engine (`ql` unwired, so no read is ever issued) still resolves to
|
|
195
|
+
an empty-but-valid envelope.
|
|
196
|
+
- Anonymous requests never reach the store, so an outage cannot make them loud.
|
|
197
|
+
- A REAL engine whose `sys_*` tables were never provisioned resolves to zero
|
|
198
|
+
capabilities, quietly — pinned to be byte-identical to the empty-store
|
|
199
|
+
envelope, in every dialect spelling and in the production wrapper shape where
|
|
200
|
+
the driver's phrase is on `cause` rather than the outer message.
|
|
201
|
+
|
|
202
|
+
**The boundary between the two kinds of read failure.** An earlier revision of
|
|
203
|
+
this changeset claimed "embedders without a data plane are unaffected". That
|
|
204
|
+
claim was too broad; it is retracted here, and the gap it named is now closed
|
|
205
|
+
rather than merely disclosed. A read also throws when the table was never
|
|
206
|
+
PROVISIONED — a real engine, wired and reachable, whose `sys_*` tables were
|
|
207
|
+
never created — and that is a supported deployment shape, not an outage. There
|
|
208
|
+
"zero capabilities" is the TRUE answer rather than a fabrication: nothing is
|
|
209
|
+
provisioned, so nothing was withheld. Only an UNREACHABLE store — the ruling's
|
|
210
|
+
own word 不可达 — leaves the capability set unknown, and only an unknown answer
|
|
211
|
+
may not be reported as a denial.
|
|
212
|
+
|
|
213
|
+
Treating the two alike was measured, not theorised: it turned four CI suites
|
|
214
|
+
red, all from `no such table` on `sys_user` / `sys_member` /
|
|
215
|
+
`sys_user_position` / `sys_user_permission_set`. Ordinary CRUD in
|
|
216
|
+
`@objectstack/client` answered `503`; batch validation errors that owe `400`
|
|
217
|
+
answered `503`, because authorization refused before validation ran; runtime
|
|
218
|
+
notifications answered `401` where authenticated callers must be served `200`;
|
|
219
|
+
and two `.integration.test.ts` noise guards reported that the driver and engine
|
|
220
|
+
diagnostics for `sys_position` stopped being emitted — the eager throw aborted
|
|
221
|
+
the resolution before that later read was ever issued, so a change made to stop
|
|
222
|
+
a failed read being silent had made two other channels silent.
|
|
223
|
+
|
|
224
|
+
`tryFind` therefore raises `AuthzStoreUnavailableError` only for a read failure
|
|
225
|
+
that is NOT positively identified as an unprovisioned table.
|
|
226
|
+
|
|
227
|
+
**`isMissingTableError` moved to `@objectstack/types`.** The classifier that
|
|
228
|
+
draws that boundary already existed and was already right — driver-code based
|
|
229
|
+
rather than prose-sniffing, documented so that "cannot say" never means "be
|
|
230
|
+
loud". It lived in `@objectstack/metadata`, which DEPENDS ON `@objectstack/core`,
|
|
231
|
+
so the resolver could not import it. Rather than keep a second copy of a
|
|
232
|
+
security-relevant predicate, the ruling relocated the one classifier to
|
|
233
|
+
`@objectstack/types` — the package core already depends on, and the repo's own
|
|
234
|
+
stated Home rule for a cross-package error predicate ("every consumer of the
|
|
235
|
+
question already depends on it, so adopting the predicate never adds an edge",
|
|
236
|
+
`packages/types/src/unique-violation.ts`). `@objectstack/metadata/errors` still
|
|
237
|
+
exports `isMissingTableError`, re-exported from the new home, so no consumer of
|
|
238
|
+
that published subpath changes.
|
|
239
|
+
|
|
240
|
+
Its sibling `isSchemaAlreadyExistsError` moved with it — the two are not two
|
|
241
|
+
modules but two signatures over one matcher, and separating them would have
|
|
242
|
+
meant re-rolling the matcher, which is the duplication the module exists to
|
|
243
|
+
prevent. Both are now exported from `@objectstack/types`; the metadata subpath
|
|
244
|
+
deliberately still publishes only `isMissingTableError`, which is the only one
|
|
245
|
+
anything imports through it.
|
|
246
|
+
|
|
247
|
+
⚠️ **Signed-off risk, recorded because it is load-bearing.** Gating loudness on
|
|
248
|
+
a driver-error predicate was approved with its false-positive direction stated:
|
|
249
|
+
mis-reading a genuine outage as "table not provisioned" silently restores the
|
|
250
|
+
quiet 403 this change removes, with no thrown error and no other failing test.
|
|
251
|
+
That direction is accepted, not overlooked — the predicate keys on driver codes,
|
|
252
|
+
SQLSTATEs and errnos first, excludes the known superstring traps up front, and
|
|
253
|
+
returns `false` for anything it does not positively recognise, so an
|
|
254
|
+
unrecognised outage stays loud by default. The risk is written beside the
|
|
255
|
+
predicate in `resolve-authz-context.ts` and both directions are pinned by name
|
|
256
|
+
in `authz-store-unavailable.test.ts`. ⛔ Do not widen `isMissingTableError` to
|
|
257
|
+
make a first boot quieter: every widening moves outages into the quiet branch.
|
|
258
|
+
|
|
259
|
+
**All-transport, not just REST.** Every transport authorizing through
|
|
260
|
+
`resolveAuthzContext` inherits this. Six of the eight production transports
|
|
261
|
+
wrapped the call in a fail-closed `catch` that would have re-silenced the
|
|
262
|
+
outage — measured, not assumed: with the resolver loud but the nets untouched,
|
|
263
|
+
the package door answered `401`, i.e. the outage merely changed disguises. Those
|
|
264
|
+
`catch` blocks now re-raise via `isAuthzStoreUnavailableError` and keep their
|
|
265
|
+
previous behaviour for every other fault. The transport set is rebuilt from
|
|
266
|
+
source and audited for set equality on every test run, so a transport added
|
|
267
|
+
later cannot inherit the old silence unnoticed.
|
|
268
|
+
|
|
269
|
+
Callers that treat any throw from `resolveAuthzContext` as "anonymous" should
|
|
270
|
+
re-raise `isAuthzStoreUnavailableError(err)` instead: degrading it restores the
|
|
271
|
+
disguise this removes.
|
|
272
|
+
|
|
273
|
+
<!-- adr-0087: not-required (runtime-interface-only packages/core/src/security/resolve-authz-context.ts#ResolvedAuthzContext, packages/core/src/security/authz-store-unavailable.ts#AuthzStoreUnavailableError) The breaking surface is runtime TypeScript in `@objectstack/core`'s security module and nothing else: `resolveAuthzContext` stops always-resolving and raises `AuthzStoreUnavailableError` when a permission-store read is issued and throws. NO metadata surface is touched in either direction. No Zod schema changes, no `packages/spec` declaration is added or removed, no authorable key moves, no stored row shape changes, and no object definition is edited — a customer's metadata app is byte-for-byte unaffected, so `objectstack migrate meta` has nothing to visit and there is no tombstone to mint. The wire vocabulary is likewise untouched: `SERVICE_UNAVAILABLE` is an EXISTING `StandardErrorCode` member that `HttpStatusErrorCodeMap` already maps to 503, so this change only selects a different DECLARED code for an outage rather than adding one. Both named symbols resolve at HEAD as exported declarations whose files are not `*.zod.ts`, are not under `packages/spec/src/contracts/`, are not object definitions and are not `z.input` projections; neither is referenced in code by any metadata surface (the `packages/spec` hits for `resolveAuthzContext` are comment prose describing the envelope, which this gate masks). The channel that reaches an affected consumer is therefore code review and this changeset, never the upgrade guide: a ledger entry could not express "your fail-closed catch should re-raise this error", because there is no metadata for a migration to rewrite. -->
|
|
274
|
+
- d8024f0: feat(core): `Plugin.type` is the closed set the spec declares — a `PluginType` derived from `CORE_PLUGIN_TYPES` (#13925)
|
|
275
|
+
|
|
276
|
+
**BREAKING** accept-set narrowing on a published type, shipped as `minor`
|
|
277
|
+
under the repo's launch-window convention for breaking changes. `Plugin.type`
|
|
278
|
+
(and, through it, `PluginMetadata.type`) was declared `string`, so nothing
|
|
279
|
+
type-checked a plugin author against the eight values the platform accepts —
|
|
280
|
+
the TSDoc beside it carried the whole enumeration as prose, and prose drifted.
|
|
281
|
+
Maintainer ruling 2026-09-01: the Zod enum in `@objectstack/spec`
|
|
282
|
+
(`PluginSchema.type`, declared `z.enum(['standard', ...CORE_PLUGIN_TYPES])`)
|
|
283
|
+
is the authority and the contract was always a closed set; the `string` in
|
|
284
|
+
core was the mismatch, and narrowing it is core aligning to the declared
|
|
285
|
+
contract rather than a new restriction. Paid in one stroke — no warning window.
|
|
286
|
+
|
|
287
|
+
What changes:
|
|
288
|
+
|
|
289
|
+
- `@objectstack/core` now exports `PluginType`, derived from the spec's own
|
|
290
|
+
constant: `'standard' | (typeof CORE_PLUGIN_TYPES)[number]` — today
|
|
291
|
+
`standard`, `ui`, `driver`, `server`, `app`, `theme`, `agent`, `objectql`.
|
|
292
|
+
It is not re-spelled in core, so the compiler's accept set and the Zod gate's
|
|
293
|
+
cannot drift apart; a runtime parity test pins the two against each other.
|
|
294
|
+
- `Plugin.type` is typed `PluginType`. A literal outside the set, or a value
|
|
295
|
+
typed `string`, no longer compiles. Runtime behaviour is unchanged: the Zod
|
|
296
|
+
gate refused such a value before and still does (`invalid_value` at `type`).
|
|
297
|
+
|
|
298
|
+
**Migration.** A plugin that declares one of the eight members needs no change.
|
|
299
|
+
A plugin that assigned a computed or `string`-typed value narrows it at the
|
|
300
|
+
producer — declare the literal, or type the variable `PluginType` — rather than
|
|
301
|
+
casting at the assignment; a value that was never one of the eight was never a
|
|
302
|
+
valid plugin type and was already refused at parse time.
|
|
303
|
+
|
|
304
|
+
<!-- adr-0087: not-required (no-migration-prescription) A TypeScript narrowing on a published runtime interface, aligning `packages/core` to the accept set `packages/spec` already declared. No metadata key, spec symbol, Zod schema, object definition or stored representation is added, removed or renamed — `CORE_PLUGIN_TYPES` and `PluginSchema.type` are read, not changed — so `objectstack migrate meta` has nothing to rewrite and there is no tombstone to mint. The channel that reaches an affected author is the compiler, at the assignment, which is more precise than a ledger line; which member a formerly `string`-typed value should become is authoring intent no migration entry can decide. The in-repo census under the workspace typecheck is recorded on the PR. -->
|
|
305
|
+
- 4635f3e: fix(spec,core): `HotReloadConfig.stateStrategy` refuses the two values it never implemented; `distributedConfig` retired (#12340, ADR-0049)
|
|
306
|
+
|
|
307
|
+
<!-- adr-0087: registered hot-reload-inert-state-strategies-retired -->
|
|
308
|
+
|
|
309
|
+
**BREAKING** accept-set narrowing + export removal, landing after the v17.0.0
|
|
310
|
+
cut (the lockstep launch-window convention ships it as `minor`; the
|
|
311
|
+
prescription is registered under protocol major 18 —
|
|
312
|
+
`RETIRED_DEFS_BY_MAJOR[18]` + the D3 semantic entry
|
|
313
|
+
`hot-reload-inert-state-strategies-retired` — where `os migrate meta` users
|
|
314
|
+
will look).
|
|
315
|
+
|
|
316
|
+
This is ADR-0049 applied one level INSIDE the library the 2026-08-25 #11825
|
|
317
|
+
ruling deliberately kept. That ruling retired the authorable lifecycle-config
|
|
318
|
+
container and kept `HotReloadConfigSchema` as a host-driven library parameter
|
|
319
|
+
type; this change measures the kept vocabulary's own remainder and finds the
|
|
320
|
+
same defect in it. The keep itself stands — `HotReloadConfigSchema`,
|
|
321
|
+
`PluginStateSnapshotSchema` and the health vocabularies still export, and
|
|
322
|
+
`HotReloadManager` / `PluginHealthMonitor` are untouched.
|
|
323
|
+
|
|
324
|
+
The `'disk'` and `'distributed'` arms of `PluginStateManager.saveState` both
|
|
325
|
+
wrote to the SAME in-memory `Map` as `'memory'` — the in-source comments said
|
|
326
|
+
"memory fallback" — and announced the substitution at DEBUG level only. A host
|
|
327
|
+
that asked for durable or cluster-replicated state got process-local memory
|
|
328
|
+
and no error: state that does not survive the restart it was configured to
|
|
329
|
+
survive. `distributedConfig` had ZERO readers anywhere, so an author could
|
|
330
|
+
name a Redis endpoint, a TTL and a replication factor and nothing ever opened
|
|
331
|
+
a connection.
|
|
332
|
+
|
|
333
|
+
FROM → TO:
|
|
334
|
+
|
|
335
|
+
- `stateStrategy: 'disk'` → `stateStrategy: 'memory'` — byte-identical runtime
|
|
336
|
+
behaviour, because `'disk'` already stored to memory. It is the spelling
|
|
337
|
+
that was false, not the behaviour.
|
|
338
|
+
- `stateStrategy: 'distributed'` → `stateStrategy: 'memory'` — same, or
|
|
339
|
+
`'none'` to disable state preservation outright.
|
|
340
|
+
- `distributedConfig: { … }` → *(removed)* — delete the key. It left with the
|
|
341
|
+
`'distributed'` value its own doc comment called it "required" for.
|
|
342
|
+
- `DistributedStateConfigSchema` / `DistributedStateConfig` /
|
|
343
|
+
`DistributedStateConfigParsed` → *(removed)* — the orphan value schema of
|
|
344
|
+
that one key.
|
|
345
|
+
|
|
346
|
+
One-line fix: replace `'disk'` or `'distributed'` with `'memory'` and delete
|
|
347
|
+
any `distributedConfig` — you were already getting in-memory state. There is
|
|
348
|
+
no in-tree replacement for durable or distributed plugin state; persist it in
|
|
349
|
+
the host, which owns the process lifetime these strategies pretended to
|
|
350
|
+
outlive. Real disk or distributed persistence returns only via the ENFORCE
|
|
351
|
+
route of ADR-0049 — the implementation first, the declaration with it.
|
|
352
|
+
|
|
353
|
+
The retirement kit:
|
|
354
|
+
|
|
355
|
+
- **enum-value narrowing** (`['memory','disk','distributed','none']` →
|
|
356
|
+
`['memory','none']`): invisible to all four ratchets by construction (the
|
|
357
|
+
def still emits), so the prescription hangs on the enum's own `error` map
|
|
358
|
+
dispatched by `issue.input` — the `crypto.hash` / `managedBy: 'system'`
|
|
359
|
+
precedent. A value that was never legal still gets zod's own enum message,
|
|
360
|
+
so a typo is not told it "was removed".
|
|
361
|
+
- **whole-def deletion** (route 3 — `HotReloadConfig` is not an authorable
|
|
362
|
+
surface: no metadata-type binding, stack collection or manifest embed ever
|
|
363
|
+
carried it, and nothing in the tree parses `HotReloadConfigSchema` outside
|
|
364
|
+
its own unit test, so there is no authored document to rewrite and nobody
|
|
365
|
+
who could receive a parse-time tombstone): `kernel/DistributedStateConfig`
|
|
366
|
+
in `RETIRED_DEFS_BY_MAJOR[18]` plus the D3 semantic entry. Ratchets moved as
|
|
367
|
+
a def removal must — `api-surface` −3, `authorable-surface` −8,
|
|
368
|
+
`json-schema.manifest` −1.
|
|
369
|
+
- **runtime doors** in `@objectstack/core`, because route 3 leaves no
|
|
370
|
+
parse-time prescription: `HotReloadManager.registerPlugin` now refuses an
|
|
371
|
+
unhonoured `stateStrategy` and a leftover `distributedConfig` with an
|
|
372
|
+
ADR-0112 envelope (`code: VALIDATION_ERROR`, `status: 400`) carrying the
|
|
373
|
+
prescription. Refused BEFORE the `enabled` check, so a disabled config
|
|
374
|
+
cannot smuggle the false declaration through. TypeScript hosts never reach
|
|
375
|
+
it — `HotReloadConfigParsed['stateStrategy']` is now `'memory' | 'none'`, a
|
|
376
|
+
compile error at the call site.
|
|
377
|
+
- **pin move, declared**: `DistributedStateConfigSchema` was NAMED in the
|
|
378
|
+
#11825 survivor list, so this reverses one line of that ruling on new
|
|
379
|
+
evidence — #11825 measured the container's six groups, never this key's own
|
|
380
|
+
readers. The pin in `kernel/plugin-lifecycle-advanced-retirement.test.ts`
|
|
381
|
+
moves in the same commit with the reasoning recorded beside it, and asserts
|
|
382
|
+
the surrounding keep is intact.
|
|
383
|
+
- zero in-tree consumers passed `'disk'` or `'distributed'` (measured at
|
|
384
|
+
cdbd9204b6 with a firing positive control; every live caller passes
|
|
385
|
+
`'memory'` or `'none'`), so no in-repo source changes ride along.
|
|
386
|
+
- ee3595c: fix(spec,core): `HotReloadManager.startWatching` refuses instead of reporting success; `HotReloadConfig.watchPatterns` retired (#12428, ADR-0049)
|
|
387
|
+
|
|
388
|
+
<!-- adr-0087: registered hot-reload-watch-placeholder-retired -->
|
|
389
|
+
|
|
390
|
+
**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep
|
|
391
|
+
launch-window convention ships it as `minor`; the prescription is registered
|
|
392
|
+
under protocol major 18 — `RETIRED_KEYS_BY_MAJOR[18]` + the D3 semantic entry
|
|
393
|
+
`hot-reload-watch-placeholder-retired` — where `os migrate meta` users will
|
|
394
|
+
look). Graded `minor` rather than `major` for the same reason #12340 was one
|
|
395
|
+
day earlier, in this same module.
|
|
396
|
+
|
|
397
|
+
ADR-0049 applied one symbol over from #12340, in the same file and on the same
|
|
398
|
+
per-key test. The #11825 keep still stands: `HotReloadConfigSchema` and
|
|
399
|
+
`PluginStateSnapshotSchema` still export, and `HotReloadManager` /
|
|
400
|
+
`PluginHealthMonitor` are untouched apart from the two doors below.
|
|
401
|
+
|
|
402
|
+
`HotReloadManager.startWatching` contained **no watcher**. Its whole body was a
|
|
403
|
+
guard plus `logger.info('File watching started', { patterns })`, above an
|
|
404
|
+
in-source note saying real watching "would require chokidar or similar". Where
|
|
405
|
+
#12340's inert fallback at least announced itself at DEBUG, this claimed
|
|
406
|
+
success at **INFO**: an operator who set `enabled: true` with `watchPatterns`
|
|
407
|
+
and read that line had been told the opposite of the truth. `watchHandles` was
|
|
408
|
+
only ever read, deleted, iterated and cleared and **never set**, so
|
|
409
|
+
`stopWatching`'s cleanup branch and the teardown loop over its keys were
|
|
410
|
+
structurally unreachable rather than merely untaken. `watchPatterns` therefore
|
|
411
|
+
had no reader that acted on it — its only two uses were log lines.
|
|
412
|
+
|
|
413
|
+
FROM → TO:
|
|
414
|
+
|
|
415
|
+
- `watchPatterns: ['src/**/*.ts']` → *(removed)* — delete the key. Declare your
|
|
416
|
+
globs wherever your own watcher reads them.
|
|
417
|
+
- `manager.startWatching(name)` → `manager.scheduleReload(name, reloadFn)`,
|
|
418
|
+
called from your own watcher's change handler. That is the debounced
|
|
419
|
+
integration point this class does implement, and it is unchanged.
|
|
420
|
+
|
|
421
|
+
One-line fix: delete `watchPatterns`, and call `scheduleReload` from your own
|
|
422
|
+
file watcher instead of `startWatching` — nothing was ever watched, so nothing
|
|
423
|
+
that used to happen stops happening. File watching is the host's job in this
|
|
424
|
+
host-driven library; `chokidar` is already a dependency of
|
|
425
|
+
`@objectstack/metadata`, `@objectstack/metadata-fs` and `@objectstack/cli` —
|
|
426
|
+
never of `@objectstack/core` — so a host has a working model to copy.
|
|
427
|
+
|
|
428
|
+
The retirement kit:
|
|
429
|
+
|
|
430
|
+
- **key tombstone**, and the build is what chose it: the plain deletion was
|
|
431
|
+
tried first and `gen:schema` gate (a) refused it, because
|
|
432
|
+
`HotReloadConfigSchema` is not `.strict()` and a bare deletion would be a
|
|
433
|
+
silent strip (#3733, ADR-0104) — the very defect being retired, one layer
|
|
434
|
+
down. #12340 could take route 3 because what left there was a whole *def*; a
|
|
435
|
+
key leaving a *surviving* def has no such exit. So `watchPatterns` is
|
|
436
|
+
`retiredKey()`-tombstoned, its surface line carries `[RETIRED]`, and
|
|
437
|
+
`kernel/HotReloadConfig:watchPatterns` is registered by exact key in
|
|
438
|
+
`RETIRED_KEYS_BY_MAJOR[18]`. A key tombstone on a surviving def moves
|
|
439
|
+
`authorable-surface` only — the def still emits, so `api-surface` and
|
|
440
|
+
`json-schema.manifest` do not.
|
|
441
|
+
- **no D2 conversion**, deliberately: the chain walks a normalized stack, and
|
|
442
|
+
`HotReloadConfig` is not an authorable surface — no metadata-type binding,
|
|
443
|
+
stack collection or manifest embed ever carried it — so a conversion would be
|
|
444
|
+
a transform with no seam that ever runs. For the same reason the prescription
|
|
445
|
+
carries no `os migrate meta` sentence, exactly as its `stateStrategy` sibling
|
|
446
|
+
in this module does not.
|
|
447
|
+
- **runtime doors** in `@objectstack/core`, because nothing in the tree parses
|
|
448
|
+
`HotReloadConfigSchema` outside its own unit test, so the tombstone alone
|
|
449
|
+
reaches nobody: `startWatching` now throws an ADR-0112 envelope
|
|
450
|
+
(`code: VALIDATION_ERROR`, `status: 400`) carrying the prescription, and
|
|
451
|
+
`registerPlugin` refuses a leftover `watchPatterns` the same way — before the
|
|
452
|
+
`enabled` check, so a disabled config cannot smuggle the false declaration
|
|
453
|
+
through. `startWatching` is kept as a throwing door rather than deleted so
|
|
454
|
+
that caller meets a prescription instead of a bare `TypeError`.
|
|
455
|
+
- **dead code removed with a firing positive control**: `watchHandles` and both
|
|
456
|
+
of its unreachable readers are gone. The zero was pinned first —
|
|
457
|
+
`reloadTimers.set` resolves a real writer in the same file and the same scan,
|
|
458
|
+
while `watchHandles.set` resolves nothing anywhere. `stopWatching` keeps the
|
|
459
|
+
half that always did something (it cancels a pending debounced reload), and
|
|
460
|
+
`shutdown` is unchanged in effect: the loop it lost iterated `watchHandles`
|
|
461
|
+
and therefore ran zero times.
|
|
462
|
+
- **ENFORCE and EXPERIMENTAL were both unavailable**, which is why this is a
|
|
463
|
+
removal: no runtime composes `HotReloadManager`, so enforcing would build for
|
|
464
|
+
a caller that does not exist; and a scan of every planning doc returned zero
|
|
465
|
+
mentions of hot-reload file watching against 145 control hits in the same
|
|
466
|
+
files, so there is no roadmap for `experimental` to point at.
|
|
467
|
+
- af56546: feat(platform-objects): packaged disable works without the automation service, and the activation ledger has one implementation (#12359, #12350)
|
|
468
|
+
|
|
469
|
+
Two halves of ADR-0126's "ledger convergence", bundled by maintainer ruling
|
|
470
|
+
(2026-08-26, verbatim and untranslated: 「同意」).
|
|
471
|
+
|
|
472
|
+
## The registration follows the declaration (#12359)
|
|
473
|
+
|
|
474
|
+
`sys_metadata_activation` is declared in `@objectstack/platform-objects`, but
|
|
475
|
+
the only thing that REGISTERED it was the automation service's manifest —
|
|
476
|
+
because flows were the ledger's first and, until packaged actions landed, only
|
|
477
|
+
consumer. Packaged actions are a second consumer with a different owner: their
|
|
478
|
+
consult and write path live on the ObjectQL engine, present in every
|
|
479
|
+
composition that can execute an action.
|
|
480
|
+
|
|
481
|
+
So a deployment with actions and no automation service had no ledger table, and
|
|
482
|
+
the activation door answered **503 SERVICE_UNAVAILABLE** on every flip —
|
|
483
|
+
correctly (ADR-0126 §6 wall 3: a flip that cannot be made durable must not be
|
|
484
|
+
reported as one) and permanently. Measured on a real boot; it is now this
|
|
485
|
+
change's positive test, measured on the same boot:
|
|
486
|
+
|
|
487
|
+
```
|
|
488
|
+
POST /api/v1/actions/_activation/showcase_task/showcase_mark_done {"enabled":false}
|
|
489
|
+
before -> 503 SERVICE_UNAVAILABLE after -> 200, and dispatch refuses 409 ACTION_DISABLED
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
`PlatformObjectsPlugin` registers it now, so every composition carrying
|
|
493
|
+
platform-objects has the ledger and each future ADR-0126 §8 consumer (`tool`,
|
|
494
|
+
`skill`, `position`) inherits it. **MOVE, not add** — the automation service no
|
|
495
|
+
longer names the object. That was not a style choice: a second code package
|
|
496
|
+
claiming one object throws `Object "…" is already owned by package "…"`
|
|
497
|
+
(ADR-0029 D3/D7), measured, so adding a registrant would have been a boot
|
|
498
|
+
failure rather than a duplicate.
|
|
499
|
+
|
|
500
|
+
**Upgrade is a no-op for existing data, and that is measured rather than
|
|
501
|
+
asserted.** A manifest is also a ROUTING decision — `resolveDatasourceBinding`
|
|
502
|
+
step 4 routes an object by its owning package's `defaultDatasource` — so the
|
|
503
|
+
registrar carries the table's datasource with it:
|
|
504
|
+
|
|
505
|
+
```
|
|
506
|
+
owner com.objectstack.service-automation (defaultDatasource:'cloud') -> 'cloud'
|
|
507
|
+
owner com.objectstack.platform-objects (none) -> undefined (global default driver)
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
The ledger table already exists in live databases, so on any deployment
|
|
511
|
+
carrying a `cloud` datasource that difference would leave the rows in one
|
|
512
|
+
database and read another — every disabled artifact silently re-arming. The
|
|
513
|
+
ledger therefore rides its own manifest from the same plugin, carrying the
|
|
514
|
+
automation manifest's `scope` / `namespace` / `defaultDatasource` triple
|
|
515
|
+
verbatim. The three siblings (`sys_migration`, `sys_migration_journal`,
|
|
516
|
+
`sys_secret`) deliberately do not get it and keep riding the project database.
|
|
517
|
+
|
|
518
|
+
## One implementation of the §4 row contract (#12350)
|
|
519
|
+
|
|
520
|
+
ADR-0126 §4 declares one activation ledger; it had two independent
|
|
521
|
+
implementations of that one row contract — `ObjectStoreFlowActivationStore`
|
|
522
|
+
(service-automation) and `ObjectStoreActionActivationStore` (objectql). They
|
|
523
|
+
agreed because the second was written from the first, and nothing structurally
|
|
524
|
+
held them together; §8 pre-charts `tool`, `skill` and `position`, and a third
|
|
525
|
+
and fourth copy is where the org-row skip and the `0`-is-false read get lost
|
|
526
|
+
quietly, in the direction (an artifact re-arming) nothing else measures.
|
|
527
|
+
|
|
528
|
+
Neither consumer could import the other, so the contract now lives once in
|
|
529
|
+
`@objectstack/core` — the package both already depend on — as
|
|
530
|
+
`ObjectStoreMetadataActivationStore(engine, metadataType)`, exported alongside
|
|
531
|
+
`InMemoryMetadataActivationStore`, `MetadataActivationRow`,
|
|
532
|
+
`MetadataActivationStore`, `MetadataActivationStoreEngine` and
|
|
533
|
+
`METADATA_ACTIVATION_TABLE`. Each consumer keeps its own name, its own
|
|
534
|
+
one-argument constructor and its own docs, and fixes the discriminator.
|
|
535
|
+
|
|
536
|
+
**No behaviour change and no API break.** `ObjectStoreFlowActivationStore` /
|
|
537
|
+
`InMemoryFlowActivationStore` / `FlowActivationStoreEngine` and
|
|
538
|
+
`ObjectStoreActionActivationStore` / `InMemoryActionActivationStore` /
|
|
539
|
+
`ActionActivationRow` / `ActionActivationStore` / `ActionActivationStoreEngine`
|
|
540
|
+
/ `ACTION_ACTIVATION_TABLE` are exported from the same modules with the same
|
|
541
|
+
shapes. Row semantics are byte-equivalent: deployment-level rows scoped only by
|
|
542
|
+
the `metadata_type` discriminator, a driver `0` read as false, read-then-write
|
|
543
|
+
rather than a blind upsert, and no `delete` in the engine slice because
|
|
544
|
+
re-enabling rewrites the row.
|
|
545
|
+
|
|
546
|
+
Both existing pin suites stay green **unchanged**, which is what makes them the
|
|
547
|
+
proof the consolidation lost nothing — verified by ablation: mutating the one
|
|
548
|
+
shared implementation turns both of them red on their own assertions, so both
|
|
549
|
+
really reach it.
|
|
550
|
+
- a8c00e2: feat(core): cache successful `sys_setting` localization reads, invalidated synchronously on write (#11966)
|
|
551
|
+
|
|
552
|
+
Leg C (ship-first) of the accepted #11633 cross-request caching design
|
|
553
|
+
(maintainer acceptance 2026-08-25, forks 1A / 2B / 3A / TTL-0).
|
|
554
|
+
`resolveLocalizationContext` re-read `sys_setting` on **every** authenticated
|
|
555
|
+
request to answer the same three keys — `timezone` / `locale` / `currency` —
|
|
556
|
+
for a workspace whose values change roughly never. That read is now cached.
|
|
557
|
+
|
|
558
|
+
**Grade: `minor`, not `patch`.** It adds a deployment variable
|
|
559
|
+
(`OS_LOCALIZATION_CACHE_TTL_MS`) and changes the query pattern of a shipped code
|
|
560
|
+
path. Not `major`: the observable contract callers actually depend on — a
|
|
561
|
+
settings write is visible to the very next read — is preserved, and pinned.
|
|
562
|
+
|
|
563
|
+
Caching this read was tried once before and reverted. #10221's first version
|
|
564
|
+
memoized every outcome for 30s and CI went red on
|
|
565
|
+
`analytics-timezone.dogfood.test.ts`, which writes a new org timezone and
|
|
566
|
+
expects the very next analytics query to bucket under it; the cache was narrowed
|
|
567
|
+
to memoize **failures** only. That verdict was on **TTL-only** caching and it
|
|
568
|
+
still stands unamended. What changed is that the process now has invalidation
|
|
569
|
+
seams it did not have then:
|
|
570
|
+
|
|
571
|
+
- **Primary — the settings change seam.** `SettingsService.subscribe(ns, handler)`
|
|
572
|
+
dispatches synchronously and in-process from the write path, after the row is
|
|
573
|
+
persisted. (⚠️ #11633 calls this a "settings change bus"; no such module
|
|
574
|
+
exists — `subscribe()` is the seam. No change was needed in
|
|
575
|
+
`@objectstack/service-settings`: the seam was already public and already does
|
|
576
|
+
exactly this.)
|
|
577
|
+
- **Backstop — the engine write epoch** from #11968's substrate. Needed because
|
|
578
|
+
this resolver's own fallback reads `sys_setting` *directly*, so a seeder or
|
|
579
|
+
any other direct engine write emits no settings event at all. It is read
|
|
580
|
+
structurally rather than imported, because `@objectstack/objectql` depends on
|
|
581
|
+
`@objectstack/core` and the substrate declared `WriteEpochLike` separately for
|
|
582
|
+
exactly this consumer. A peer node's hint arrives as a local bump, so an
|
|
583
|
+
attached `authz.invalidated` bridge narrows cross-node convergence for free.
|
|
584
|
+
- **TTL** — the residual bound, for what neither seam can see. Default 30s,
|
|
585
|
+
`0` disables the cache on a real path rather than a degenerate one.
|
|
586
|
+
|
|
587
|
+
Two rules carry the change and are pinned rather than merely documented:
|
|
588
|
+
|
|
589
|
+
1. **A success is cached only when the engine exposes the write epoch.** A `ql`
|
|
590
|
+
with no seam is a `ql` whose writes the cache cannot observe, so rather than
|
|
591
|
+
degrade to the TTL-only shape that was already reverted here once, the cache
|
|
592
|
+
declines. A partial `{ current }` shape is not a seam either — a counter
|
|
593
|
+
nothing can bump would read as a live invalidation source and pin the answer
|
|
594
|
+
for a whole TTL.
|
|
595
|
+
2. **Invalidation retires success entries only.** #10221's failure memo exists
|
|
596
|
+
for an environment where `sys_setting` is missing; retiring it on a write
|
|
597
|
+
would restart precisely the per-request driver log spam that memo removed,
|
|
598
|
+
and no write can create a missing table. It stays TTL-bound and behaves
|
|
599
|
+
exactly as #10221/#11877 shipped it.
|
|
600
|
+
|
|
601
|
+
`analytics-timezone.dogfood.test.ts` is unchanged and unweakened — it is this
|
|
602
|
+
leg's acceptance test, and an ablation that reduces the cache to its TTL turns
|
|
603
|
+
it red on the same assertion the original revert was recorded against.
|
|
604
|
+
- 7131f12: **Security:** the "is this user id a platform admin?" question is now asked in exactly one place, and the two copies that answered it differently are gone (#10348, #10949).
|
|
605
|
+
|
|
606
|
+
ADR-0068 D2 defines platform standing as one thing — an unscoped `admin_full_access` grant, held now. `core/security/resolve-authz-context.ts` is the declared authority for authorization derivation and its header states that every entry point must resolve through it and never re-read the grant tables itself. `plugin-auth`'s `auth-manager.ts` did exactly that twice: once inside the `customSession` callback, and once in the predicate that authorizes `/sso/register` and, through the impersonation oracle, `/admin/impersonate-user`. Both copies are deleted. Both callers — and the session payload — now ask `hasPlatformAdminStanding(engine, userId)`, a projection of `resolveUserAuthzGrants` exported from `@objectstack/core`, so a platform-admin verdict is derived in one place for the whole platform.
|
|
607
|
+
|
|
608
|
+
**What that changes, and it is a tightening on all three counts.** The deleted copies applied neither the ADR-0091 validity window nor the ADR-0049 `active` check, and resolved `admin_full_access` by matching a name over a page of the permission-set catalogue. The authority applies both checks before any derivation and resolves the set by id. So:
|
|
609
|
+
|
|
610
|
+
- an **expired** platform-admin grant no longer authorizes `/sso/register` or `/admin/impersonate-user`, and no longer appears in the session payload;
|
|
611
|
+
- a **deactivated** `admin_full_access` permission set no longer confers platform standing anywhere — the deactivation dialog's promise now holds on these gates too;
|
|
612
|
+
- an environment holding **more permission sets than a single catalogue page** can no longer lose the `admin_full_access` row and demote every platform admin at once.
|
|
613
|
+
|
|
614
|
+
**One behaviour widens, and it was ruled deliberately** (maintainer, 2026-08-24). The `customSession` copy read without a system identity while the other read with one. The single authority reads as system, so on a strictly org-scoped deployment the session payload stops under-reporting platform admin — the fail-closed drift between the payload and the gates ends. Open-core composition is unaffected: the two reads reached identical rows there already.
|
|
615
|
+
|
|
616
|
+
**The org boundary is unchanged and now pinned at both gates.** An org owner, an org admin, a `TENANT_ADMIN`-posture principal and an org-scoped `admin_full_access` grant are all refused — the `PLATFORM_ADMIN` rung derives from the unscoped capability grant alone. The predicate takes an engine and a user id and nothing else: it deliberately does not accept the resolver's caller-supplied seeds, so no part of a request can supply part of its own verdict.
|
|
617
|
+
|
|
618
|
+
Population queries are a different kind and are untouched: `ensure-default-organization.ts` asks *which* user is the platform admin, which a per-user predicate cannot express.
|
|
619
|
+
- 33184fd: `PLATFORM_ADMIN` can now be anchored on deployment CONFIGURATION instead of a stored grant row: an account whose `sys_user.email` is on `OS_PLATFORM_OWNER_EMAIL` **and** whose `email_verified` reads verified resolves `PLATFORM_ADMIN` with the declared `admin_full_access` capability set, derived live on each authorization resolution (#11663 leg L2, design accepted 2026-08-25 as bundle 1A/2B/3A/4A/5A/6A/7A).
|
|
620
|
+
|
|
621
|
+
**Additive — nothing is revoked.** The legacy unscoped `admin_full_access` grant still confers exactly as it did; a holder whose standing rests on the row alone now gets a once-per-process pointer at the configuration line that re-anchors them. A deployment that has declared no administrators resolves byte-identically to before: the config list is empty, the derivation answers "not an admin" before it reads any row, and the pinned batch-equivalence query multiset is unchanged.
|
|
622
|
+
|
|
623
|
+
**The variable takes a list.** `OS_PLATFORM_OWNER_EMAIL` accepts one address or a comma-separated list of them — one normalization (`trim().toLowerCase()`), duplicates collapsed, blank entries dropped. ⛔ Any entry that is not an address **fails the whole variable closed** with a loud refusal naming it, rather than being skipped: silently dropping a typo would leave a narrower administrator set than the operator declared, with nothing anywhere to notice. Unset, blank or refused all mean **zero** config-derived administrators.
|
|
624
|
+
|
|
625
|
+
**Verified-email match only.** An unverified account holding a configured address confers nothing, and an ABSENT `email_verified` column reads unverified. The match reads the caller's own **stored** `sys_user` row, never the caller-supplied session email.
|
|
626
|
+
|
|
627
|
+
New exports from `@objectstack/core`: `resolvePlatformAdminEmails`, `parsePlatformAdminEmails`, `matchesConfiguredPlatformAdmin`, `normalizePlatformAdminEmail`, `PLATFORM_ADMIN_EMAIL_SEPARATOR`, `ADMIN_STANDING_NON_TABLE_INPUTS` and the test hooks beside them. `@objectstack/core` now depends on `@objectstack/types` (measured acyclic: `types` depends only on `spec`).
|
|
628
|
+
|
|
629
|
+
`@objectstack/plugin-auth`'s break-glass guard follows the derivation, as it must: `ADMIN_STANDING_SURFACE.sys_user` is reclassified `derives`, the last-administrator enumeration counts config-derived administrators through the resolver's own predicate, and a fifth write shape is judged — a change of address or an `email_verified` reset that would leave the environment with no administrator is refused, naming the configuration as the remedy. An ordinary profile write still costs the guard no reads.
|
|
630
|
+
- b72db01: fix(spec,core): `PluginHealthMonitor` stops claiming a restart it never performed; the three `PluginHealthCheck` restart keys retired (#12032, ADR-0049)
|
|
631
|
+
|
|
632
|
+
<!-- adr-0087: registered plugin-auto-restart-never-reinitialised -->
|
|
633
|
+
|
|
634
|
+
**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep
|
|
635
|
+
launch-window convention ships it as `minor`; the prescriptions are registered
|
|
636
|
+
under protocol major 18 — three `RETIRED_KEYS_BY_MAJOR[18]` entries plus the D3
|
|
637
|
+
semantic entry `plugin-auto-restart-never-reinitialised` — where
|
|
638
|
+
`os migrate meta` users will look). Graded `minor` rather than `major` for the
|
|
639
|
+
same reason #12340 and #12428 were, the day before, in this same module.
|
|
640
|
+
|
|
641
|
+
## What was measured
|
|
642
|
+
|
|
643
|
+
`PluginHealthMonitor.attemptRestart` called `plugin.destroy()` and stopped
|
|
644
|
+
there. The comment above the call read *"Call destroy and init to restart"*,
|
|
645
|
+
and `init` appeared in `health-monitor.ts` **only inside that comment**. So a
|
|
646
|
+
plugin whose health checks crossed `failureThreshold` with `autoRestart: true`
|
|
647
|
+
got: `destroy()`, a log line reading `Plugin restarted`, status `recovering`,
|
|
648
|
+
and periodic health checks that carried on running against the destroyed
|
|
649
|
+
instance. The default check when no `checkMethod` resolves is
|
|
650
|
+
`{ name: 'plugin-loaded', status: 'passed' }`, which a destroyed object passes
|
|
651
|
+
indefinitely — so the **terminal** report on a torn-down, never-re-initialised
|
|
652
|
+
plugin was `healthy`.
|
|
653
|
+
|
|
654
|
+
Reproduced at `ee3595cefd` before anything was changed, with
|
|
655
|
+
`successThreshold: 3`:
|
|
656
|
+
|
|
657
|
+
```
|
|
658
|
+
round 1 (failing): status=failed destroyed=0 alive=true
|
|
659
|
+
after backoff: status=recovering destroyed=1 alive=false
|
|
660
|
+
recovery round 1: status=recovering destroyed=1 alive=false
|
|
661
|
+
recovery round 2: status=recovering destroyed=1 alive=false
|
|
662
|
+
recovery round 3: status=healthy destroyed=1 alive=false
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
#11955 made that report *more* convincing rather than less: reaching `healthy`
|
|
666
|
+
now costs `successThreshold` consecutive passing rounds, so a destroyed plugin
|
|
667
|
+
has to earn a declared number of passes before it is misreported.
|
|
668
|
+
`restartAttempts` was incremented as though a restart had occurred, and
|
|
669
|
+
`maxRestartAttempts` / `restartBackoff` scheduled further "restarts" of a plugin
|
|
670
|
+
that was never brought back up.
|
|
671
|
+
|
|
672
|
+
## Why REMOVE and not the other two ADR-0049 states
|
|
673
|
+
|
|
674
|
+
**ENFORCE** would have to build the restart, and the class cannot host one.
|
|
675
|
+
`Plugin.init(ctx)` needs a `PluginContext`; the only two `plugin.init(...)` call
|
|
676
|
+
sites in the tree are the kernel's own boot loops (`kernel-base.ts:202`,
|
|
677
|
+
`kernel.ts:607`), both over the full plugin list, with a context that is
|
|
678
|
+
`private` on `ObjectKernel` and `protected` on `KernelBase`. No host can obtain
|
|
679
|
+
one, so a host-provided re-init hook would have had nothing to call. (Positive
|
|
680
|
+
control for that scan: the same pass resolves five real non-test
|
|
681
|
+
`plugin.destroy()` call sites, so it does see lifecycle drivers.) Building a
|
|
682
|
+
per-plugin re-init API for a caller that does not exist — no runtime constructs
|
|
683
|
+
`PluginHealthMonitor` (#11825) — is the speculation ADR-0049's staged decision
|
|
684
|
+
names as the wrong default at this milestone, where the shippable liability is
|
|
685
|
+
the false promise and not the missing feature.
|
|
686
|
+
|
|
687
|
+
**EXPERIMENTAL** requires a roadmap. A scan of the whole `docs/` planning + ADR
|
|
688
|
+
corpus returned **zero** mentions of plugin auto-restart, against 118 control
|
|
689
|
+
hits for "health" and 13 for "hot reload" in the same corpus.
|
|
690
|
+
|
|
691
|
+
`maxRestartAttempts` and `restartBackoff` leave with `autoRestart` rather than
|
|
692
|
+
as a tidy-up: with no restart, *"Maximum restart attempts before giving up"* and
|
|
693
|
+
*"Backoff strategy for restart delays"* have nothing left to be the vocabulary
|
|
694
|
+
**of** — the test that took `distributedConfig` out with the `stateStrategy`
|
|
695
|
+
value it was documented as requiring (#12340).
|
|
696
|
+
|
|
697
|
+
## What changes for a host
|
|
698
|
+
|
|
699
|
+
All three keys are **tombstoned**, not deleted: `PluginHealthCheckSchema` is not
|
|
700
|
+
`.strict()`, so a bare deletion would be a silent strip (#3733, ADR-0104) — a
|
|
701
|
+
milder form of the defect being retired. A TypeScript host gets a `tsc` error
|
|
702
|
+
(the keys are typed `never`); a parse raises the prescription; and
|
|
703
|
+
`PluginHealthMonitor.registerPlugin` refuses a hand-built config carrying any of
|
|
704
|
+
them with an ADR-0112 envelope (`code: VALIDATION_ERROR`, `status: 400`), thrown
|
|
705
|
+
before any state is stored so a refused config leaves no half-registered plugin
|
|
706
|
+
behind.
|
|
707
|
+
|
|
708
|
+
`PluginHealthMonitor` no longer calls `plugin.destroy()` at all. A plugin that
|
|
709
|
+
crosses `failureThreshold` is reported `degraded` / `unhealthy` / `failed` and
|
|
710
|
+
left running; acting on that is the host's job in this host-driven library
|
|
711
|
+
(#11825 route 2). Poll `getHealthStatus(pluginName)` / `getHealthReport(pluginName)`
|
|
712
|
+
and restart at the level that owns the plugin's lifetime.
|
|
713
|
+
|
|
714
|
+
Everything else in the monitor is unchanged: registration, periodic checks, the
|
|
715
|
+
`timeout` race and its refd-timer guard (#4875), both failure routes sharing the
|
|
716
|
+
counters (#11852), and `successThreshold` binding from every status that records
|
|
717
|
+
a failure (#11955). `recovering` is now written only by the success branch —
|
|
718
|
+
the one writer that ever meant it.
|
|
719
|
+
- 49f0dcf: feat(core): retire the inert `PluginMetadata` surfaces — `configSchema` with `PluginConfigValidator`, and `hotReloadable` (#11982, #12587, ADR-0049)
|
|
720
|
+
|
|
721
|
+
<!-- adr-0087: not-required (runtime-interface-only packages/core/src/plugin-loader.ts#PluginMetadata) PluginMetadata is a runtime TS interface in packages/core with no Zod schema, no spec declaration and no stored representation; no metadata surface references it (the PluginMetadata in packages/spec/src/kernel/plugin-validator.zod.ts is an unrelated locally-declared homonym). The deleted PluginConfigValidator / createPluginConfigValidator were runtime classes in the same non-metadata module family, so `objectstack migrate meta` has nothing to rewrite; the compiler is the notification channel — TS2353 on the removed fields, TS2305 on the removed exports. -->
|
|
722
|
+
|
|
723
|
+
**BREAKING**: removes a published-but-inert capability from the `.` entry of
|
|
724
|
+
`@objectstack/core`. Shipped as `minor` under the lockstep launch-window
|
|
725
|
+
convention (a `major` bump is refused repo-wide by `check:changeset-no-major`).
|
|
726
|
+
|
|
727
|
+
Removed, each measured at zero live consumers with positive controls (the
|
|
728
|
+
sibling `startupTimeout` is read live by the kernel's startup timeout guard);
|
|
729
|
+
maintainer ruled retire under ADR-0049 enforce-or-remove, 2026-08-27,
|
|
730
|
+
decision-inbox batch 5; recorded in ADR-0025 §3.7:
|
|
731
|
+
|
|
732
|
+
- `PluginMetadata.configSchema` — declared "Configuration schema for
|
|
733
|
+
validation", but the mechanism could never run: the loader's only call
|
|
734
|
+
passed no config, and no caller could — plugin factories close over their
|
|
735
|
+
config, so the kernel never receives it. Every one of ~40 production
|
|
736
|
+
`kernel.use()` compositions already passes config as constructor arguments
|
|
737
|
+
and works.
|
|
738
|
+
- `PluginConfigValidator` / `createPluginConfigValidator` — the validator
|
|
739
|
+
behind that field: real code with zero reachable invocations, deleted along
|
|
740
|
+
with its unit test and its export from the security barrel.
|
|
741
|
+
- `PluginMetadata.hotReloadable` — declared "Whether plugin supports hot
|
|
742
|
+
reload" with zero reads and zero declarations: `HotReloadManager.reloadPlugin`
|
|
743
|
+
gates only on its own registered reload configs, so `hotReloadable: false`
|
|
744
|
+
was hot-reloaded identically to `true`.
|
|
745
|
+
- The `packages/core/ADVANCED_FEATURES.md` example whose inline comment
|
|
746
|
+
promised "Config is validated before init is called" — false on the
|
|
747
|
+
retired ref, and the retired surface's only in-repo declaration site.
|
|
748
|
+
|
|
749
|
+
One-line fixes, per symbol. If you declared `configSchema` on a plugin:
|
|
750
|
+
delete the field and parse your config at the plugin's own seam —
|
|
751
|
+
`MyConfigSchema.parse(options)` in the plugin factory or constructor, the
|
|
752
|
+
pattern `packages/rest` uses. If you imported `PluginConfigValidator` or
|
|
753
|
+
`createPluginConfigValidator`: delete the import and hold your own
|
|
754
|
+
`schema.parse` call; the compiler (TS2305) locates every such site. If you
|
|
755
|
+
declared `hotReloadable`: delete the field — it never gated anything, and
|
|
756
|
+
hot-reload participation remains governed solely by
|
|
757
|
+
`HotReloadManager.registerReloadConfig`.
|
|
758
|
+
|
|
759
|
+
Re-declaring a kernel-owned config-validation surface is a fresh decision for
|
|
760
|
+
the day ADR-0025's plugin distribution layer lands, with #11982's zero-caller
|
|
761
|
+
measurement as its starting evidence.
|
|
762
|
+
- add4360: fix(core): tell "service never registered" apart from "service failed to construct" on the async path (#13905)
|
|
763
|
+
|
|
764
|
+
`PluginLoader.getService` — reached through `Kernel.getServiceAsync` — answered two
|
|
765
|
+
different facts with the same bare `Error`. "Nothing ever registered this service" and
|
|
766
|
+
"the service is registered and could not be built" arrived at a caller as one
|
|
767
|
+
indistinguishable rejection, separated only by message text.
|
|
768
|
+
|
|
769
|
+
That was load-bearing one layer out. `RestServer.computeExecCtx`'s kernel branch absorbs a
|
|
770
|
+
failed `getServiceAsync('objectql')` and degrades to "no engine is wired", and it must keep
|
|
771
|
+
doing so — a kernel with no data plane is a supported configuration, declared by
|
|
772
|
+
`rest-api-plugin.ts` as `optionalDependencies: ['com.objectstack.engine.objectql']`. So a
|
|
773
|
+
multi-tenant host whose engine *failed to construct* reached the same resolver as "no
|
|
774
|
+
engine is wired", degrading silently where it should have refused loudly. The branch could
|
|
775
|
+
not be repaired from outside, because the fact it needed had been collapsed before it
|
|
776
|
+
arrived.
|
|
777
|
+
|
|
778
|
+
The asynchronous path now carries the distinction the **synchronous** context accessor in
|
|
779
|
+
`kernel.ts` has always drawn from the registry. `@objectstack/core` publishes exactly two
|
|
780
|
+
new symbols for it:
|
|
781
|
+
|
|
782
|
+
- `isServiceNotRegisteredError(err)` — true only when nothing was ever registered under
|
|
783
|
+
that name;
|
|
784
|
+
- `SERVICE_NOT_REGISTERED_CODE` — the code the rejection carries.
|
|
785
|
+
|
|
786
|
+
The test is closed and its default is loud: exactly one rejection in `getService` means
|
|
787
|
+
"never registered" and only that one is branded, so every other way it can fail — a factory
|
|
788
|
+
that threw, a missing scope id, an unset loader context, a circular service dependency —
|
|
789
|
+
stays unbranded, and a consumer that absorbs only the branded rejection is loud about
|
|
790
|
+
everything else, including rejections added later.
|
|
791
|
+
|
|
792
|
+
⛔ Not message matching. Adding a second text classifier on a resolution path is the failure
|
|
793
|
+
mode this change removes: reading "not found" off the async path once reported every
|
|
794
|
+
missing service as `is async - use await` — the wrong fix, pointing at the wrong layer.
|
|
795
|
+
|
|
796
|
+
Nothing existing moves. The rejection keeps a byte-identical message and `name: 'Error'`;
|
|
797
|
+
the only observable change is the two added own-properties.
|
|
798
|
+
|
|
799
|
+
### Patch Changes
|
|
800
|
+
|
|
801
|
+
- efb3513: fix(platform-objects,core): `sys_metadata_activation` ships tenant-less — drop the reserved organization column (#15024)
|
|
802
|
+
|
|
803
|
+
The ADR-0126 activation ledger records that **this environment** switched a
|
|
804
|
+
packaged artifact off. That is deployment-level state, owned by no
|
|
805
|
+
organization — so the table ships with no tenant column at all.
|
|
806
|
+
|
|
807
|
+
It briefly declared one: an `organization_id` marked "RESERVED", nullable, and
|
|
808
|
+
written by nobody, held for a per-organization dimension ADR-0126 §5
|
|
809
|
+
pre-charted. A reserved nullable tenant column is exactly the shape the
|
|
810
|
+
total-organization-ownership record proposed in PR #14976 rules out, and this
|
|
811
|
+
one had no reader either. **This is a plain removal, not a migration:** the
|
|
812
|
+
table landed after the 17.2.0 tag, so no released version ever carried the
|
|
813
|
+
column and no deployment has data in it. Should a per-organization dimension
|
|
814
|
+
ever be wanted, it returns as a separate org-owned object — never as a column
|
|
815
|
+
on this ledger.
|
|
816
|
+
|
|
817
|
+
What changed:
|
|
818
|
+
|
|
819
|
+
- **`sys_metadata_activation` declares `systemFields: { tenant: false }`** and
|
|
820
|
+
no longer declares the column. Both halves are needed: the tenant anchor is
|
|
821
|
+
INJECTED at registration, so deleting the field alone would have left the
|
|
822
|
+
column exactly where it was. ⚠️ Deliberately NOT `tenancy: { enabled: false }`
|
|
823
|
+
— that key is the ADR-0066 D2 platform-global *posture*, which the sibling
|
|
824
|
+
`sys_sso_provider` uses for the opposite shape (a table that KEEPS its tenant
|
|
825
|
+
column and needs the wall over it stood down). Here there is no column to
|
|
826
|
+
wall. Both spellings reach `plugin-security`'s `tenancyDisabled`, which is
|
|
827
|
+
required rather than incidental: a Layer 0 wall composing an equality on a
|
|
828
|
+
column the table does not have denies every row.
|
|
829
|
+
- **The declared unique index states `unique: 'global'`** over
|
|
830
|
+
`(metadata_type, name)` instead of `'organization'`. ⚠️ The materialized DDL
|
|
831
|
+
is unchanged: `normalizeDeclaredIndex` prepends the NULL-safe tenant key part
|
|
832
|
+
only when the table HAS a tenant column, so `'organization'` already degraded
|
|
833
|
+
to exactly these two columns. What changes is that the declaration now states
|
|
834
|
+
the boundary it actually gets, rather than claiming a per-organization one
|
|
835
|
+
that does not exist. Still explicit rather than bare `unique: true`, which
|
|
836
|
+
lint `unique/unscoped-declared-index` warns on and protocol 18 rejects.
|
|
837
|
+
- **`ObjectStoreMetadataActivationStore` drops its NULL filter and its
|
|
838
|
+
org-row skip.** `list()` is now every activation row of its type, scoped by
|
|
839
|
+
the `metadata_type` discriminator alone, and `setActive` takes the single row
|
|
840
|
+
its keyed read returns instead of picking the NULL-organization one out of
|
|
841
|
+
the result. Both guarded a column that no longer exists; the declared unique
|
|
842
|
+
index over the two columns the lookup keys on is what makes that read
|
|
843
|
+
single-valued. `ObjectStoreFlowActivationStore` and
|
|
844
|
+
`ObjectStoreActionActivationStore` inherit the change.
|
|
845
|
+
|
|
846
|
+
Unchanged, and pinned: the operator gate on activation writes under walled
|
|
847
|
+
postures (ADR-0126 D3), the `execute()`-time flow consult and the dispatch-time
|
|
848
|
+
action consult, "absence of a row means ACTIVE", re-enabling UPDATES the row
|
|
849
|
+
rather than deleting it, and a driver `0` reading as false. The pins that
|
|
850
|
+
asserted the reserved column and the org-row skip are rewritten to pin the
|
|
851
|
+
column's ABSENCE rather than deleted — including at the injection authority
|
|
852
|
+
(`resolveInjectedSystemColumns`, which decides whether the column exists) and
|
|
853
|
+
in a real booted stack, where the row's key set is a reading of the physical
|
|
854
|
+
table.
|
|
855
|
+
- e27583e: docs(core): the `AuthzStoreUnavailableError` brand doc states the measured `structuredClone` behaviour instead of claiming survival (#14006)
|
|
856
|
+
|
|
857
|
+
Documentation only — no runtime change, no type change, no accept/reject
|
|
858
|
+
behaviour moves. It ships as a patch because the docblock is a **published
|
|
859
|
+
byte**: `tsup`'s declaration rollup carries it into `dist/index.d.ts` and
|
|
860
|
+
`dist/index.d.cts`, so it is what a consumer reads on hover.
|
|
861
|
+
|
|
862
|
+
The brand's docblock justified the string-keyed own property with two reasons
|
|
863
|
+
joined by an `and`, of which only the second was true:
|
|
864
|
+
|
|
865
|
+
> A string-keyed own property (not a `Symbol.for` registry key) so it survives
|
|
866
|
+
> `structuredClone`, and so a duplicated copy of this module still brands
|
|
867
|
+
> identically.
|
|
868
|
+
|
|
869
|
+
Measured on Node 22.22.2: the structured-clone algorithm gives `Error` a
|
|
870
|
+
dedicated serialization carrying `message`, `stack` and `cause` only, and drops
|
|
871
|
+
every other own property — the brand, the ADR-0112 `code`, `status` and
|
|
872
|
+
`object` alike (a subclass's own `name` returns as `'Error'`). The
|
|
873
|
+
plain-object control is the half that proves it: `{ __brand: true, code: 'C' }`
|
|
874
|
+
keeps **both** keys through the same call, so the loss is specific to `Error`,
|
|
875
|
+
not general to `structuredClone`.
|
|
876
|
+
|
|
877
|
+
The property and the reason that actually earns it are kept — a duplicated copy
|
|
878
|
+
of the module still brands identically, which is exactly what `instanceof`
|
|
879
|
+
cannot do across two installed copies of `@objectstack/core`. The false half is
|
|
880
|
+
replaced by the measured behaviour, carrying the reproducible script and the
|
|
881
|
+
Node version rather than a second unsourced assertion, and phrased to match
|
|
882
|
+
what `service-not-registered.ts` already records for its own brand (one
|
|
883
|
+
phrasing across the two modules, not two).
|
|
884
|
+
|
|
885
|
+
⛔ The clone gap is deliberately NOT "fixed" with a `toJSON` or a custom
|
|
886
|
+
serialization: no call site crosses a clone boundary today
|
|
887
|
+
(`rethrowAuthzStoreUnavailable` on the rest rethrow paths,
|
|
888
|
+
`isAuthzStoreUnavailableError` inside service `catch` blocks — all in-process),
|
|
889
|
+
and adding one would widen the module's surface with nothing pulling on it. The
|
|
890
|
+
docblock instead names the trap the false claim invited: branching on the brand
|
|
891
|
+
across a worker or `postMessage` boundary would answer `false` and fail OPEN on
|
|
892
|
+
a security path.
|
|
893
|
+
- 983edf1: fix(core): `autoRestart` now fires for a health check that throws or times out, not only for one that returns a failure (#11852)
|
|
894
|
+
|
|
895
|
+
`PluginHealthMonitor.performHealthCheck` reaches its failure handling by two
|
|
896
|
+
disjoint routes, and only one of them could ever restart the plugin.
|
|
897
|
+
|
|
898
|
+
A check that **returned** a failure (`false` or `{ status: 'unhealthy' }`)
|
|
899
|
+
incremented `failureCounters`, cleared `successCounters`, and — once
|
|
900
|
+
`failureThreshold` consecutive failures accumulated — consulted `autoRestart`
|
|
901
|
+
and restarted the plugin. A check that **threw** took a separate `catch` block
|
|
902
|
+
that incremented `failureCounters` and stopped there: it never cleared
|
|
903
|
+
`successCounters` and never read `autoRestart`. Because `raceCheckTimeout`
|
|
904
|
+
rejects rather than resolving, every `timeout` overrun lands in that `catch`,
|
|
905
|
+
so a plugin that hung was marked `failed` and never restarted no matter how
|
|
906
|
+
many rounds passed or what `autoRestart` said. The severer of the two failure
|
|
907
|
+
modes was the one that could not trigger recovery.
|
|
908
|
+
|
|
909
|
+
Both routes now funnel into one `recordFailedRound` step that owns the
|
|
910
|
+
counters, the `failureThreshold` comparison and the `autoRestart` decision, so
|
|
911
|
+
a thrown or timed-out check is restart-eligible on exactly the same terms as a
|
|
912
|
+
returned failure.
|
|
913
|
+
|
|
914
|
+
The per-route *status* label is deliberately unchanged: a throw is still the
|
|
915
|
+
separate `failed` status applied immediately with no threshold, as
|
|
916
|
+
`content/docs/protocol/kernel/lifecycle.mdx` documents. Only the counters and
|
|
917
|
+
the restart decision are shared — those are what `failureThreshold` and
|
|
918
|
+
`autoRestart` declare, and neither names a route.
|
|
919
|
+
- 8120808: feat(tooling): `@objectstack/core` declares a `typecheck` script, and its test and examples layers enter the ratchet (#14613)
|
|
920
|
+
|
|
921
|
+
`packages/core/package.json` declared exactly `build`, `test` and `test:watch`.
|
|
922
|
+
Around twenty sibling packages declare `typecheck`, and `turbo run typecheck`
|
|
923
|
+
selects only packages that declare the task — so the lint workflow's typecheck
|
|
924
|
+
job had no way to reach this package, and `pnpm --filter @objectstack/core
|
|
925
|
+
typecheck` failed with `ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT` for anyone who tried
|
|
926
|
+
it. The package's types ship anyway: `build` emits a 233 KB `dist/index.d.ts`,
|
|
927
|
+
and rest, runtime, mcp, services and plugins all import it.
|
|
928
|
+
|
|
929
|
+
The state was tracked but not runnable. `check:type-check-coverage` carried
|
|
930
|
+
`@objectstack/core` as a DEBT entry of 98 and had already re-measured it once
|
|
931
|
+
(91 to 98), so nothing was invisible — but a ledger only the gate can read is
|
|
932
|
+
not something a contributor working in the package can run, which is how a
|
|
933
|
+
dispatched task came to assume the script existed.
|
|
934
|
+
|
|
935
|
+
**Measured at `84b8190ae`, dependency closure built first.** The undivided
|
|
936
|
+
program (`tsc --noEmit -p tsconfig.json`, exactly as the DEBT entry measured it)
|
|
937
|
+
reports 98 errors across 12 files, and every one of the 12 is a `.test.ts`. The
|
|
938
|
+
same program restricted to the 63 non-test source files reports **zero**. So the
|
|
939
|
+
build layer graduated as it stood, and the 98 did not have to be repaired before
|
|
940
|
+
the script could exist.
|
|
941
|
+
|
|
942
|
+
**94 of the 98 were the check, not the code.** The repair is the split this
|
|
943
|
+
repo already runs for `spec`, `rest`, `objectql` and `client`: `tsconfig.json`
|
|
944
|
+
stays the build config and excludes the test layer; a new `tsconfig.test.json`
|
|
945
|
+
compiles that layer under the module semantics vitest actually executes it with
|
|
946
|
+
(`module: esnext`, `moduleResolution: bundler`), which retires 22 x TS2835, the
|
|
947
|
+
TS2347 beside them and the share of 71 x TS7006 they cascade into — an import
|
|
948
|
+
that does not resolve makes every symbol it names `any`. **No test file was
|
|
949
|
+
edited.** Strictness is inherited and untouched. The residue is 4 errors over 4
|
|
950
|
+
files, held per file and per signature in `test-typecheck-debt.json`, EXACT and
|
|
951
|
+
shrink-only.
|
|
952
|
+
|
|
953
|
+
**The `examples/` half was found by the new script, not by the card.** Declaring
|
|
954
|
+
`typecheck` flips the package from COVERED-BY-LEDGER to COVERED-BY-SCRIPT, and
|
|
955
|
+
`check:type-check-coverage`'s SOURCES_COVERED invariant immediately reported
|
|
956
|
+
`packages/core/examples` — 2 non-test source files in no tsc program at all.
|
|
957
|
+
Neither had ever compiled: `kernel-features-example.ts` imported `../index.js`
|
|
958
|
+
(above the package root, never existed) and `phase2-integration.ts` imported
|
|
959
|
+
`@objectstack/core`, i.e. this package self-referencing by a name it declares in
|
|
960
|
+
no dependency block. Collapsing that cascade exposed rather than removed errors,
|
|
961
|
+
12 to 29, all of them real and none of them new: 20 reads of `ObjectKernel`'s
|
|
962
|
+
**private** `logger`; four members of the security scan result that do not exist
|
|
963
|
+
(`passed`, `score`, `summary.critical`, `summary.high`, where the type carries
|
|
964
|
+
`status` and per-severity counts); and two config literals passing the unparsed
|
|
965
|
+
shapes where `PluginHealthMonitor.registerPlugin` and
|
|
966
|
+
`HotReloadManager.registerPlugin` are declared over the `Parsed` ones. That last
|
|
967
|
+
pair is retirement drift — this file was edited by two retirements (restart keys,
|
|
968
|
+
`watchPatterns`) while no tsc program could check the result. Every correction is
|
|
969
|
+
pinned to this package's own signatures; `packages/spec` was not touched.
|
|
970
|
+
|
|
971
|
+
`packages/core` therefore leaves the DEBT ledger: the coverage gate now reads
|
|
972
|
+
70/79 packages type-checked with 9 ledgered, where it read 68/78 with 10.
|
|
973
|
+
- f658793: Restore the #10096 standing invariant (「浏览器可达的 spec 导出面必须
|
|
974
|
+
schema-free」) for `@objectstack/core`'s plural→singular store-key fold.
|
|
975
|
+
|
|
976
|
+
`@objectstack/spec`: the `defineStack()` manifest-collection vocabulary
|
|
977
|
+
(`PLURAL_TO_SINGULAR`, `SINGULAR_TO_PLURAL`, `pluralToSingular`,
|
|
978
|
+
`singularToPlural`) moved to a schema-free module and is now ALSO exported
|
|
979
|
+
from the sanctioned schema-free entry `@objectstack/spec/meta-spelling`
|
|
980
|
+
(widened per the #10096 ruling's reference pattern). `@objectstack/spec/shared`
|
|
981
|
+
keeps the same four symbols as re-exports — no consumer-visible removal. The
|
|
982
|
+
manifest map and `META_URL_TO_SINGULAR` remain deliberately distinct contracts
|
|
983
|
+
(#8424).
|
|
984
|
+
|
|
985
|
+
`@objectstack/core`: `canonicalMetadataServiceType`'s one value import moves
|
|
986
|
+
from `@objectstack/spec/shared` to `@objectstack/spec/meta-spelling`, so
|
|
987
|
+
browser consumers of `@objectstack/core` (every `@objectstack/client` bundle)
|
|
988
|
+
no longer link the zod schema closure through the store-key fold.
|
|
989
|
+
- 0a8ebf3: Scope the legacy platform-admin deprecation pointer to walled tenancy postures
|
|
990
|
+
|
|
991
|
+
The request-side notice that tells an operator their unscoped `admin_full_access`
|
|
992
|
+
grant row is the OLD anchor — "it is removed in a later release", "re-anchor this
|
|
993
|
+
deployment by declaring its administrators in configuration" — was emitted without
|
|
994
|
+
regard to the deployment's tenancy posture, so it fired on `single` rigs too.
|
|
995
|
+
|
|
996
|
+
`single` is the DEFAULT posture, and on a `single` rig that row is not legacy at
|
|
997
|
+
all: the boot-time `bootstrapPlatformAdmin` mints it to promote the first human
|
|
998
|
+
user, and that promotion is ruled correct and unchanged. Such a deployment was
|
|
999
|
+
therefore being told, once per process, to migrate off an anchor that is not
|
|
1000
|
+
scheduled to go away, toward a variable its own promotion is pinned never to read.
|
|
1001
|
+
|
|
1002
|
+
The pointer is now gated on `postureEnforcesWall(resolveTenancyPosture())`, the
|
|
1003
|
+
same predicate and the same source the boot-side detector already reads, so the
|
|
1004
|
+
migration window's loudness is scoped to the walled postures actually in it.
|
|
1005
|
+
Walled rigs are unaffected and still receive the notice.
|
|
1006
|
+
|
|
1007
|
+
⛔ Standing is not touched: this is a log-line trigger, not access control. Every
|
|
1008
|
+
deployment resolves exactly the `PLATFORM_ADMIN` it resolved before.
|
|
1009
|
+
- fd289be: `HotReloadManager`'s refusal messages — the plugin-registration doors for retired `stateStrategy` values and removed config keys, and the `startWatching()` removal notice — no longer cite internal tracker ids. The prescriptions keep their customer-resolvable anchors (ADR-0049 enforce-or-remove, the `@objectstack/spec` / `@objectstack/core` versions, and the `scheduleReload` migration call); the `#NNNN` tokens, which resolve to nothing for the host author reading the refusal, are gone.
|
|
1010
|
+
- 2d5cee3: docs(core,service-cluster): retire the two docblocks left stale by `IPubSub`'s corrected delivery guarantee (#12836)
|
|
1011
|
+
|
|
1012
|
+
#12651 corrected `IPubSub`'s contract docblock: delivery is whatever the
|
|
1013
|
+
configured driver declares, no shipped driver exceeds at-most-once, a missed
|
|
1014
|
+
message is EXPECTED, and handlers must be idempotent **and** tolerate loss.
|
|
1015
|
+
Two docblocks elsewhere still described the world before that correction.
|
|
1016
|
+
|
|
1017
|
+
**`@objectstack/core` — `security/authz-invalidation-channel.ts`.** It carried a
|
|
1018
|
+
paragraph asserting, in the present tense, that the interface docblock "still
|
|
1019
|
+
says" *At-least-once delivery*, and that repairing it was a `packages/spec`
|
|
1020
|
+
change filed separately. That filing was #12651 and it has landed, so the
|
|
1021
|
+
paragraph is now false rather than merely stale — it sends the next reader
|
|
1022
|
+
looking for a live disagreement between the interface and the drivers that no
|
|
1023
|
+
longer exists. Replaced with a plain pointer to the interface docblock.
|
|
1024
|
+
Everything else in that docblock is unchanged: the at-most-once reasoning, the
|
|
1025
|
+
TTL-is-the-bound rule, and the best-effort-at-the-publish-site note all still
|
|
1026
|
+
hold.
|
|
1027
|
+
|
|
1028
|
+
**`@objectstack/service-cluster` — `memory/pubsub.ts`.** The line "At-least-once
|
|
1029
|
+
semantics held vacuously (a single in-process delivery)" was wrong on its own
|
|
1030
|
+
terms even before #12651: the same docblock states that handler errors are
|
|
1031
|
+
swallowed and logged via `onError`, so a handler that throws loses the message
|
|
1032
|
+
with no retry and no persistence. That is not at-least-once in any sense, and
|
|
1033
|
+
"vacuously" does not save it. Replaced with the honest statement — one
|
|
1034
|
+
synchronous in-process delivery attempt per subscriber, no persistence, no
|
|
1035
|
+
retry, no replay.
|
|
1036
|
+
|
|
1037
|
+
Prose only. No behaviour change, and no test changed.
|
|
1038
|
+
- a17da05: fix(core): only a backend fault populates `resolveLocalizationContext`'s failure memo (#11877)
|
|
1039
|
+
|
|
1040
|
+
`resolveLocalizationContext` memoizes an outcome for 30s whenever the read
|
|
1041
|
+
"failed" (#10221 — so a repeatedly-failing `sys_setting` query does not re-run,
|
|
1042
|
+
and the driver does not re-log it, on every request). The write condition was
|
|
1043
|
+
wider than the cache's own docblock: six legs set the flag and only **one** of
|
|
1044
|
+
them is the backend fault the docblock describes (the direct `ql.find` throw).
|
|
1045
|
+
The other five are the **settings service refusing** — a thrown `getMany`, each
|
|
1046
|
+
of the three older per-key `get`s, and the whole-block "service unavailable"
|
|
1047
|
+
handler.
|
|
1048
|
+
|
|
1049
|
+
Those five legs are reachable inside the settings engine's **bind window**
|
|
1050
|
+
(`SettingsService.getMany` refuses all-or-nothing for a `localization`
|
|
1051
|
+
namespace whose manifest is not yet registered), so:
|
|
1052
|
+
|
|
1053
|
+
- A caller that deliberately re-reads **after** the bind — the #11580 stdio
|
|
1054
|
+
repair re-resolves at `kernel:bootstrapped` for exactly this reason — was
|
|
1055
|
+
answered from the memo taken **inside** the window for up to 30s. The
|
|
1056
|
+
correction silently did not happen, with nothing in the output saying so.
|
|
1057
|
+
- A settings refusal standing alongside a perfectly **successful** direct read
|
|
1058
|
+
memoized that successful value — the staleness the docblock forbids outright
|
|
1059
|
+
and that `analytics-timezone.dogfood.test.ts` (#1982/#2018) exists to catch.
|
|
1060
|
+
|
|
1061
|
+
The memo is now written only for the direct-read fault. **#10221's protection
|
|
1062
|
+
is unchanged for the legs it was built for**: its environment (table not
|
|
1063
|
+
migrated yet) still memoizes, because the direct read throws there whether or
|
|
1064
|
+
not a settings refusal stands in front of it — pinned in both directions. And
|
|
1065
|
+
nothing is lost on the narrowed legs: those refusals throw out of an in-memory
|
|
1066
|
+
registry check *before* any query and *before* any log line, so memoizing them
|
|
1067
|
+
suppressed neither.
|
|
1068
|
+
|
|
1069
|
+
No signature, export or accepted-input change — the flag is internal to the
|
|
1070
|
+
module.
|
|
1071
|
+
- 7c41693: fix(core,plugin-auth,plugin-security): every `OS_PLATFORM_OWNER_EMAIL` reader asks the ONE list-aware parser (#13147)
|
|
1072
|
+
|
|
1073
|
+
`OS_PLATFORM_OWNER_EMAIL` accepts one address **or a comma-separated list** of
|
|
1074
|
+
them (#11663 Choice 2B). The list parse landed in a single home
|
|
1075
|
+
(`@objectstack/core`'s `platform-admin.ts`) and the authorization derivation
|
|
1076
|
+
consumed it — but every other reader kept calling `resolvePlatformOwnerEmail()`,
|
|
1077
|
+
which returns the operator's value trimmed and otherwise verbatim, and kept
|
|
1078
|
+
treating that whole string as ONE address.
|
|
1079
|
+
|
|
1080
|
+
An operator who configured a list therefore entered a self-contradictory state:
|
|
1081
|
+
authorization recognised them as a platform administrator, while four separate
|
|
1082
|
+
capabilities silently did nothing. Every direction failed **closed** — no
|
|
1083
|
+
privilege escalation existed at any point — but a declared capability vanished
|
|
1084
|
+
with no error anywhere:
|
|
1085
|
+
|
|
1086
|
+
- `bootstrap-platform-admin` promoted **nobody**, logging "will be promoted when
|
|
1087
|
+
that account registers" on every boot forever;
|
|
1088
|
+
- the walled operator stamp (`plugin-auth`) stamped **no** list member verified,
|
|
1089
|
+
so the account it should have provisioned was then refused elevation as
|
|
1090
|
+
`walled_owner_not_verified`;
|
|
1091
|
+
- `isVerifiedPlatformOwnerSession` / `platform-owner-wall-bypass` let **nobody**
|
|
1092
|
+
across the Layer 0 organization wall — the largest of the affected surfaces;
|
|
1093
|
+
- the walled boot diagnostic printed the raw list in the slot where an operator
|
|
1094
|
+
reads one address, and its dev-seed silence clause never matched.
|
|
1095
|
+
|
|
1096
|
+
All six readers now ask the same parser. `@objectstack/core` gains
|
|
1097
|
+
`isConfiguredPlatformAdminEmail(email, config)` — the membership half of
|
|
1098
|
+
`matchesConfiguredPlatformAdmin`, spelled once and shared, for the readers that
|
|
1099
|
+
hold a bare address rather than a `sys_user` row (the elevation gate keeps its
|
|
1100
|
+
two halves apart so `walled_owner_not_registered` and `walled_owner_not_verified`
|
|
1101
|
+
stay distinct answers; the stamp is handed an email before any row exists; the
|
|
1102
|
+
wall takes a fast negative before spending a row read). `PlatformAdminEmailConfig`
|
|
1103
|
+
gains `declaredSpellings`, the entries as the operator typed them, so the by-email
|
|
1104
|
+
`sys_user` lookup and the boot diagnostic get the as-typed form **from the one
|
|
1105
|
+
parse** instead of splitting the raw value a second time.
|
|
1106
|
+
|
|
1107
|
+
Behaviour for a single declared address is unchanged, including the
|
|
1108
|
+
case-insensitive match and the verbatim-spelling store lookup. A **refused**
|
|
1109
|
+
list (Choice 2B fails the whole variable closed on one unparseable entry) now
|
|
1110
|
+
reaches these readers as "zero administrators", which is the same answer they
|
|
1111
|
+
already gave for an unset variable — never a silently narrower set.
|
|
1112
|
+
|
|
1113
|
+
Two readers deliberately keep reading the raw value: the walled-boot refusal and
|
|
1114
|
+
the verification-path probe guard in `auth-plugin.ts` both use it as a pure
|
|
1115
|
+
truthiness test ("did the operator declare anything at all?"), which is
|
|
1116
|
+
grammar-independent. A census pin now enumerates the raw readers across both
|
|
1117
|
+
plugin packages and fails on a seventh.
|
|
1118
|
+
- 9688f58: `os plugin publish` now verifies the artifact's own declared `manifest.integrity` digests before uploading, and refuses the publish on a digest mismatch, a declared entry with no file, or a packaged file the map does not declare (an absent map still publishes — the field is optional). The pure checker, `verifyIntegrity`, lives in `@objectstack/core` beside the artifact-signature contract. Unpack-time re-verification remains the cloud control plane's obligation (#11331) and is not changed by this release.
|
|
1119
|
+
- 556ebc1: docs(core): correct `Plugin.type`'s TSDoc enumeration — it omitted `objectql` (#13762)
|
|
1120
|
+
|
|
1121
|
+
`Plugin.type` is typed `string` in `@objectstack/core`, so its TSDoc is the only
|
|
1122
|
+
enumeration a plugin author reading the interface ever sees; nothing type-checks
|
|
1123
|
+
them against it. That comment listed seven values while the declared set is
|
|
1124
|
+
eight: `PluginSchema.type` in `@objectstack/spec` is
|
|
1125
|
+
`z.enum(['standard', ...CORE_PLUGIN_TYPES])`, and `CORE_PLUGIN_TYPES` carries
|
|
1126
|
+
`objectql` — the type `packages/objectql/src/plugin.ts` declares on the engine
|
|
1127
|
+
plugin essentially every runtime loads first.
|
|
1128
|
+
|
|
1129
|
+
The comment now lists all eight and names `CORE_PLUGIN_TYPES` as the
|
|
1130
|
+
authoritative set. The same omission in the hand-written
|
|
1131
|
+
`content/docs/plugins/anatomy.mdx` transcription of this interface is corrected
|
|
1132
|
+
in the same change.
|
|
1133
|
+
- f7b25c5: fix(core): `successThreshold` now binds from every status that records a failure, so a declared count above 2 stops being unreachable (#11955)
|
|
1134
|
+
|
|
1135
|
+
`PluginHealthMonitor` consulted `successThreshold` only while a plugin's status
|
|
1136
|
+
was `unhealthy` or `degraded`. The first success in a recovery wrote
|
|
1137
|
+
`recovering` — a status that gate did not name — so the **second** success took
|
|
1138
|
+
the outer `else` and went straight to `healthy` without the counter being read
|
|
1139
|
+
at all. `failed` was in neither set either, so a plugin whose check threw
|
|
1140
|
+
recovered on its **first** success.
|
|
1141
|
+
|
|
1142
|
+
The declared value was therefore capped in practice:
|
|
1143
|
+
|
|
1144
|
+
| Status when the successes start | Consecutive successes actually required |
|
|
1145
|
+
| :--- | :--- |
|
|
1146
|
+
| `unhealthy` / `degraded` | 2, whatever `successThreshold` said |
|
|
1147
|
+
| `failed` / `recovering` | 1, whatever `successThreshold` said |
|
|
1148
|
+
|
|
1149
|
+
A declared `successThreshold: 5` was indistinguishable from `2`. The default is
|
|
1150
|
+
`1`, which is exactly the value at which the defect is invisible — every
|
|
1151
|
+
declared value above it was the one that misbehaved.
|
|
1152
|
+
|
|
1153
|
+
The counter is now consulted on the way out of every status that records an
|
|
1154
|
+
observed failure — `degraded`, `unhealthy`, `failed` and `recovering` — so
|
|
1155
|
+
`successThreshold: N` requires N consecutive successes from each of them, as
|
|
1156
|
+
its declaration says ("Consecutive successes needed to mark healthy"). The gate
|
|
1157
|
+
is a map that is exhaustive over `PluginHealthStatus`, so a status added to the
|
|
1158
|
+
spec fails to compile until it is placed on one side or the other; that is what
|
|
1159
|
+
`recovering` slipped through before.
|
|
1160
|
+
|
|
1161
|
+
`healthy` and `unknown` still promote on the first success, deliberately: the
|
|
1162
|
+
count is declared as a **recovery** criterion ("Number of consecutive successes
|
|
1163
|
+
to recover from unhealthy state") and neither of those records a failure to
|
|
1164
|
+
recover from — `unknown` is the status `registerPlugin` writes before any check
|
|
1165
|
+
has run.
|
|
1166
|
+
|
|
1167
|
+
**Behaviour change, only for configs that declare `successThreshold` above 1.**
|
|
1168
|
+
At the default `1` every route is byte-for-byte what it was: one success has
|
|
1169
|
+
always been enough and still is. A plugin declaring a higher count now takes
|
|
1170
|
+
the number of consecutive successes it asked for before it is reported
|
|
1171
|
+
`healthy`, including after a `failed` round and after an `autoRestart`.
|
|
1172
|
+
|
|
1173
|
+
This also makes #11852's `successCounters` reset load-bearing. That fix cleared
|
|
1174
|
+
the counter on the thrown failure route, and could not be pinned: the counter's
|
|
1175
|
+
only read site was unreachable with a stale non-zero value, so any test would
|
|
1176
|
+
have passed for the wrong reason. With `failed` gated on the counter, a throw
|
|
1177
|
+
that interrupts a recovery now demonstrably starts the count over.
|
|
1178
|
+
- Updated dependencies [809d417]
|
|
1179
|
+
- Updated dependencies [387e231]
|
|
1180
|
+
- Updated dependencies [f794e4e]
|
|
1181
|
+
- Updated dependencies [cae2169]
|
|
1182
|
+
- Updated dependencies [b812a54]
|
|
1183
|
+
- Updated dependencies [2d4fa75]
|
|
1184
|
+
- Updated dependencies [0e4e51b]
|
|
1185
|
+
- Updated dependencies [e84bbf6]
|
|
1186
|
+
- Updated dependencies [effae80]
|
|
1187
|
+
- Updated dependencies [d62f990]
|
|
1188
|
+
- Updated dependencies [c45d8e6]
|
|
1189
|
+
- Updated dependencies [2e3e8c7]
|
|
1190
|
+
- Updated dependencies [e621291]
|
|
1191
|
+
- Updated dependencies [40a93b5]
|
|
1192
|
+
- Updated dependencies [101ad2c]
|
|
1193
|
+
- Updated dependencies [d5b330d]
|
|
1194
|
+
- Updated dependencies [dda969c]
|
|
1195
|
+
- Updated dependencies [1f45690]
|
|
1196
|
+
- Updated dependencies [277948f]
|
|
1197
|
+
- Updated dependencies [8bdd955]
|
|
1198
|
+
- Updated dependencies [f3bbbef]
|
|
1199
|
+
- Updated dependencies [4f24e9d]
|
|
1200
|
+
- Updated dependencies [6a180e4]
|
|
1201
|
+
- Updated dependencies [474242f]
|
|
1202
|
+
- Updated dependencies [63cd487]
|
|
1203
|
+
- Updated dependencies [bd4aa4e]
|
|
1204
|
+
- Updated dependencies [803eaab]
|
|
1205
|
+
- Updated dependencies [f8e8f03]
|
|
1206
|
+
- Updated dependencies [eae824e]
|
|
1207
|
+
- Updated dependencies [f6fa22c]
|
|
1208
|
+
- Updated dependencies [8a483b3]
|
|
1209
|
+
- Updated dependencies [97bcd99]
|
|
1210
|
+
- Updated dependencies [df59de0]
|
|
1211
|
+
- Updated dependencies [96e25a8]
|
|
1212
|
+
- Updated dependencies [f75a38a]
|
|
1213
|
+
- Updated dependencies [7a25e7d]
|
|
1214
|
+
- Updated dependencies [1fa05a6]
|
|
1215
|
+
- Updated dependencies [c85a265]
|
|
1216
|
+
- Updated dependencies [dcb10a5]
|
|
1217
|
+
- Updated dependencies [773a999]
|
|
1218
|
+
- Updated dependencies [35dffea]
|
|
1219
|
+
- Updated dependencies [776a098]
|
|
1220
|
+
- Updated dependencies [5060877]
|
|
1221
|
+
- Updated dependencies [4f6325d]
|
|
1222
|
+
- Updated dependencies [52954c0]
|
|
1223
|
+
- Updated dependencies [2aa8456]
|
|
1224
|
+
- Updated dependencies [93809a3]
|
|
1225
|
+
- Updated dependencies [7c0d0c3]
|
|
1226
|
+
- Updated dependencies [daae7aa]
|
|
1227
|
+
- Updated dependencies [8dc22d6]
|
|
1228
|
+
- Updated dependencies [279431e]
|
|
1229
|
+
- Updated dependencies [948dd6b]
|
|
1230
|
+
- Updated dependencies [3b4c56c]
|
|
1231
|
+
- Updated dependencies [ae8edd2]
|
|
1232
|
+
- Updated dependencies [e25403c]
|
|
1233
|
+
- Updated dependencies [a81aa9d]
|
|
1234
|
+
- Updated dependencies [64baa68]
|
|
1235
|
+
- Updated dependencies [9fa70d7]
|
|
1236
|
+
- Updated dependencies [09db64a]
|
|
1237
|
+
- Updated dependencies [92916e7]
|
|
1238
|
+
- Updated dependencies [a84f3ea]
|
|
1239
|
+
- Updated dependencies [f2eaae8]
|
|
1240
|
+
- Updated dependencies [56c093c]
|
|
1241
|
+
- Updated dependencies [c09451b]
|
|
1242
|
+
- Updated dependencies [ba64877]
|
|
1243
|
+
- Updated dependencies [7345308]
|
|
1244
|
+
- Updated dependencies [79b6a22]
|
|
1245
|
+
- Updated dependencies [30d96ab]
|
|
1246
|
+
- Updated dependencies [f658793]
|
|
1247
|
+
- Updated dependencies [c95ad19]
|
|
1248
|
+
- Updated dependencies [e58ea8b]
|
|
1249
|
+
- Updated dependencies [4a17645]
|
|
1250
|
+
- Updated dependencies [3795c5f]
|
|
1251
|
+
- Updated dependencies [8ab926b]
|
|
1252
|
+
- Updated dependencies [7317cf2]
|
|
1253
|
+
- Updated dependencies [e25e839]
|
|
1254
|
+
- Updated dependencies [5997207]
|
|
1255
|
+
- Updated dependencies [8b13cc8]
|
|
1256
|
+
- Updated dependencies [4a4a35d]
|
|
1257
|
+
- Updated dependencies [86e765a]
|
|
1258
|
+
- Updated dependencies [1d7e76a]
|
|
1259
|
+
- Updated dependencies [53dc739]
|
|
1260
|
+
- Updated dependencies [fd289be]
|
|
1261
|
+
- Updated dependencies [03bf7b1]
|
|
1262
|
+
- Updated dependencies [f90e820]
|
|
1263
|
+
- Updated dependencies [18d816a]
|
|
1264
|
+
- Updated dependencies [e8bd715]
|
|
1265
|
+
- Updated dependencies [b91c351]
|
|
1266
|
+
- Updated dependencies [a28a3c0]
|
|
1267
|
+
- Updated dependencies [daeaaf9]
|
|
1268
|
+
- Updated dependencies [c459da6]
|
|
1269
|
+
- Updated dependencies [e914733]
|
|
1270
|
+
- Updated dependencies [f887e52]
|
|
1271
|
+
- Updated dependencies [881f8d8]
|
|
1272
|
+
- Updated dependencies [3bfa1e6]
|
|
1273
|
+
- Updated dependencies [901355c]
|
|
1274
|
+
- Updated dependencies [34ce8e7]
|
|
1275
|
+
- Updated dependencies [33681ea]
|
|
1276
|
+
- Updated dependencies [bfe13c8]
|
|
1277
|
+
- Updated dependencies [0fb3044]
|
|
1278
|
+
- Updated dependencies [4635f3e]
|
|
1279
|
+
- Updated dependencies [ee3595c]
|
|
1280
|
+
- Updated dependencies [b2eab95]
|
|
1281
|
+
- Updated dependencies [93940d4]
|
|
1282
|
+
- Updated dependencies [3a04b01]
|
|
1283
|
+
- Updated dependencies [45b9051]
|
|
1284
|
+
- Updated dependencies [b9e9227]
|
|
1285
|
+
- Updated dependencies [d395692]
|
|
1286
|
+
- Updated dependencies [5894d30]
|
|
1287
|
+
- Updated dependencies [a3765f6]
|
|
1288
|
+
- Updated dependencies [e22158f]
|
|
1289
|
+
- Updated dependencies [7404925]
|
|
1290
|
+
- Updated dependencies [0c2334f]
|
|
1291
|
+
- Updated dependencies [778c59f]
|
|
1292
|
+
- Updated dependencies [d2619fd]
|
|
1293
|
+
- Updated dependencies [6acb11a]
|
|
1294
|
+
- Updated dependencies [33c5fd3]
|
|
1295
|
+
- Updated dependencies [20b0fdb]
|
|
1296
|
+
- Updated dependencies [905019b]
|
|
1297
|
+
- Updated dependencies [a286411]
|
|
1298
|
+
- Updated dependencies [98c0d33]
|
|
1299
|
+
- Updated dependencies [368a82e]
|
|
1300
|
+
- Updated dependencies [a3d5724]
|
|
1301
|
+
- Updated dependencies [93ea19b]
|
|
1302
|
+
- Updated dependencies [9ee2dcf]
|
|
1303
|
+
- Updated dependencies [8cb96ec]
|
|
1304
|
+
- Updated dependencies [8f10a79]
|
|
1305
|
+
- Updated dependencies [6269a55]
|
|
1306
|
+
- Updated dependencies [22e5236]
|
|
1307
|
+
- Updated dependencies [0fb8760]
|
|
1308
|
+
- Updated dependencies [e5ce2ed]
|
|
1309
|
+
- Updated dependencies [be21955]
|
|
1310
|
+
- Updated dependencies [bc56e18]
|
|
1311
|
+
- Updated dependencies [be21955]
|
|
1312
|
+
- Updated dependencies [a9ee989]
|
|
1313
|
+
- Updated dependencies [4d0d944]
|
|
1314
|
+
- Updated dependencies [15d58db]
|
|
1315
|
+
- Updated dependencies [d63b014]
|
|
1316
|
+
- Updated dependencies [9abe4e4]
|
|
1317
|
+
- Updated dependencies [2cc7122]
|
|
1318
|
+
- Updated dependencies [50d6c92]
|
|
1319
|
+
- Updated dependencies [9e0ba21]
|
|
1320
|
+
- Updated dependencies [311433f]
|
|
1321
|
+
- Updated dependencies [3e5ad08]
|
|
1322
|
+
- Updated dependencies [9abe4e4]
|
|
1323
|
+
- Updated dependencies [b7131f3]
|
|
1324
|
+
- Updated dependencies [e5812fa]
|
|
1325
|
+
- Updated dependencies [7085f90]
|
|
1326
|
+
- Updated dependencies [dee4dd4]
|
|
1327
|
+
- Updated dependencies [ce7e497]
|
|
1328
|
+
- Updated dependencies [51ecb2f]
|
|
1329
|
+
- Updated dependencies [9086761]
|
|
1330
|
+
- Updated dependencies [42a117b]
|
|
1331
|
+
- Updated dependencies [1401ae7]
|
|
1332
|
+
- Updated dependencies [4297fe7]
|
|
1333
|
+
- Updated dependencies [e398863]
|
|
1334
|
+
- Updated dependencies [d16df74]
|
|
1335
|
+
- Updated dependencies [f11fc61]
|
|
1336
|
+
- Updated dependencies [e808890]
|
|
1337
|
+
- Updated dependencies [8f79379]
|
|
1338
|
+
- Updated dependencies [e6ca40e]
|
|
1339
|
+
- Updated dependencies [0c77ea4]
|
|
1340
|
+
- Updated dependencies [52954c0]
|
|
1341
|
+
- Updated dependencies [89eb997]
|
|
1342
|
+
- Updated dependencies [aa5994e]
|
|
1343
|
+
- Updated dependencies [be93457]
|
|
1344
|
+
- Updated dependencies [a65db76]
|
|
1345
|
+
- Updated dependencies [2cf5a96]
|
|
1346
|
+
- Updated dependencies [15eb2c9]
|
|
1347
|
+
- Updated dependencies [5691b07]
|
|
1348
|
+
- Updated dependencies [2a6122b]
|
|
1349
|
+
- Updated dependencies [225e769]
|
|
1350
|
+
- Updated dependencies [8af88dd]
|
|
1351
|
+
- Updated dependencies [fb5fbb8]
|
|
1352
|
+
- Updated dependencies [d7b3963]
|
|
1353
|
+
- Updated dependencies [b72db01]
|
|
1354
|
+
- Updated dependencies [dce5cd4]
|
|
1355
|
+
- Updated dependencies [177ebdc]
|
|
1356
|
+
- Updated dependencies [8d237b4]
|
|
1357
|
+
- Updated dependencies [2d2e6f0]
|
|
1358
|
+
- Updated dependencies [2d8dd8d]
|
|
1359
|
+
- Updated dependencies [22d573e]
|
|
1360
|
+
- Updated dependencies [b5a2398]
|
|
1361
|
+
- Updated dependencies [348860c]
|
|
1362
|
+
- Updated dependencies [5383fa6]
|
|
1363
|
+
- Updated dependencies [5b3ff63]
|
|
1364
|
+
- Updated dependencies [1a6a19c]
|
|
1365
|
+
- Updated dependencies [527e050]
|
|
1366
|
+
- Updated dependencies [dd33bf9]
|
|
1367
|
+
- Updated dependencies [4cb2a90]
|
|
1368
|
+
- Updated dependencies [74a7804]
|
|
1369
|
+
- Updated dependencies [53d3689]
|
|
1370
|
+
- Updated dependencies [b3a63d3]
|
|
1371
|
+
- Updated dependencies [033a34c]
|
|
1372
|
+
- Updated dependencies [4d25d22]
|
|
1373
|
+
- Updated dependencies [1ffee51]
|
|
1374
|
+
- Updated dependencies [5ae4303]
|
|
1375
|
+
- Updated dependencies [ece4dad]
|
|
1376
|
+
- Updated dependencies [e9b377e]
|
|
1377
|
+
- Updated dependencies [146f448]
|
|
1378
|
+
- Updated dependencies [735f5c7]
|
|
1379
|
+
- Updated dependencies [a7e18de]
|
|
1380
|
+
- Updated dependencies [366f895]
|
|
1381
|
+
- Updated dependencies [dc75ba8]
|
|
1382
|
+
- Updated dependencies [cce0aa9]
|
|
1383
|
+
- Updated dependencies [e764507]
|
|
1384
|
+
- Updated dependencies [cff17af]
|
|
1385
|
+
- Updated dependencies [39404f3]
|
|
1386
|
+
- Updated dependencies [ca1965f]
|
|
1387
|
+
- Updated dependencies [8619f95]
|
|
1388
|
+
- Updated dependencies [b706af9]
|
|
1389
|
+
- Updated dependencies [db8c288]
|
|
1390
|
+
- Updated dependencies [0e5fe7f]
|
|
1391
|
+
- Updated dependencies [fc9ba76]
|
|
1392
|
+
- Updated dependencies [0f94cc7]
|
|
1393
|
+
- Updated dependencies [a11c1a5]
|
|
1394
|
+
- Updated dependencies [71f9cd1]
|
|
1395
|
+
- Updated dependencies [ee17d86]
|
|
1396
|
+
- Updated dependencies [cdbd920]
|
|
1397
|
+
- Updated dependencies [18c432e]
|
|
1398
|
+
- Updated dependencies [3c418c4]
|
|
1399
|
+
- Updated dependencies [fa8715a]
|
|
1400
|
+
- Updated dependencies [a933ed7]
|
|
1401
|
+
- Updated dependencies [b3ca463]
|
|
1402
|
+
- Updated dependencies [a933ed7]
|
|
1403
|
+
- Updated dependencies [0d4a6a8]
|
|
1404
|
+
- Updated dependencies [518d5e5]
|
|
1405
|
+
- Updated dependencies [6643ba1]
|
|
1406
|
+
- Updated dependencies [eeba2ef]
|
|
1407
|
+
- Updated dependencies [ec4c4d2]
|
|
1408
|
+
- Updated dependencies [424f73c]
|
|
1409
|
+
- Updated dependencies [cccbe51]
|
|
1410
|
+
- Updated dependencies [a8d6b1d]
|
|
1411
|
+
- Updated dependencies [e4a7695]
|
|
1412
|
+
- Updated dependencies [87075b1]
|
|
1413
|
+
- Updated dependencies [fc58a99]
|
|
1414
|
+
- Updated dependencies [14cfc00]
|
|
1415
|
+
- Updated dependencies [1c6f7b4]
|
|
1416
|
+
- Updated dependencies [e854a53]
|
|
1417
|
+
- Updated dependencies [dfebfc8]
|
|
1418
|
+
- Updated dependencies [d028b37]
|
|
1419
|
+
- Updated dependencies [122ef38]
|
|
1420
|
+
- Updated dependencies [4a37870]
|
|
1421
|
+
- Updated dependencies [428f9b2]
|
|
1422
|
+
- Updated dependencies [aa7ff56]
|
|
1423
|
+
- Updated dependencies [c41b42e]
|
|
1424
|
+
- Updated dependencies [c4db311]
|
|
1425
|
+
- Updated dependencies [750fff5]
|
|
1426
|
+
- Updated dependencies [c19035e]
|
|
1427
|
+
- Updated dependencies [ececf7a]
|
|
1428
|
+
- Updated dependencies [d173125]
|
|
1429
|
+
- Updated dependencies [8eeca27]
|
|
1430
|
+
- Updated dependencies [8425c17]
|
|
1431
|
+
- Updated dependencies [a5ef1d8]
|
|
1432
|
+
- Updated dependencies [87ad30c]
|
|
1433
|
+
- Updated dependencies [772d5de]
|
|
1434
|
+
- Updated dependencies [ce80ec2]
|
|
1435
|
+
- Updated dependencies [b372318]
|
|
1436
|
+
- Updated dependencies [97a2263]
|
|
1437
|
+
- Updated dependencies [29d0676]
|
|
1438
|
+
- Updated dependencies [0169d49]
|
|
1439
|
+
- Updated dependencies [6bd3231]
|
|
1440
|
+
- Updated dependencies [d2b5ba8]
|
|
1441
|
+
- Updated dependencies [b799ac5]
|
|
1442
|
+
- Updated dependencies [8f74307]
|
|
1443
|
+
- Updated dependencies [d23dc08]
|
|
1444
|
+
- Updated dependencies [644ad50]
|
|
1445
|
+
- Updated dependencies [9735662]
|
|
1446
|
+
- Updated dependencies [4d5b4f8]
|
|
1447
|
+
- Updated dependencies [0da7cd2]
|
|
1448
|
+
- Updated dependencies [28a5c3e]
|
|
1449
|
+
- Updated dependencies [4bc18e5]
|
|
1450
|
+
- @objectstack/spec@17.3.0
|
|
1451
|
+
- @objectstack/types@17.3.0
|
|
1452
|
+
|
|
3
1453
|
## 17.2.0
|
|
4
1454
|
|
|
5
1455
|
### Minor Changes
|