@objectstack/core 17.2.0 → 17.4.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 +2099 -0
- package/dist/index.cjs +1108 -543
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1445 -233
- package/dist/index.d.ts +1445 -233
- package/dist/index.js +1064 -531
- package/dist/index.js.map +1 -1
- package/dist/logger.cjs.map +1 -1
- package/dist/logger.js.map +1 -1
- package/package.json +11 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,2104 @@
|
|
|
1
1
|
# @objectstack/core
|
|
2
2
|
|
|
3
|
+
## 17.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2ed6be6: Advisory validation rules no longer flood the startup log, and no longer count a row twice on a clean first boot.
|
|
8
|
+
|
|
9
|
+
A `severity: 'warning'` (or `'info'`) validation rule is advisory: it never blocks a write, and its message is written for a person filling in a form. Evaluated across a seed load it produced one `WARN` line per row, so a clean-database first boot opened with a wall of form hints re-cast as boot diagnostics — and an app could reach "zero warnings" only by bending its data or deleting the rule.
|
|
10
|
+
|
|
11
|
+
Two changes, and neither moves what a rule evaluates to:
|
|
12
|
+
|
|
13
|
+
- **Aggregated reporting on the seed/boot path.** `SeedLoaderService.load()` now runs inside an advisory aggregation scope, and reports one summary line per rule — the rule, the object, the row count, the rule's own message and example rows — instead of one line per row. Off that path (an ordinary interactive write) nothing changes: the same per-write line is emitted verbatim. The new scope is `runWithAdvisoryAggregation` / `recordAdvisoryHit` in `@objectstack/core`.
|
|
14
|
+
- **Advisory rules are counted by row, not by write.** An `update` whose payload touches only platform-injected system columns — the shape `claimSeedOwnership` writes when it hands seeded rows to the first admin, `{ owner_id }` — changes no business field, so it no longer re-evaluates the object's advisory rules. Previously a seeded row rang once on insert and again when the claim scan rewrote `owner_id`, so anyone counting startup warnings over-estimated by the number of claimed objects.
|
|
15
|
+
|
|
16
|
+
`error`-severity rules are untouched by both changes: an invariant is still enforced on every write, whoever issued it and however little it moved. Membership of the "system column" set is resolved per object by `resolveInjectedSystemColumns`, so an object that declares `ownership: 'org'` (no `owner_id`) or `systemFields: false` is judged on its own columns rather than a fixed list.
|
|
17
|
+
- b0529e1: fix(core): `ResolvedAuthzContext.authRefusal` is removed — a published member nothing ever read (#14273)
|
|
18
|
+
|
|
19
|
+
**BREAKING** published-type narrowing, shipped as `minor` under the repo's
|
|
20
|
+
launch-window convention for breaking changes. `ResolvedAuthzContext` — the
|
|
21
|
+
envelope `resolveAuthzContext` answers, exported from `@objectstack/core`'s
|
|
22
|
+
root entry — loses its optional `authRefusal?: { reason; message }` member.
|
|
23
|
+
Maintainer ruling 2026-09-02 (option A, ADR-0049 enforce-or-remove),
|
|
24
|
+
re-affirmed 2026-09-03 as A1 with the carriers a published narrowing owes
|
|
25
|
+
once the type was measured as public API: the member was written by the two
|
|
26
|
+
posture-conditional API-key refusals (`organization_required` at admission,
|
|
27
|
+
`organization_membership_ended` after grants) since #8287 and read by nothing
|
|
28
|
+
— zero runtime readers across every transport and consumer in the repo for
|
|
29
|
+
its whole life; only test assertions ever looked at it.
|
|
30
|
+
|
|
31
|
+
What changes:
|
|
32
|
+
|
|
33
|
+
- `ResolvedAuthzContext` no longer declares `authRefusal`. Code that reads
|
|
34
|
+
`ctx.authRefusal` stops compiling (`TS2339`); at runtime the property was
|
|
35
|
+
already absent from every resolved context except the two refused ones.
|
|
36
|
+
- The two refusals themselves are UNCHANGED: they still fire, still fail
|
|
37
|
+
closed (no `userId`, empty grants), and every transport still answers the
|
|
38
|
+
generic anonymous `401 UNAUTHENTICATED`. No status code, body or header
|
|
39
|
+
moves — a holder of someone else's key learns nothing, exactly as before.
|
|
40
|
+
- The refusal REASON is observable on exactly one surface, and it is not the
|
|
41
|
+
envelope: the server-side `[security] API key refused (reason) ...` `warn`
|
|
42
|
+
line at the decision point (#15256 / 2A), which names the key row id,
|
|
43
|
+
principal and organization for the operator. The pins that kept the two
|
|
44
|
+
reasons distinguishable through the field now read that line.
|
|
45
|
+
- `ApiKeyRefusalReason` and `ApiKeyAdmission` are unchanged — the reason
|
|
46
|
+
vocabulary still exists; it just no longer has a copy on the resolved
|
|
47
|
+
context.
|
|
48
|
+
|
|
49
|
+
**Migration.** A consumer that read `ctx.authRefusal` deletes the read; there
|
|
50
|
+
is no replacement on the envelope, by design — disclosing the reason to a
|
|
51
|
+
caller (option B) was ruled out as a security-boundary question, and the
|
|
52
|
+
recorded fallback if a reader ever appears is an audit-side outlet (option C),
|
|
53
|
+
never the wire. Fail-closed handling keys on the absent `userId`, as every
|
|
54
|
+
in-repo transport already did. An operator who needs the reason reads the
|
|
55
|
+
server log line.
|
|
56
|
+
|
|
57
|
+
<!-- adr-0087: not-required (runtime-interface-only packages/core/src/security/resolve-authz-context.ts#ResolvedAuthzContext) A published runtime TypeScript interface lost an optional member. No Zod schema, no `packages/spec` declaration, no object definition and no stored representation is touched — `ResolvedAuthzContext` is a plain interface in `packages/core`, projected from no schema and referenced by no metadata surface — so `objectstack migrate meta` has nothing to rewrite and there is no tombstone to mint. The channel that reaches an affected consumer is the compiler at the read site (`TS2339`), which is more precise than a ledger line. The in-repo census (zero runtime readers; the only readers were test assertions, relocated onto the `warnApiKeyRefusal` line) and the workspace typecheck are recorded on the PR. -->
|
|
58
|
+
- 66dc6ab: Plugin startup elapsed time is now reported as `durationMs` — the unit-bearing name the spec contract for the same result declares. `startTime`, which never held a start time, is deprecated and still populated.
|
|
59
|
+
|
|
60
|
+
`PluginStartupResult.startTime` (`packages/core/src/plugin-loader.ts`) has always been assigned `Date.now() - startTime`, an elapsed duration, on both the success and the failure path. The name therefore asserts the opposite of the value: a reader who correctly takes `startTime` for an instant and writes `Date.now() - result.startTime` gets an age near the epoch rather than a wait. That is the one failure mode a unit convention cannot rescue — an ambiguous name makes someone stop and check, this one lets them proceed confidently wrong.
|
|
61
|
+
|
|
62
|
+
This is not a naming preference but a divergence between what is declared and what is enforced. `packages/spec/src/kernel/startup-orchestrator.zod.ts` declares `durationMs: z.number().min(0)` — "Time taken to start the plugin in milliseconds" — for the same measure on the same result, the outcome of starting one plugin; the bare `duration` spelling is retired there with a `retiredKey()` tombstone whose prescription is "Rename the key to `durationMs`", because a duration-shaped number carries its unit in its key name, never only in describe prose. The contract surface was already correct and `packages/core` had drifted away from it. The same computation already has an honest name twelve lines above the defect in the same file: `PluginLoadResult.loadTime` carries the identical `Date.now() - startTime` under a name that does not lie.
|
|
63
|
+
|
|
64
|
+
Three sites move, and every one of them is additive — nothing is removed, so no consumer has to change anything on this release:
|
|
65
|
+
|
|
66
|
+
- `PluginStartupResult` gains `durationMs?: number`. `startTime?: number` stays, still carrying the same value, marked `@deprecated` with a doc comment that states plainly it is elapsed milliseconds and not an instant.
|
|
67
|
+
- `ObjectKernel.getPluginStartupDurations()` is added; `getPluginMetrics()` becomes a `@deprecated` delegating alias returning the same map.
|
|
68
|
+
- The private `pluginStartTimes` map is renamed `pluginStartupDurations` (private; no reader outside `kernel.ts` in this repo or in the pinned `objectui` sibling).
|
|
69
|
+
|
|
70
|
+
Migration, where you want it: read `result.durationMs` where you read `result.startTime`, and `kernel.getPluginStartupDurations()` where you called `kernel.getPluginMetrics()`. The values are identical, so the change can be made at leisure; both old spellings keep working until they are removed.
|
|
71
|
+
|
|
72
|
+
ADR-0087 disposition: no migration-ledger entry, and none is required. Nothing is retired by this release — the old member and the old method both remain, populated and callable, which is ADR-0087's L1 outcome (the old shape keeps loading while the fleet moves) rather than a retirement. There is also nothing for `objectstack migrate meta` to rewrite: `packages/core/src/plugin-loader.ts#PluginStartupResult` is a runtime TypeScript interface with no Zod schema, no `packages/spec` declaration and no stored representation — the `PluginStartupResult` in `packages/spec/src/kernel/startup-orchestrator.zod.ts` is a separate, differently-shaped declaration that this change does not touch, and that schema's own `duration` tombstone entry (`packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginStartupResult__duration.ts`) records that core's interface is not a reader of it. Core simply does not adopt the retired spelling. When the deprecated spellings are removed, that removal is the change that carries the ledger disposition.
|
|
73
|
+
- 2025b1f: `kernel.use()` now enforces the declared plugin contract. A plugin object that `PluginSchema` (`@objectstack/spec`, `kernel/plugin.zod.ts`) refuses is refused at load instead of being stored and mounted.
|
|
74
|
+
|
|
75
|
+
**BREAKING** accept-set narrowing on a published runtime entry point, shipped as `minor` under the repo's launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`). **This refuses input the runtime accepted before**, which is also why it is not a `patch`: `PluginSchema` had zero runtime callers, so every constraint it declared beyond `name`, `init` and semver was a declaration with nothing behind it. The sharpest reading of that gap, one input and two answers: `defineStack` accepted `type: 'ui-plugin'` while `PluginSchema.safeParse` refused it — and only one of those answers was on the path a real plugin takes. Maintainer ruling of 2026-09-06 (ADR-0049 enforce-or-remove): the protocol is the baseline, the runtime aligns to it.
|
|
76
|
+
|
|
77
|
+
**Exactly what is newly refused: all EIGHT declared keys, not three.** The schema declares nine optional keys; the loader excludes `version` (below), so enforcement reaches these eight, each refused with the offending key named in the message:
|
|
78
|
+
|
|
79
|
+
- **`id`** — a non-string, or the empty string (`z.string().min(1)`).
|
|
80
|
+
- **`type`** — any value outside the closed set `standard`, `ui`, `driver`, `server`, `app`, `theme`, `agent`, `objectql`.
|
|
81
|
+
- **`staticPath`** — a non-string.
|
|
82
|
+
- **`slug`** — a non-string, or a string that does not match `/^[a-z0-9-_]+$/`.
|
|
83
|
+
- **`default`** — a non-boolean.
|
|
84
|
+
- **`description`** — a non-string.
|
|
85
|
+
- **`author`** — a non-string. An object such as `{ name: 'x' }` is refused; the declared type is a plain string.
|
|
86
|
+
- **`homepage`** — a non-string, or a string that is not a URL.
|
|
87
|
+
|
|
88
|
+
**`null` is refused on every one of the eight.** These keys are `.optional()`, which admits absence and `undefined` — never an explicit `null`. A plugin object that spells "no value" as `null` on any of the eight loaded before and is refused now.
|
|
89
|
+
|
|
90
|
+
**What a refusal looks like.** It travels the loader's existing plugin-load error path — no new error channel — carrying the stable code `PLUGIN_CONTRACT_VIOLATION` at the head of the message and on the error's `code` property, and naming the plugin plus the first violated key:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
PLUGIN_CONTRACT_VIOLATION: plugin '@acme/console' is refused by the declared
|
|
94
|
+
plugin contract at 'type': Invalid option: expected one of "standard"|"ui"|…
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A wrong `type` is therefore diagnosable at boot rather than at route mount. The code is a **boot refusal**, not wire vocabulary: it is raised before any HTTP boundary exists, and no door answers with it.
|
|
98
|
+
|
|
99
|
+
**What is STILL ACCEPTED — the door is not narrowed past those eight keys.** Measured on this tree, not assumed:
|
|
100
|
+
|
|
101
|
+
- **Unknown keys still pass.** `PluginSchema` is a plain `z.object` with **no `.strict()`** — the strip posture — and the parse output is discarded, so a valid plugin carrying four keys the schema never declares loads, and is stored as the very object that was passed in with all of its keys intact. A plugin is refused for what it says about a **declared** key, never for saying something extra.
|
|
102
|
+
- **A version-less plugin still loads**, exactly as before.
|
|
103
|
+
- **A plugin declaring no `type` still loads and still stores no `type`**: `PluginSchema`'s `.default('standard')` is **not** written back.
|
|
104
|
+
- **A class-based plugin keeps its identity, its prototype and its prototype methods.** The plugin object is validated, never replaced: `safeParse` is read for `success` and its output discarded, because a copy destroys the prototype chain of class-based plugins — the reason `PluginLoader.toPluginMetadata` is a cast. That survival is pinned by test, not asserted in prose.
|
|
105
|
+
- **`version` is excluded from this enforcement entirely**, so `1.0.0-alpha.1` and `1.0.0+20230101` still load. The schema spells `version` as `/^\d+\.\d+\.\d+$/`, which refuses the prerelease and build-metadata forms SemVer 2.0.0 defines, while the loader's own `isValidSemanticVersion` implements the full grammar and accepts them — deliberately, pinned by `plugin-loader.test.ts`. Enforcing the narrower spelling would retire that capability silently, so the loader's check remains authoritative for `version`. Reconciling the two spellings is spec work, tracked separately.
|
|
106
|
+
|
|
107
|
+
**Blast radius, measured rather than assumed.** Every in-repo plugin object declares a `type` inside the closed set (`standard` ×62, `server` ×2, `driver` ×2, `objectql`, `app`), and the repo contains no producer of `slug` or `homepage` on a plugin object at all — so no in-repo plugin changes behaviour. Externally authored plugins are the population this reaches, and they are exactly the population that never met the compile-time `Plugin.type` union either.
|
|
108
|
+
|
|
109
|
+
<!-- adr-0087: not-required (no-migration-prescription) An accept-set narrowing performed entirely at the runtime boot path: `PluginSchema` is READ by `kernel.use()`, not changed. No metadata key, spec symbol, Zod schema, object definition or stored representation is added, removed or given a different name, so `objectstack migrate meta` has nothing to visit and there is no tombstone to mint. Stored metadata is untouched; what moves is which plugin OBJECTS a boot accepts. The channel that reaches an affected plugin author is the refusal itself, which names the offending key at `kernel.use()` and is more precise than a ledger line — and which value a formerly-refused key should carry is authoring intent no ledger entry can decide. -->
|
|
110
|
+
- 51ae731: `LiteKernel.use()` now enforces the declared plugin contract — the same check, the same refusal, as `ObjectKernel.use()`. A plugin object that `PluginSchema` (`@objectstack/spec`, `kernel/plugin.zod.ts`) refuses is refused at registration on **both** published kernels instead of on one.
|
|
111
|
+
|
|
112
|
+
**BREAKING** accept-set narrowing on a published runtime entry point, shipped as `minor` under the repo's launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`). **A plugin object `LiteKernel` accepted before can be refused now.** Until this release `LiteKernel.use()` wrote the object straight into its registry: `PluginSchema` was run by `PluginLoader.validatePluginContract` only, and `PluginLoader` is reached from `ObjectKernel.use()` alone. So the same plugin was accepted by one kernel and refused by the other — a `type: 'ui'` plugin with no `slug` was refused by `ObjectKernel` with `PLUGIN_CONTRACT_VIOLATION` and mounted a route on `LiteKernel`. `AGENTS.md` names `LiteKernel` for tests, serverless and edge, so the lenient kernel was the one authors develop against and the strict one was production: a plugin could be green in vitest and refused at boot. Maintainer ruling of 2026-09-08 (option A, under the precedent that the two kernels converge rather than diverge): `LiteKernel` validates too.
|
|
113
|
+
|
|
114
|
+
**Exactly what `LiteKernel.use()` newly refuses** is exactly what `ObjectKernel.use()` has refused since the `kernel.use()` enforcement release: all EIGHT declared keys, each refused with the offending key named in the message —
|
|
115
|
+
|
|
116
|
+
- **`id`** — a non-string, or the empty string.
|
|
117
|
+
- **`type`** — any value outside the closed set `standard`, `ui`, `driver`, `server`, `app`, `theme`, `agent`, `objectql`.
|
|
118
|
+
- **`staticPath`** — a non-string.
|
|
119
|
+
- **`slug`** — a non-string, or a string that does not match `/^[a-z0-9-_]+$/`.
|
|
120
|
+
- **`default`** — a non-boolean.
|
|
121
|
+
- **`description`** — a non-string.
|
|
122
|
+
- **`author`** — a non-string.
|
|
123
|
+
- **`homepage`** — a non-string, or a string that is not a URL.
|
|
124
|
+
|
|
125
|
+
**`null` is refused on every one of the eight**, and a `type: 'ui'` plugin missing `staticPath` or `slug` is refused with `PLUGIN_UI_REQUIRED_KEY_MISSING` inside the same envelope.
|
|
126
|
+
|
|
127
|
+
**What a refusal looks like — one refusal, from either kernel.** The check is now one function (`assertPluginContract`, package-internal) that both kernels call, so the code and the message are produced once:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
PLUGIN_CONTRACT_VIOLATION: plugin '@acme/console' is refused by the declared
|
|
131
|
+
plugin contract at 'slug': PLUGIN_UI_REQUIRED_KEY_MISSING: a `type: 'ui'` plugin must declare `slug` — …
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`LiteKernel.use()` is synchronous and throws that error as-is, so the stable code is on the error's `code` property as well as at the head of the message. `ObjectKernel.use()` is unchanged: it still re-wraps a failed load as `Failed to load plugin: <name> - <that message>`, its existing wrapper for every load failure. The text after that prefix is byte-for-byte the `LiteKernel` message for the same input, pinned by test.
|
|
135
|
+
|
|
136
|
+
**What is STILL ACCEPTED on `LiteKernel` — the narrowing stops where `ObjectKernel`'s does.** Unknown keys still pass (`PluginSchema` carries no `.strict()`, and the parse output is discarded, so the stored object is the very object passed in). A version-less plugin still loads, and so do `1.0.0-alpha.1` and `1.0.0+20230101`: `version` is excluded from the schema check on both kernels, and `LiteKernel` — which has never judged `version` — still does not. A plugin declaring no `type` still loads and still stores no `type`. A class-based plugin keeps its identity, its prototype and its prototype methods. And `PluginLoader`'s structural checks (`name`, `init`, semver) stay the loader's own: the convergence is on the schema, not on the loader.
|
|
137
|
+
|
|
138
|
+
**Ordering, stated because it is observable.** `LiteKernel.use()` checks its state first (a kernel past bootstrap still says `Cannot register plugins after bootstrap has started`, never `PLUGIN_CONTRACT_VIOLATION`), then the contract, then registers — so a refused plugin never reaches the registry and cannot supersede an earlier registration under its name.
|
|
139
|
+
|
|
140
|
+
**Blast radius, measured before landing rather than assumed.** Across this repository's suites, 813 `LiteKernel.use()` calls were reachable; 807 were accepted by the schema unchanged and the six refusals came from three test-local fixture objects in two files — zero product or library code. Externally authored plugins registered on `LiteKernel` are the population this reaches, and they are exactly the plugins that would already have been refused by `ObjectKernel` at production boot.
|
|
141
|
+
|
|
142
|
+
**Migration.** There is nothing to rename. A plugin refused on `LiteKernel` now was already refused on `ObjectKernel`; fix the named key: give `type` a value from the closed set (or drop it — an absent `type` reads as `standard`), declare `staticPath` and `slug` on a `type: 'ui'` plugin, spell `slug` in `[a-z0-9-_]`, make `homepage` a URL, and never `null` a declared key. The refusal names the plugin and the first violated key.
|
|
143
|
+
|
|
144
|
+
<!-- adr-0087: not-required (no-migration-prescription) An accept-set narrowing performed entirely at the runtime registration path: `PluginSchema` is READ by `LiteKernel.use()` now, exactly as `ObjectKernel.use()` has read it since the `kernel.use()` enforcement release — the schema itself is not changed. No metadata key, spec symbol, Zod schema, object definition or stored representation is added, removed or given a different name, so `objectstack migrate meta` has nothing to visit and there is no tombstone to mint. Stored metadata is untouched; what moves is which plugin OBJECTS the second kernel accepts, and every object it newly refuses was already refused by the first. The channel that reaches an affected plugin author is the refusal itself, which names the offending key at `use()` and is more precise than a ledger line — and which value a refused key should carry is authoring intent no ledger entry can decide. -->
|
|
145
|
+
- cf9bda4: The kernel's in-memory i18n fallback learns the declared `i18n.fallbackLocale`, so one declaration stops answering two ways (#15694)
|
|
146
|
+
|
|
147
|
+
`i18n.fallbackLocale` is authorable on the stack artifact (`TranslationConfigSchema`), and `FileI18nAdapter` — the provider `I18nServicePlugin` installs — has always honoured it: both boot paths construct it with `fallbackLocale || defaultLocale || 'en'`, and its `t()` consults that locale, per key, after the requested one.
|
|
148
|
+
|
|
149
|
+
The kernel's in-memory fallback is constructed with nothing. `AppPlugin.loadTranslations` injected the declared `defaultLocale` and `supportedLocales` (#7679) into whichever `i18n` service was registered, but never `fallbackLocale`, and the provider had no setter to receive one. On every stack running that fallback — any stack that declares `translations` without `@objectstack/service-i18n` registered (not installed, or `tierEnabled('i18n')` false) — the declaration was inert. A stack declaring `defaultLocale: 'zh-CN'` with `fallbackLocale: 'en'` answered a missing `zh-CN` key from `en` under `I18nServicePlugin` and from `zh-CN`, i.e. not at all, under the fallback: one declaration, two providers, two answers. That the fallback self-declares `degraded` licenses fewer capabilities, not a different answer to the same declared key.
|
|
150
|
+
|
|
151
|
+
What changed:
|
|
152
|
+
|
|
153
|
+
- **`II18nService.setFallbackLocale?(locale)`** — a new OPTIONAL member, the injection counterpart of `getFallbackLocale`. It is the same shape `setDefaultLocale` and `setSupportedLocales` already have, and for the same reason: the declaration lives on the stack artifact, which only the runtime app-plugin layer can see. A provider constructed with its fallback (`FileI18nAdapter`) omits the method and keeps the value it was built with.
|
|
154
|
+
- **`createMemoryI18n` receives it and acts on it.** `t()` now consults the declared fallback per KEY after the requested locale — the same second leg `FileI18nAdapter.t()` has. Per key, not per bundle: the pre-existing `resolveTranslations(locale) ?? mergedLocale(defaultLocale)` line swaps whole bundles and only when the requested locale has none, so a `zh-CN` bundle that simply lacked the key never reached anything else. That older leg is unchanged.
|
|
155
|
+
- **`AppPlugin.loadTranslations` threads the declaration**, through the same `typeof … === 'function'` optional-capability probe as `setDefaultLocale`, and guarded on the app having declared something — several `AppPlugin`s can share one kernel, and an app that declares no `i18n` block must not clear a fallback another app declared.
|
|
156
|
+
|
|
157
|
+
A stack that declares no `fallbackLocale` gets exactly the behaviour it has today: the setter is never called, and `t()` walks the same chain it always did. A fallback nobody asked for would be a new chain, not a fix.
|
|
158
|
+
|
|
159
|
+
`getFallbackLocale()` is deliberately still absent from the memory fallback. The setter is what the provider is TOLD; the accessor is what the serving layer ASKS it when building the metadata-document translators' fallback chain (#14882). Answering the second from `defaultLocale` — the only value always available there — would settle the default-locale contract question #14882 leaves deliberately open, from a degraded provider. Those reads keep the resolvers' own default, which is known and intentional.
|
|
160
|
+
- 2a3decc: `PluginSchema` now REQUIRES `staticPath` and `slug` when `type` is `'ui'`, and core's `Plugin` interface inherits every `PluginSchema` key from `PluginDefinition` instead of restating two of them.
|
|
161
|
+
|
|
162
|
+
**BREAKING** accept-set narrowing on a published schema, shipped as `minor` under the repo's launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`). `packages/spec/src/kernel/plugin.zod.ts` described `staticPath` and `slug` as *"Required for type=\"ui\""* while declaring both `.optional()`, with nothing behind the prose; since `kernel.use()` runs the schema on the boot path (#16049), that was a promise the runtime visibly did not keep. This is the spec half of #16049, split by director ruling (decision batch #58, 2026-09-06).
|
|
163
|
+
|
|
164
|
+
**Exactly what is newly refused.** A plugin object with `type: 'ui'` that omits `staticPath`, omits `slug`, or spells either as `undefined`. Nothing else: every other declared type (`standard`, `driver`, `server`, `app`, `theme`, `agent`, `objectql`), and a plugin declaring no `type` at all, still parses with neither key. A PRESENT value is judged exactly as before — `slug` keeps its `/^[a-z0-9-_]+$/` regex, `staticPath` stays any string, and the empty string is not refused by this change.
|
|
165
|
+
|
|
166
|
+
**What a refusal looks like.** One zod issue per missing key, `path` naming the key, the new stable code `PLUGIN_UI_REQUIRED_KEY_MISSING` (exported from `@objectstack/spec/kernel`) at the head of the issue `message` and on the issue's `params.code`. At `kernel.use()` it rides the existing `PLUGIN_CONTRACT_VIOLATION` envelope unchanged, because the loader surfaces the first issue's `path` and `message` and reads nothing else:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
PLUGIN_CONTRACT_VIOLATION: plugin '@acme/console' is refused by the declared
|
|
170
|
+
plugin contract at 'staticPath': PLUGIN_UI_REQUIRED_KEY_MISSING: a `type: 'ui'`
|
|
171
|
+
plugin must declare `staticPath` — the absolute path of the static assets it
|
|
172
|
+
serves. Declare it, or drop `type: 'ui'` if this plugin serves no assets.
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**The fix for an affected plugin** is the one the message names: declare both keys (`staticPath`: the absolute path of the assets it serves; `slug`: the URL segment it is mounted under), or drop `type: 'ui'` if the plugin serves no assets. There is no fallback to lean on: the Hono server's `slug || name.split('/').pop()` derivation is no longer reachable through the kernel, because the object is refused before it is stored.
|
|
176
|
+
|
|
177
|
+
**`@objectstack/core` — `Plugin` derives its metadata keys.** `Plugin` now `extends PluginDefinition` (`z.input<typeof PluginSchema>`), so `id`, `type`, `staticPath`, `slug`, `default`, `version`, `description`, `author` and `homepage` are ONE declaration shared with the schema the kernel enforces. Additive for every existing implementer: `type` and `version` keep the shapes they had (`type` is still `PluginType | undefined`, pinned type-equal in `packages/rest`; `version` still `string | undefined`), and the seven other keys are new optional members. A `ui` plugin can now carry `staticPath` / `slug` without widening its own type. Runtime-only members (`name`, `dependencies`, `optionalDependencies`, `requiresServices`, `providesServices`, `init`, `start`, `destroy`) stay declared on the interface.
|
|
178
|
+
|
|
179
|
+
**Blast radius, measured.** No in-repo plugin object outside test fixtures declares `type: 'ui'` (searched `packages/`, `apps/`, `examples/` non-dist sources for a `type` key or class field holding the literal `'ui'`: three test files, nothing shipped), so no in-repo composition changes behaviour. Externally authored `ui` plugins that relied on the slug derivation, or declared no assets, are the population this reaches — and they are refused at boot, by name, with the key to add.
|
|
180
|
+
|
|
181
|
+
<!-- adr-0087: not-required (no-migration-prescription) An accept-set narrowing on plugin OBJECTS, which are never stored metadata: `PluginSchema` gains a refinement and one exported constant; no metadata key, object definition or stored representation is added, removed or renamed, so `objectstack migrate meta` has nothing to visit and there is no tombstone to mint. The channel that reaches an affected plugin author is the refusal itself, which names the missing key at `kernel.use()`; which value that key should carry is authoring intent no ledger entry can decide. -->
|
|
182
|
+
- cc00df2: feat(core)!: retire `PluginSecurityScanner` — plugin security scanning is not a platform capability (#14919)
|
|
183
|
+
|
|
184
|
+
<!-- adr-0087: registered plugin-security-scanner-retired -->
|
|
185
|
+
|
|
186
|
+
**ADR-0087 disposition: registered**, as `plugin-security-scanner-retired` in
|
|
187
|
+
`MIGRATIONS_BY_MAJOR[18].semantic` — a **D3 semantic** entry, not a D2 conversion,
|
|
188
|
+
and so not the metadata migration the ruling excludes. The class has no spec schema
|
|
189
|
+
and never had one, so there is no authorable key to tombstone with `retiredKey()`
|
|
190
|
+
and no stored `sys_metadata` row a conversion could rewrite: a scanner was
|
|
191
|
+
constructed per call and every result lived in a per-instance Map discarded with the
|
|
192
|
+
object, so `applyConversionsToStoredItem` has no seam that would ever see one. An
|
|
193
|
+
entry is nevertheless owed rather than optional, because this changeset carries a
|
|
194
|
+
real consumer prescription — the enforced channel is tsc at the import site, and for
|
|
195
|
+
any consumer it does not reach, the ledger and the generated upgrade guide are the
|
|
196
|
+
only channel there is. Same disposition as `contracts.IDataDriver.findStream` and
|
|
197
|
+
`actor-user-roles-to-positions`.
|
|
198
|
+
|
|
199
|
+
**BREAKING** — `PluginSecurityScanner` is removed from `@objectstack/core`,
|
|
200
|
+
together with its two companion types `ScanTarget` and `SecurityIssue`. Landing
|
|
201
|
+
as `minor` under the repo's launch-window convention for breaking changes.
|
|
202
|
+
**There is no replacement**, and none is planned.
|
|
203
|
+
|
|
204
|
+
⚠️ **The out-of-repo consumer population for these three exports is NOT
|
|
205
|
+
MEASURED.** This changeset can state only what was measured *inside* the
|
|
206
|
+
sources this repo can read: zero constructors in objectstack, zero in objectui
|
|
207
|
+
at the pinned sha, and zero in the deleted example itself. How many published
|
|
208
|
+
consumers of `@objectstack/core` import the class is unknown — no download,
|
|
209
|
+
dependent or source telemetry was consulted. Read the removal as breaking for
|
|
210
|
+
an unmeasured population, not as a removal proven to break nobody.
|
|
211
|
+
|
|
212
|
+
## Why it was removed rather than repaired
|
|
213
|
+
|
|
214
|
+
The class was a shell that reported success. `scan()` composed five private
|
|
215
|
+
scanners: four of them (`scanCode`, `scanMalware`, `scanLicenses`,
|
|
216
|
+
`scanConfiguration`) allocated an empty issue array, logged, and returned it
|
|
217
|
+
with no code in between — none could report a finding for any input. The fifth,
|
|
218
|
+
`scanDependencies`, ran a real loop but matched only against an in-memory
|
|
219
|
+
vulnerability database whose sole writer, the public `addVulnerability`, had
|
|
220
|
+
zero callers; `updateVulnerabilityDatabase()` logged twice and fetched nothing.
|
|
221
|
+
The database was therefore empty on every code path that has ever executed, so
|
|
222
|
+
no issue was ever produced, the score stayed 100, and the result was
|
|
223
|
+
`status: 'passed'` for every plugin the scanner was ever handed — a malicious
|
|
224
|
+
one included.
|
|
225
|
+
|
|
226
|
+
A security control that cannot fail is worse than no security control, because
|
|
227
|
+
callers rely on it. Repair — writing a real vulnerability scanner — was refused
|
|
228
|
+
by name: it is a feature with a design surface and no demand, not a defect fix.
|
|
229
|
+
|
|
230
|
+
## FROM → TO
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
// FROM — compiles today, and passes every plugin it is given
|
|
234
|
+
import { PluginSecurityScanner } from '@objectstack/core';
|
|
235
|
+
|
|
236
|
+
const scanner = new PluginSecurityScanner(kernel.logger);
|
|
237
|
+
const result = await scanner.scan({ pluginId, version, dependencies });
|
|
238
|
+
if (result.status === 'passed') { await kernel.use(plugin); }
|
|
239
|
+
|
|
240
|
+
// TO — delete it. The condition above was always true.
|
|
241
|
+
await kernel.use(plugin);
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**The one-line fix:** delete the import and every call; no symbol replaces it.
|
|
245
|
+
If your code branched on `result.status`, take the `'passed'` branch — that is
|
|
246
|
+
the only branch it ever took.
|
|
247
|
+
|
|
248
|
+
**If you were relying on it for actual security**, you were not getting any.
|
|
249
|
+
Audit dependencies with the tools built for it (`npm audit` / `pnpm audit`,
|
|
250
|
+
Dependabot, the GitHub Advisory Database, OSV) and treat an unaudited
|
|
251
|
+
third-party plugin as untrusted code. What ObjectStack does still enforce is
|
|
252
|
+
artifact **integrity and signatures** (`verifyPluginArtifactIntegrity`, the
|
|
253
|
+
plugin signature verifier — "is this what the publisher signed?", never "is
|
|
254
|
+
this safe?"), explicit plugin **permissions**, and the sandbox **resource
|
|
255
|
+
limits**; all three are unchanged.
|
|
256
|
+
|
|
257
|
+
Removed under ADR-0049 enforce-or-remove, per the maintainer ruling of
|
|
258
|
+
2026-09-05 (director summon #14, decision batch #42). The retirement is pinned
|
|
259
|
+
as an export-list assertion on both barrels in
|
|
260
|
+
`packages/core/src/security/security-scanner-retirement.pin.test.ts`.
|
|
261
|
+
|
|
262
|
+
### Patch Changes
|
|
263
|
+
|
|
264
|
+
- 6f94458: fix(core): narrow the operation-private-keys pin's scanner to `.ts`, so it judges exactly the population turbo re-runs it for (#15090)
|
|
265
|
+
|
|
266
|
+
`packages/core/src/security/operation-private-keys.pin.test.ts` filtered its
|
|
267
|
+
candidate set with `/\.tsx?$/` — `.ts` **and** `.tsx` — while this package's
|
|
268
|
+
declared radius in the cross-package declaration table is a `packages/**`
|
|
269
|
+
subtree glob ending in `.ts`. So the pin judged a population **strictly wider**
|
|
270
|
+
than the one either scoping layer of `check:cross-package-test-inputs` knows
|
|
271
|
+
about: Layer A never unions this package into the test shard when a `.tsx` file
|
|
272
|
+
changes, and Layer B never moves the `test` task's cache hash for one. A `.tsx`
|
|
273
|
+
file under `packages/` declaring its own `OPERATION_PRIVATE_KEY_PREFIX` or
|
|
274
|
+
`withoutOperationPrivateKeys` was therefore scanned by the pin and invisible to
|
|
275
|
+
CI's scoping — landing on `main` with every PR green and then reddening whichever
|
|
276
|
+
unrelated PR next touched a `.ts` file. That is the #7802 shape the declaration
|
|
277
|
+
table exists to close, one extension wide.
|
|
278
|
+
|
|
279
|
+
Repaired by narrowing the **scanner**, not by widening the **glob** — and that
|
|
280
|
+
asymmetry is measured rather than assumed. On `b548e438d`, adding a `.tsx` glob
|
|
281
|
+
to this package's roster entry and re-deriving `check:cross-package-test-inputs`'
|
|
282
|
+
watch hints flips the dispatch-gates self-test case *"nor a .tsx test file inside
|
|
283
|
+
it"* from true to false, with the added glob itself as the covering hint. That
|
|
284
|
+
case is a live specimen for "a test class the hint route cannot reach", so the
|
|
285
|
+
red is real and re-pointing it is a decision in another lane, not a fixup.
|
|
286
|
+
|
|
287
|
+
What the boundary costs, measured on the pin's own surface (tracked **plus**
|
|
288
|
+
untracked, ignored paths excluded) at `b548e438d`: **5408** `.ts` files scanned,
|
|
289
|
+
8 of them mentioning a guarded symbol; **8** `.tsx` files excluded, **0** of them
|
|
290
|
+
mentioning either symbol. The loss is empty today — and that reading is no longer
|
|
291
|
+
transcribed and trusted. A new case re-measures it on every run: it asserts the
|
|
292
|
+
excluded `.tsx` population is non-empty (so the boundary is an exclusion and not
|
|
293
|
+
an empty tree describing itself), that the filter really drops those files, and
|
|
294
|
+
that none of them declares either symbol. Ablation, with the restore proven by
|
|
295
|
+
blob hash rather than by exit code: re-widening the scanner reddens it while the
|
|
296
|
+
offender assertion stays green — which is precisely the failure mode, since a
|
|
297
|
+
wider scanner reads as coverage CI never runs — and planting a `.tsx`
|
|
298
|
+
redeclaration reddens it with a message that says the choice is a second-gate
|
|
299
|
+
trade, not a one-line widening.
|
|
300
|
+
|
|
301
|
+
The correspondence between scanner and glob is now stated at **both** ends: the
|
|
302
|
+
pin's header and the declaration table's entry for this package. No published
|
|
303
|
+
surface moves — the only source file edited is a test.
|
|
304
|
+
- 6e67b86: refactor(core): the authz context's time-zone probe is now the shared value-domain predicate, not a third copy of it
|
|
305
|
+
|
|
306
|
+
`resolve-authz-context.ts` carried a module-private `isValidTimeZone` — the
|
|
307
|
+
`Intl.DateTimeFormat` probe, re-stated. It was the third copy of one
|
|
308
|
+
definition, alongside `@objectstack/spec/shared`'s `isValueDomainMember` and
|
|
309
|
+
`service-settings`' own re-statement. `coerceTimeZone` now calls
|
|
310
|
+
`isValueDomainMember('iana_time_zone', …)` and the copy is gone.
|
|
311
|
+
|
|
312
|
+
**No behavioural change, measured rather than asserted.** The two predicates
|
|
313
|
+
were run over a shared 4,058-input corpus — the zones
|
|
314
|
+
`Intl.supportedValuesOf('timeZone')` omits (`UTC`, `Asia/Kolkata`,
|
|
315
|
+
`Europe/Kyiv`, `Asia/Ho_Chi_Minh`, `US/Eastern`, `GMT`), every member of that
|
|
316
|
+
enumeration plus its case- and space-padded variants, refusals, `Etc/` and
|
|
317
|
+
offset spellings, legacy aliases, and fuzz — with **zero disagreements**, and
|
|
318
|
+
the same zero at the `coerceTimeZone` level. The call site's own
|
|
319
|
+
pre-processing (trim, stringify a non-string, refuse blank) is unchanged.
|
|
320
|
+
|
|
321
|
+
What this buys is drift resistance, not a fix: core's time-zone acceptance now
|
|
322
|
+
sits under the shared pins, so a future "modernisation" to
|
|
323
|
+
`Intl.supportedValuesOf('timeZone')` — which would silently narrow what the
|
|
324
|
+
authz context accepts, since that enumeration omits this platform's own
|
|
325
|
+
default `UTC` — turns a test red instead of shipping.
|
|
326
|
+
- e9fcd6b: feat(spec)!: the fourteen `kernel/` duration keys carry their unit in the key name (#15678, ruling B on #14478)
|
|
327
|
+
|
|
328
|
+
<!-- adr-0087: registered kernel-event-bus-retention-unit-in-key, kernel-package-lifecycle-durations-unit-in-key, kernel-plugin-health-report-durations-unit-in-key, kernel-plugin-security-durations-unit-in-key, kernel-startup-orchestrator-durations-unit-in-key -->
|
|
329
|
+
|
|
330
|
+
**BREAKING** — fourteen published `kernel/` duration keys are renamed and
|
|
331
|
+
tombstoned. Shipped as `minor` under the repo's launch-window convention for
|
|
332
|
+
breaking changes; the hand-migration prescriptions are registered under protocol
|
|
333
|
+
major 18. Maintainer ruling B on #14478 (2026-09-02, decision batch #43,
|
|
334
|
+
「同意」).
|
|
335
|
+
|
|
336
|
+
`check:duration-unit-keys` makes a duration-shaped `z.number()` carry its unit
|
|
337
|
+
in the key NAME, never only in its `.describe()` prose, and grandfathers no
|
|
338
|
+
existing offender. Stack card 1/6 (#15676) landed the rule's two structural
|
|
339
|
+
exemptions and card 2/6 (#15677) cleared `api/`; this card clears `kernel/`.
|
|
340
|
+
Measured with the gate itself: `src/kernel/**` goes from 14 offenders to **0**,
|
|
341
|
+
and the whole-tree count falls **36 → 22**.
|
|
342
|
+
|
|
343
|
+
## FROM → TO
|
|
344
|
+
|
|
345
|
+
| key | replacement | unit |
|
|
346
|
+
|:--|:--|:--|
|
|
347
|
+
| `EventPersistence.retention` | `retentionDays` | days |
|
|
348
|
+
| `EventSourcingConfig.retention` | `retentionDays` | days |
|
|
349
|
+
| `UpgradePlan.estimatedDuration` | `estimatedDurationSeconds` | seconds |
|
|
350
|
+
| `PluginHealthReport.metrics.uptime` | `uptimeMs` | milliseconds |
|
|
351
|
+
| `PluginHealthReport.metrics.responseTime` | `responseTimeMs` | milliseconds |
|
|
352
|
+
| `SandboxConfig.process.timeout` | `timeoutMs` | milliseconds |
|
|
353
|
+
| `KernelSecurityPolicy.authentication.tokenExpiration` | `tokenExpirationSeconds` | seconds |
|
|
354
|
+
| `KernelSecurityPolicy.auditLog.retention` | `retentionDays` | days |
|
|
355
|
+
| `PluginSecurityManifest.vulnerabilityDisclosure.responseTime` | `responseTimeHours` | hours |
|
|
356
|
+
| `PackageDependencyResolutionResult.resolvedIn` | `resolvedInMs` | milliseconds |
|
|
357
|
+
| `MultiVersionSupport.rollout.duration` | `durationMs` | milliseconds |
|
|
358
|
+
| `StartupOptions.timeout` | `timeoutMs` | milliseconds |
|
|
359
|
+
| `PluginStartupResult.duration` | `durationMs` | milliseconds |
|
|
360
|
+
| `StartupOrchestrationResult.totalDuration` | `totalDurationMs` | milliseconds |
|
|
361
|
+
|
|
362
|
+
**Every value is unchanged** — only key names move, and every default moves with
|
|
363
|
+
its key (`StartupOptions` still defaults to 30000, `EventSourcingConfig` to
|
|
364
|
+
365). Every old spelling is a `retiredKey()` tombstone, so it fails `tsc` at the
|
|
365
|
+
authoring site (input type `never`) and fails the parse with the rename
|
|
366
|
+
prescription rather than a bare unrecognized-key error.
|
|
367
|
+
|
|
368
|
+
## ⚠️ Two collisions this rename removes — check these by hand, not by search-and-replace
|
|
369
|
+
|
|
370
|
+
**`responseTime` meant two different units on two kernel shapes.** On
|
|
371
|
+
`PluginSecurityManifest.vulnerabilityDisclosure` it is HOURS (how fast a
|
|
372
|
+
publisher promises to answer a vulnerability report); on
|
|
373
|
+
`PluginHealthReport.metrics` the identical bare name is MILLISECONDS. So
|
|
374
|
+
`responseTime: 24` was a day on one shape and a fortieth of a second on the
|
|
375
|
+
other, with nothing at the authoring site to tell them apart. They land on
|
|
376
|
+
`responseTimeHours` and `responseTimeMs` respectively — do not let one
|
|
377
|
+
find-and-replace rewrite both.
|
|
378
|
+
|
|
379
|
+
**`uptime` is milliseconds here and SECONDS on `GET /health`.** That collision
|
|
380
|
+
was already costing prose: the protocol lifecycle page carried a standing
|
|
381
|
+
paragraph whose only job was telling the two apart. `metrics.uptime` becomes
|
|
382
|
+
`metrics.uptimeMs`; the seconds-valued `uptime` of the HTTP health body is a
|
|
383
|
+
separate, unchanged surface and must not be renamed with it.
|
|
384
|
+
|
|
385
|
+
A third split worth reading before you migrate: `estimatedDurationSeconds: 120`
|
|
386
|
+
is two MINUTES while `durationMs: 3600000` is one HOUR. Three adjacent
|
|
387
|
+
measurements of the same package install carried two different units, and no
|
|
388
|
+
parse can catch a value moved between them — both bounds accept any
|
|
389
|
+
non-negative integer.
|
|
390
|
+
|
|
391
|
+
## Dispositions — five semantic entries, no D2 conversion
|
|
392
|
+
|
|
393
|
+
Justified per key rather than defaulted, and this card's answer is uniform:
|
|
394
|
+
**none of the fourteen gets an ADR-0087 D2 conversion.** A D2 conversion runs
|
|
395
|
+
over a stack document, and `stack.zod.ts` declares no `eventBus`, `startup`,
|
|
396
|
+
`upgrade` or plugin-security root — none of these twelve defs is a stack
|
|
397
|
+
collection member or a registered metadata kind stored as a `sys_metadata` row,
|
|
398
|
+
so the conversion chain has no seam that would see one. They are host
|
|
399
|
+
construction arguments (`EventBusConfig`, `StartupOptions`, `SandboxConfig`,
|
|
400
|
+
`MultiVersionSupport`), package artifacts (`PluginSecurityManifest`) and
|
|
401
|
+
runtime-emitted measurements (`PluginHealthReport`, `PluginStartupResult`,
|
|
402
|
+
`StartupOrchestrationResult`, `UpgradePlan`,
|
|
403
|
+
`PackageDependencyResolutionResult`). Each therefore carries a **semantic**
|
|
404
|
+
entry, which is the disposition `kernel/HealthStatus:timestamp` already holds on
|
|
405
|
+
one of these very files (`epoch-instant-keys-renamed`, card 1/6) and what ruling
|
|
406
|
+
B prescribes for a key that is not authorable metadata. All fourteen are
|
|
407
|
+
registered by exact key in `RETIRED_KEYS_BY_MAJOR`.
|
|
408
|
+
|
|
409
|
+
## Keys deliberately left alone
|
|
410
|
+
|
|
411
|
+
`EventSourcingConfig.snapshotRetention` is a COUNT of snapshots and
|
|
412
|
+
`MultiVersionSupport.rollout.percentage` is a proportion — neither is a
|
|
413
|
+
duration, so neither has a unit to carry and both keep their names.
|
|
414
|
+
`RuntimeConfig.resourceLimits.timeout` names its unit only in the JSDoc above
|
|
415
|
+
the key ("Execution timeout in milliseconds"), a channel
|
|
416
|
+
`check:duration-unit-keys` does not read: it reads `.describe()` and
|
|
417
|
+
`.meta({ description })`, and this key's describe ("Maximum execution time")
|
|
418
|
+
names none. The gate therefore lists it among the duration-shaped keys but
|
|
419
|
+
deliberately does not judge it — neither an offender nor an exemption — so it is
|
|
420
|
+
outside this rename; that JSDoc-channel gap is filed as #15939. A pin test
|
|
421
|
+
asserts the key still parses bare, so a later sweep cannot read the four
|
|
422
|
+
security renames as "every timeout on that file".
|
|
423
|
+
|
|
424
|
+
## Readers moved in the same PR, at the same magnitude
|
|
425
|
+
|
|
426
|
+
`@objectstack/core`'s health monitor (`metrics.uptimeMs: Date.now() -
|
|
427
|
+
startTime`), the kernel and contracts test suites, and the hand-written
|
|
428
|
+
`content/docs/protocol/kernel/lifecycle.mdx`, whose `uptime` paragraph now
|
|
429
|
+
states the collision the rename removes.
|
|
430
|
+
|
|
431
|
+
⚠️ `packages/core/src/plugin-loader.ts` declares its OWN local
|
|
432
|
+
`PluginStartupResult` interface — a different type, carrying `startTime` rather
|
|
433
|
+
than any duration key. It is not a reader of this schema, it is untouched by
|
|
434
|
+
this rename, and the divergence between the two shapes is tracked separately.
|
|
435
|
+
- c78c918: Documentation: the manifest surface no longer describes itself as an open object.
|
|
436
|
+
|
|
437
|
+
`ManifestSchema` became a `strictObject` when the manifest surface was closed against unknown keys, but five prose sites still described the earlier posture. They shipped, so an author (or an AI writing metadata) reading the declarations was told the manifest tolerates undeclared keys — while the runtime rejects them by name and offers the declared spelling for a near miss. Prose that contradicts a tightened contract teaches exactly the wrong reflex, so each site now states the current refusal rather than merely dropping the old claim:
|
|
438
|
+
|
|
439
|
+
- `AssembledPackageBodySchema`'s docblock no longer explains its lack of a `strictObject` spelling by calling `ManifestSchema` open. The posture is inherited: the schema is `ManifestSchema.extend(...)`, and `.extend()` carries the base's unknown-key handling, so an undeclared key on an assembled body is refused — measured, with the rename suggestion intact.
|
|
440
|
+
- The artifact-registration seam kept the half of its reasoning that still holds (the schema applies defaults, so a parsed clone would not be byte-identical) and retired the half that does not ("Zod strips undeclared keys") — the key is now refused at that parse rather than dropped from the clone.
|
|
441
|
+
- The `os compile` per-package rule pass explains why a body may be re-read as its own manifest: nothing parses that superset, and against `ManifestSchema` it would now be refused.
|
|
442
|
+
|
|
443
|
+
No schema, behaviour or export changed; `check:api-surface` and the generated reference pages are unmoved.
|
|
444
|
+
- 4771bd9: The `Server is ready` line now reports the degraded boot it is standing on, instead of printing a green `✓` over it.
|
|
445
|
+
|
|
446
|
+
`✓ Server is ready` and the kernel's `System started with degraded capabilities. Missing core services: …` were two statements about one boot, produced by two packages — the banner in `@objectstack/cli`, the conclusion in `@objectstack/core` — with **no data path between them**. So the ready signal did not depend on the thing that broke, and therefore could not report it. Measured twice within a day, from unrelated causes: an objectui CI boot where the auth plugin failed and not one `sys_*` table existed, and this repo's own weekly registry canary on the published `npx create-objectstack@latest` on-ramp, where the tick printed directly **above** four boot warnings. In the second case the ready line carried no weight in the job's verdict at all — it was present, green, wrong, and believed by nobody.
|
|
447
|
+
|
|
448
|
+
- **The data path.** `ObjectKernel.validateSystemRequirements()` now publishes the list it had already computed — the same array behind its own warning — on the kernel's service registry, which is the seam boot facts already cross to reach the banner (`serve` reads `auth` and `seed-summary` off it the same way). No member and no type is added to `@objectstack/core`'s public surface, and nothing re-derives which services count as `core`: that judgement stays in `ServiceRequirementDef` alone.
|
|
449
|
+
- **The line.** On a degraded boot the banner prints `⚠ Server is ready — DEGRADED: missing core services: <names>`, naming exactly what the kernel found missing. On a healthy boot the ready block is byte-for-byte unchanged, so an ordinary boot's output does not move.
|
|
450
|
+
- **Readiness is NOT made strict.** Nothing about what boots, binds, or exits changes. A machine deliberately running without auth still starts, still prints ready, and still exits 0 — the line just says what state it is ready in.
|
|
451
|
+
- d4f9b2a: A session whose active organization is no longer one the user belongs to now resolves with no active organization instead of that one's data.
|
|
452
|
+
|
|
453
|
+
Under a wall-enforcing tenancy posture (`isolated` / `group`), `resolveAuthzContext` took a browser session's stored `activeOrganizationId` as the request tenant without ever comparing it to the user's current memberships — the framework's only such comparison was gated on an API-key principal. A session whose owner had been removed from an organization therefore kept reading that organization's rows and writing into it until the session expired on its own (7 days by default), including when the removal went through the product's own offboarding path.
|
|
454
|
+
|
|
455
|
+
That claim is now vetted: if it is not in the caller's `accessible_org_ids`, it is dropped and the context resolves with no active organization at all, which the tenant wall already fails closed on (reads resolve to nothing; a tenant-scoped write is refused by ADR-0123 D2). The principal is **not** refused — a session is a person who may hold memberships elsewhere, so they stay signed in and can switch to an organization they are actually in. The API-key arm is unchanged: a key is its organization binding and is still refused outright. The wire is unchanged; the drop is reported to the operator as a single server-side `warn`.
|
|
456
|
+
- a727043: fix(rest,core): an organization-less or ex-member API key on a walled single-kernel deployment now answers 401 where it answered 200
|
|
457
|
+
|
|
458
|
+
Under a wall-enforcing tenancy posture (`isolated`), an API key stamped with an
|
|
459
|
+
organization its owner is no longer a member of **read and wrote that
|
|
460
|
+
organization's rows** on the wiring the open core actually builds. Not a silent
|
|
461
|
+
empty set — a GET that returned the other organization's records, and a POST
|
|
462
|
+
that landed a row read back from the store carrying that organization's id and
|
|
463
|
+
the ex-member as its creator. An organization-less key on the same deployment
|
|
464
|
+
read `200` with an empty set, which is the silent failure the wall exists to
|
|
465
|
+
replace.
|
|
466
|
+
|
|
467
|
+
The cause was a seam, not a predicate. `RestServer.computeExecCtx` derived the
|
|
468
|
+
effective tenancy posture from a per-request kernel, and on the single-kernel
|
|
469
|
+
wiring there is no per-request kernel — so the posture was `undefined` on every
|
|
470
|
+
request, and both posture-conditional API-key refusals are gated on it:
|
|
471
|
+
`organization_required` in `api-key.ts` and `organization_membership_ended` in
|
|
472
|
+
`resolve-authz-context.ts`. Neither ever ran. The Layer 0 wall itself was
|
|
473
|
+
active the whole time; it compares against the caller's active organization,
|
|
474
|
+
and an API key's tenant is `sys_api_key.active_organization_id` copied verbatim
|
|
475
|
+
— the holder's own stored claim. Enforcing the wall is what let the ex-member
|
|
476
|
+
through, because the one fact that would expose the ended membership was not an
|
|
477
|
+
input to the layer that could act on it.
|
|
478
|
+
|
|
479
|
+
The single-kernel branch now derives the posture from a provider `rest-api-plugin`
|
|
480
|
+
wires to the lone local kernel's `tenancy` service, in the same shape as the
|
|
481
|
+
auth-service provider beside it. A host that registers no `tenancy` service is
|
|
482
|
+
unchanged and still admits: there is no wall on such a deployment, so there is
|
|
483
|
+
nothing for an organization-less key to be walled out of. A `tenancy` service
|
|
484
|
+
that was registered and **failed to build** is an outage and answers `503`, not
|
|
485
|
+
an admission — a posture that could not be read is not a posture that is absent.
|
|
486
|
+
|
|
487
|
+
Refusals are now also said out loud on the server side, at `warn`, where each
|
|
488
|
+
one is decided: the key's row id (never the credential or its hash), the
|
|
489
|
+
principal, the organization and the reason. **The wire is unchanged** — both
|
|
490
|
+
refusals still answer the generic `401 UNAUTHENTICATED` with no reason in the
|
|
491
|
+
body, so a holder of someone else's key learns nothing a plain 401 does not
|
|
492
|
+
already tell them. The operator, who previously had a key that was neither
|
|
493
|
+
revoked nor expired and a 401 that said nothing, now has a line to find.
|
|
494
|
+
|
|
495
|
+
Behaviour that does not move: a current member's key on the same route still
|
|
496
|
+
returns its rows and still writes; a request with no credential still answers
|
|
497
|
+
401; and an unknown, revoked or expired key is not a refusal at all, so a key
|
|
498
|
+
scanner produces no log volume.
|
|
499
|
+
- c5d6803: Published `.js.map` files no longer embed the complete original source text (`sourcesContent`) — comments included. `sourcemap: true` was esbuild shorthand, and esbuild's own default for `sourcesContent` is `true`; nobody had decided to publish every package's full source (including `@internal`/test-only comments) to npm inside its source maps, it fell out of a default nobody had looked at. Measured before this change: 55 of 57 publishable packages shipped embedded source text, and maps were roughly half of `@objectstack/spec`'s published bytes.
|
|
500
|
+
|
|
501
|
+
`sourcesContent: false` is now set at one shared place (`scripts/tsup-drop-sources-content.mjs`, wired into every `tsup.config.ts` via tsup's `esbuildOptions` hook — most packages build through the repo-root config directly and pick this up with no config change of their own). `mappings` are untouched, so stack-trace positions still resolve correctly to the original file/line/column; only the embedded source text is gone.
|
|
502
|
+
|
|
503
|
+
`@objectstack/cli` (built with `tsc`, not `tsup`) never embedded source text to begin with — its maps' `sources` entries point at `src/**` paths that are not part of the published tarball either way. That is not a defect unique to `cli`: every `tsup`-built package's `sources` entries are `../src/**`-relative paths that are equally outside `files: ["dist", …]`, and were merely masked by the embedded content that just stopped shipping. Shipping `src/**` in `files[]` to make `sources` resolve was rejected — it would put most of the removed bytes straight back. So `cli`'s maps are left exactly as `tsc` emits them: this is now the fleet-consistent shape (accurate `mappings`, non-resolving-but-honest `sources` labels, no embedded text), not an outlier.
|
|
504
|
+
|
|
505
|
+
A new gate, `pnpm check:sourcemap-no-sources-content`, sweeps every built, non-private package's `dist/**/*.map` and fails if any of them carries a non-empty `sourcesContent` array — so a future `tsup.config.ts` that skips the shared hook, or a toolchain upgrade that changes esbuild's default back, is caught rather than silently re-publishing source text.
|
|
506
|
+
- f89812e: Five source comments in `@objectstack/cli` and `@objectstack/core` stop attributing unpack-time `manifest.integrity` re-verification to the cloud control plane and name the owner this repo has already ruled: the **future runtime loader** (ADR-0025 §3.5 steps 4–7). The enforce leg stays tracked on #11331.
|
|
507
|
+
|
|
508
|
+
`packages/spec`'s `manifest.zod.ts` was corrected to that owner in an earlier change, and these five sites were left behind — so the repo stated both things at once. A comment that names the wrong owner costs nobody a build, but it teaches a reader (and a reading AI) to expect a verification that no component performs and that ADR-0025's own status line records as unimplemented.
|
|
509
|
+
|
|
510
|
+
- `packages/cli/src/utils/osplugin.ts` — the `.osplugin` packaging docblock, and the `sriDigest` TSDoc.
|
|
511
|
+
- `packages/cli/src/commands/plugin/publish.ts` — the integrity-preflight comment.
|
|
512
|
+
- `packages/core/src/security/index.ts` — the `verifyIntegrity` export comment.
|
|
513
|
+
- `packages/core/src/security/plugin-artifact-integrity.ts` — the verifier's own module docblock, which had explained the module's byte-for-byte portability *by* the wrong owner. It now explains it by the leg itself: the module stays portable to whatever runs unpack-time re-verification.
|
|
514
|
+
|
|
515
|
+
**What does NOT change.** The other half of every one of these comments — the digest map is computed by `os plugin build` and self-checked by the `os plugin publish` preflight — is true and is kept verbatim. No accept set, export, signature or runtime behaviour moves; the diff is comment prose only.
|
|
516
|
+
|
|
517
|
+
**What moves for consumers, measured on the built output.** `@objectstack/cli` ships `dist/`, and the `sriDigest` TSDoc rides into `dist/utils/osplugin.d.ts`, so an editor's hover on `sriDigest` stops naming the control plane. `@objectstack/core`'s two sites do **not** reach its published bundle — a module docblock and a line comment above an `export {}` are both dropped from `dist/index.d.ts` — so nothing in that package's shipped bytes moves. It is declared here anyway because the pre-correction attribution is quoted in `packages/core/CHANGELOG.md`, a generated record that may not be hand-edited; a changeset naming the package is the only way the correction reaches that published record.
|
|
518
|
+
- Updated dependencies [fe0d9a4]
|
|
519
|
+
- Updated dependencies [ecd2158]
|
|
520
|
+
- Updated dependencies [f2b5e46]
|
|
521
|
+
- Updated dependencies [ed7243d]
|
|
522
|
+
- Updated dependencies [6ba0db4]
|
|
523
|
+
- Updated dependencies [625b0c3]
|
|
524
|
+
- Updated dependencies [233222e]
|
|
525
|
+
- Updated dependencies [07f40e5]
|
|
526
|
+
- Updated dependencies [ceb4877]
|
|
527
|
+
- Updated dependencies [e9fcd6b]
|
|
528
|
+
- Updated dependencies [90e7e6d]
|
|
529
|
+
- Updated dependencies [2bdabe6]
|
|
530
|
+
- Updated dependencies [ca326b5]
|
|
531
|
+
- Updated dependencies [8f404a5]
|
|
532
|
+
- Updated dependencies [68437d4]
|
|
533
|
+
- Updated dependencies [abb140c]
|
|
534
|
+
- Updated dependencies [8333a6c]
|
|
535
|
+
- Updated dependencies [3e3ecb0]
|
|
536
|
+
- Updated dependencies [3030369]
|
|
537
|
+
- Updated dependencies [d5d8d50]
|
|
538
|
+
- Updated dependencies [e08892d]
|
|
539
|
+
- Updated dependencies [ae05f2e]
|
|
540
|
+
- Updated dependencies [b548e43]
|
|
541
|
+
- Updated dependencies [c463d03]
|
|
542
|
+
- Updated dependencies [64bd6a3]
|
|
543
|
+
- Updated dependencies [13c48c2]
|
|
544
|
+
- Updated dependencies [132742f]
|
|
545
|
+
- Updated dependencies [85a2459]
|
|
546
|
+
- Updated dependencies [50dc214]
|
|
547
|
+
- Updated dependencies [e89fa92]
|
|
548
|
+
- Updated dependencies [e9fcd6b]
|
|
549
|
+
- Updated dependencies [8976ea1]
|
|
550
|
+
- Updated dependencies [56fe8c2]
|
|
551
|
+
- Updated dependencies [acabd24]
|
|
552
|
+
- Updated dependencies [ab50c8f]
|
|
553
|
+
- Updated dependencies [6491463]
|
|
554
|
+
- Updated dependencies [89cf4d6]
|
|
555
|
+
- Updated dependencies [21c5dcb]
|
|
556
|
+
- Updated dependencies [6d4d5d3]
|
|
557
|
+
- Updated dependencies [ed5d557]
|
|
558
|
+
- Updated dependencies [bca21f7]
|
|
559
|
+
- Updated dependencies [e9fcd6b]
|
|
560
|
+
- Updated dependencies [1a7a7c9]
|
|
561
|
+
- Updated dependencies [e9fcd6b]
|
|
562
|
+
- Updated dependencies [ef3a138]
|
|
563
|
+
- Updated dependencies [68d5dfd]
|
|
564
|
+
- Updated dependencies [3e21cf0]
|
|
565
|
+
- Updated dependencies [4cfc93b]
|
|
566
|
+
- Updated dependencies [efd6b43]
|
|
567
|
+
- Updated dependencies [859ded3]
|
|
568
|
+
- Updated dependencies [fa125f3]
|
|
569
|
+
- Updated dependencies [74628d9]
|
|
570
|
+
- Updated dependencies [a646120]
|
|
571
|
+
- Updated dependencies [6f1ce7d]
|
|
572
|
+
- Updated dependencies [7778115]
|
|
573
|
+
- Updated dependencies [2c753fe]
|
|
574
|
+
- Updated dependencies [52804cd]
|
|
575
|
+
- Updated dependencies [3f89967]
|
|
576
|
+
- Updated dependencies [53cf263]
|
|
577
|
+
- Updated dependencies [21aabbc]
|
|
578
|
+
- Updated dependencies [9c270bb]
|
|
579
|
+
- Updated dependencies [76c8c5a]
|
|
580
|
+
- Updated dependencies [088f761]
|
|
581
|
+
- Updated dependencies [a84e1ce]
|
|
582
|
+
- Updated dependencies [bf1054a]
|
|
583
|
+
- Updated dependencies [d8d2776]
|
|
584
|
+
- Updated dependencies [222dc0f]
|
|
585
|
+
- Updated dependencies [e9fcd6b]
|
|
586
|
+
- Updated dependencies [32c917d]
|
|
587
|
+
- Updated dependencies [f9a3c32]
|
|
588
|
+
- Updated dependencies [f502898]
|
|
589
|
+
- Updated dependencies [af7edfe]
|
|
590
|
+
- Updated dependencies [b60f48b]
|
|
591
|
+
- Updated dependencies [c78c918]
|
|
592
|
+
- Updated dependencies [cf9bda4]
|
|
593
|
+
- Updated dependencies [784cb92]
|
|
594
|
+
- Updated dependencies [7629f4d]
|
|
595
|
+
- Updated dependencies [51df9fd]
|
|
596
|
+
- Updated dependencies [a7da4de]
|
|
597
|
+
- Updated dependencies [de0bcdd]
|
|
598
|
+
- Updated dependencies [70f7d6d]
|
|
599
|
+
- Updated dependencies [c677cda]
|
|
600
|
+
- Updated dependencies [554a160]
|
|
601
|
+
- Updated dependencies [f7da71e]
|
|
602
|
+
- Updated dependencies [7f745c3]
|
|
603
|
+
- Updated dependencies [5eb24f8]
|
|
604
|
+
- Updated dependencies [2a3decc]
|
|
605
|
+
- Updated dependencies [cc00df2]
|
|
606
|
+
- Updated dependencies [f4e6adf]
|
|
607
|
+
- Updated dependencies [ee4a59b]
|
|
608
|
+
- Updated dependencies [4db3c61]
|
|
609
|
+
- Updated dependencies [5ca314a]
|
|
610
|
+
- Updated dependencies [e0af1a8]
|
|
611
|
+
- Updated dependencies [414c1fc]
|
|
612
|
+
- Updated dependencies [22c0279]
|
|
613
|
+
- Updated dependencies [0db2947]
|
|
614
|
+
- Updated dependencies [92b5d7f]
|
|
615
|
+
- Updated dependencies [613bfbd]
|
|
616
|
+
- Updated dependencies [abae16a]
|
|
617
|
+
- Updated dependencies [094b8fd]
|
|
618
|
+
- Updated dependencies [c7aca0d]
|
|
619
|
+
- Updated dependencies [c1d8f98]
|
|
620
|
+
- Updated dependencies [8e0b297]
|
|
621
|
+
- Updated dependencies [5f7fa1d]
|
|
622
|
+
- Updated dependencies [87f0ccc]
|
|
623
|
+
- Updated dependencies [aedbaef]
|
|
624
|
+
- Updated dependencies [c5d6803]
|
|
625
|
+
- Updated dependencies [10d05bb]
|
|
626
|
+
- Updated dependencies [69602e5]
|
|
627
|
+
- Updated dependencies [c3ce76c]
|
|
628
|
+
- Updated dependencies [7936b29]
|
|
629
|
+
- Updated dependencies [46803fa]
|
|
630
|
+
- Updated dependencies [c2a336c]
|
|
631
|
+
- Updated dependencies [9f890d3]
|
|
632
|
+
- Updated dependencies [0bb2318]
|
|
633
|
+
- Updated dependencies [f7db8f4]
|
|
634
|
+
- Updated dependencies [1ecee3e]
|
|
635
|
+
- Updated dependencies [9408b7f]
|
|
636
|
+
- Updated dependencies [e9fcd6b]
|
|
637
|
+
- Updated dependencies [9bcd9be]
|
|
638
|
+
- Updated dependencies [b398ad2]
|
|
639
|
+
- Updated dependencies [99261a7]
|
|
640
|
+
- Updated dependencies [81b426f]
|
|
641
|
+
- Updated dependencies [001af1c]
|
|
642
|
+
- Updated dependencies [fb77aa5]
|
|
643
|
+
- Updated dependencies [3d3f60e]
|
|
644
|
+
- Updated dependencies [581d8f8]
|
|
645
|
+
- Updated dependencies [f81afe3]
|
|
646
|
+
- Updated dependencies [40a44b9]
|
|
647
|
+
- Updated dependencies [7a7fb03]
|
|
648
|
+
- Updated dependencies [8fd246d]
|
|
649
|
+
- @objectstack/spec@17.4.0
|
|
650
|
+
- @objectstack/types@17.4.0
|
|
651
|
+
|
|
652
|
+
## 17.3.0
|
|
653
|
+
|
|
654
|
+
### Minor Changes
|
|
655
|
+
|
|
656
|
+
- 655b106: fix(metadata): register a `packages[]` artifact per package at the metadata door so every object has one owner across every door (#14599)
|
|
657
|
+
|
|
658
|
+
A release artifact carrying `packages[]` (ADR-0130 D4) was read at the metadata
|
|
659
|
+
door as if it carried one package: `MetadataPlugin._parseAndRegisterArtifact`
|
|
660
|
+
iterated the **flattened top level** and stamped every item with the artifact's
|
|
661
|
+
own `manifest.id`. For an artifact composed with `composeStacks(…, { manifest:
|
|
662
|
+
'preserve' })` that id is one arbitrary member's — `selectManifest`'s `'last'`
|
|
663
|
+
pick — so a two-package artifact registered the **module's** object under the
|
|
664
|
+
**App** package's identity, while the ObjectQL load path, reading the same
|
|
665
|
+
artifact's `packages[]`, owned it under the module's.
|
|
666
|
+
|
|
667
|
+
The platform then held two answers to "who owns this object", and which one a
|
|
668
|
+
consumer saw depended on the door it went through. Measured on a real boot of
|
|
669
|
+
`examples/app-multi-package`:
|
|
670
|
+
|
|
671
|
+
- `GET /api/v1/meta/object` served `crm_order` **twice** — the list merge keys
|
|
672
|
+
slots by `${packageId}${name}`, so the two differently-attributed copies
|
|
673
|
+
landed in two slots;
|
|
674
|
+
- `GET /api/v1/meta/object?package=<the App package>` returned the **module's**
|
|
675
|
+
object, because the App-stamped copy was re-ingested into the registry as that
|
|
676
|
+
package's contribution;
|
|
677
|
+
- the layers door named the App package while the item door and
|
|
678
|
+
`GET /api/v1/packages` named the module;
|
|
679
|
+
- Studio's Data pillar for the App package listed the module's object — ADR-0130
|
|
680
|
+
Consequences §1.3a ("Studio's scope is the package") did not hold.
|
|
681
|
+
|
|
682
|
+
**The door now reads both shapes, and attributes every item to the body it was
|
|
683
|
+
found in.** `packages` present → each assembled package body's collections are
|
|
684
|
+
registered stamped with **that body's** id; `packages` absent → the single
|
|
685
|
+
`manifest` branch runs exactly as before (D7). The owner is read off the body an
|
|
686
|
+
item was found in — never reverse-derived by matching a top-level item's name
|
|
687
|
+
against a name-to-package index, which would be the second metadata-identity
|
|
688
|
+
resolution path #14512's triage rejected by name.
|
|
689
|
+
|
|
690
|
+
**Ordering and the entry gate are reused, not re-derived (D5).** The door calls
|
|
691
|
+
the same `resolveArtifactPackageOrder` the ObjectQL load path calls, so the two
|
|
692
|
+
readers of one `packages[]` cannot disagree about the registration order **or**
|
|
693
|
+
about which artifacts are loadable at all.
|
|
694
|
+
|
|
695
|
+
⚠️ **`resolveArtifactPackageOrder` / `artifactPackageId` moved to
|
|
696
|
+
`@objectstack/core`** — hence the `minor` there. They were in
|
|
697
|
+
`@objectstack/objectql`, which **depends on** `@objectstack/metadata`, so the
|
|
698
|
+
metadata door could not import them from where they lived; `@objectstack/core`
|
|
699
|
+
already owns `resolvePluginOrder` and is already a dependency of both readers,
|
|
700
|
+
so hosting them there adds **no edge** to the package graph. `@objectstack/objectql`
|
|
701
|
+
re-exports both under their existing names — its published surface is unchanged,
|
|
702
|
+
which is why it is graded `patch`. `@objectstack/runtime` is `patch` for the
|
|
703
|
+
dispatcher error vocabulary's `file:` anchors, repointed at the new path.
|
|
704
|
+
|
|
705
|
+
**Single-package artifacts are byte-for-byte unaffected (D7)**, measured rather
|
|
706
|
+
than asserted: the whole `manager.register` sequence for a single-`manifest`
|
|
707
|
+
artifact — every call, in order, with the id and version each item was stamped
|
|
708
|
+
with — is pinned as a literal in
|
|
709
|
+
`packages/metadata/src/plugin-artifact-packages-attribution.test.ts` and was
|
|
710
|
+
recorded identically on both legs of the ablation. A real boot of
|
|
711
|
+
`examples/app-todo` answers every door identically before and after.
|
|
712
|
+
|
|
713
|
+
**Nothing a booted instance can see today disappears.** Every live
|
|
714
|
+
`ARTIFACT_FIELD_TO_TYPE` key is a member of `AssembledPackageBodySchema`
|
|
715
|
+
(measured, not assumed), so iterating bodies loses no collection; and because
|
|
716
|
+
`packages` composes by `concat`, an artifact whose top level carries a
|
|
717
|
+
definition no package body repeats keeps it — registered once, attributed to the
|
|
718
|
+
artifact's own identity, and logged, because it means the artifact's two halves
|
|
719
|
+
disagree about what it ships.
|
|
720
|
+
|
|
721
|
+
⛔ The **producer** half is untouched: `composeStacks` and `os build` keep
|
|
722
|
+
emitting the flattened top level alongside `packages[]`. Whether they should is
|
|
723
|
+
#14512's decision, not this door's.
|
|
724
|
+
- 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)
|
|
725
|
+
|
|
726
|
+
The substrate step (§10.3) of the accepted #11633 cross-request caching design
|
|
727
|
+
(maintainer acceptance 2026-08-25, Fork 2 → B). It ships the invalidation
|
|
728
|
+
machinery once, before the grants cache (#11967) that will consume it, so that
|
|
729
|
+
leg does not carry it. **Nothing here caches anything.**
|
|
730
|
+
|
|
731
|
+
- **`ObjectQL.writeEpoch`** — a monotonic counter advanced by the engine
|
|
732
|
+
middleware seam on every `insert` / `update` / `delete`, ahead of the whole
|
|
733
|
+
chain (and so ahead of any `isSystem` bypass a middleware applies). It
|
|
734
|
+
generalises the private counter `@objectstack/plugin-security` has carried
|
|
735
|
+
since #10757: the mechanism was always the engine's, and hoisting it lets a
|
|
736
|
+
second consumer share **one** signal instead of minting a parallel one that
|
|
737
|
+
watches a different set of writes. A seam rather than a list of call sites,
|
|
738
|
+
because a forgotten call site fails as silent over-permission and writing
|
|
739
|
+
through the engine is the only way to write at all — including better-auth's
|
|
740
|
+
own adapter.
|
|
741
|
+
- **`authz.invalidated`** — one new channel on the existing `IPubSub`, bridged
|
|
742
|
+
in the shape `MetadataClusterBridgePlugin` already uses. ⭐ **The TTL a
|
|
743
|
+
consuming cache carries is the correctness contract; this channel is not.** No
|
|
744
|
+
shipped driver delivers better than at-most-once (`cluster.mdx` §4.2), so a
|
|
745
|
+
missed message is *expected*, the bridge stays out of the write path (a
|
|
746
|
+
publish failure is logged and swallowed, never awaited by the writer), and the
|
|
747
|
+
channel only moves the *typical* convergence from one TTL to one network hop.
|
|
748
|
+
That statement lives in the code at the channel, where a consumer reads it.
|
|
749
|
+
- **The boot-time posture statement** — non-optional by the ruling. Whenever a
|
|
750
|
+
grants cache is enabled (`OS_AUTHZ_GRANTS_CACHE_TTL_MS` > 0) and there is no
|
|
751
|
+
cross-node invalidation bus, the deployment is told so at `warn`, every boot,
|
|
752
|
+
naming the window it accepted and the remedy. It is a statement, not a
|
|
753
|
+
refusal: a TTL-bounded per-process cache is a legitimate configuration. It is
|
|
754
|
+
said out loud because a silently-absent invalidation bridge is how a security
|
|
755
|
+
control gets disabled with nobody noticing (#4785). The in-process `memory`
|
|
756
|
+
driver counts as **no** bus — a cluster service exists on the shipped default
|
|
757
|
+
while fanning out to nobody, which is the case a "is a cluster service
|
|
758
|
+
registered?" check answers `yes` to and is wrong about.
|
|
759
|
+
|
|
760
|
+
**Runtime behaviour is unchanged.** With no cache consumer the epoch has zero
|
|
761
|
+
subscribers, so nothing is published and nothing is invalidated; with the
|
|
762
|
+
shipped default TTL of `0` the bridge attaches nothing and logs nothing above
|
|
763
|
+
`debug`. The one composition change worth naming: `Runtime` now registers
|
|
764
|
+
`AuthzClusterBridgePlugin` **unconditionally**, including under `cluster: false`
|
|
765
|
+
— that is not an oversight, it is the loudest case the posture check has, and
|
|
766
|
+
skipping it there would put the statement's absence exactly where the missing
|
|
767
|
+
bus is.
|
|
768
|
+
|
|
769
|
+
`@objectstack/plugin-security` is a `patch`: its permission-set memo now reads
|
|
770
|
+
the engine's epoch when the wired engine exposes one and keeps its private
|
|
771
|
+
counter otherwise (test doubles, embeddings). The covered set of writes is
|
|
772
|
+
identical — the plugin's own middleware was already global — and it is now
|
|
773
|
+
identical *by construction* rather than by two files agreeing on which
|
|
774
|
+
operations count.
|
|
775
|
+
- 86cbe37: feat(core): cross-request authorization grants cache — leg B of #11633 (#11971)
|
|
776
|
+
|
|
777
|
+
`resolveUserAuthzGrants` can now cache its resolved envelope across requests,
|
|
778
|
+
governed by `OS_AUTHZ_GRANTS_CACHE_TTL_MS`. **The default is `0` — the cache is
|
|
779
|
+
OFF and the shipped behaviour is unchanged** (Fork 4 of the accepted #11633
|
|
780
|
+
design): a deployment that enables it accepts the configured staleness window
|
|
781
|
+
explicitly, and the boot-time posture statement says so out loud when no
|
|
782
|
+
cross-node invalidation bus is attached.
|
|
783
|
+
|
|
784
|
+
With the cache on:
|
|
785
|
+
|
|
786
|
+
- **Coarse write-invalidation (Fork 1A).** Any engine write to a watched
|
|
787
|
+
authorization object (`sys_member`, `sys_user_position`,
|
|
788
|
+
`sys_user_permission_set`, `sys_position`, `sys_position_permission_set`,
|
|
789
|
+
`sys_permission_set`, `sys_user`) retires every entry on the writing node —
|
|
790
|
+
a grant/revoke/role change is observed by the very next request there, by
|
|
791
|
+
invalidation and not by TTL. `metadata.changed` and peer-node
|
|
792
|
+
`authz.invalidated` hints retire wholesale via the engine write epoch.
|
|
793
|
+
`sys_session` is deliberately not watched (its once-a-minute
|
|
794
|
+
`last_activity_at` cadence would turn the cache into a non-cache).
|
|
795
|
+
- **Expiry-boundary rule.** Entries expire at `min(ttl, nextBoundary)`, where
|
|
796
|
+
`nextBoundary` is the earliest upcoming ADR-0091 `valid_from`/`valid_until`
|
|
797
|
+
among the rows consulted — a validity window flipping is a permission change
|
|
798
|
+
with no write anywhere, so the timer is the only mechanism for that class.
|
|
799
|
+
- **Ruled bypass list.** The permission explainer
|
|
800
|
+
(`plugin-security` `buildContextForUser`) and `runAs:'user'` automation runs
|
|
801
|
+
(`service-automation`) always resolve fresh, and never populate the cache.
|
|
802
|
+
- The TTL remains the correctness contract; the `authz.invalidated` bus only
|
|
803
|
+
narrows the typical cross-node window (no shipped driver exceeds
|
|
804
|
+
at-most-once delivery).
|
|
805
|
+
- 6a180e4: fix(core,rest,services)!: a permission-store read failure now fails LOUD instead of resolving as an authenticated caller holding zero capabilities (#13279)
|
|
806
|
+
|
|
807
|
+
**BREAKING** runtime behaviour change on the shared authorization resolver,
|
|
808
|
+
shipped as `minor` under the repo's launch-window convention.
|
|
809
|
+
|
|
810
|
+
`resolveAuthzContext`'s per-read helper `tryFind` answered a THROWN read exactly
|
|
811
|
+
the way it answered an EMPTY one: `[]`. So an outage of the permission store
|
|
812
|
+
resolved as a well-formed context for an authenticated principal holding no
|
|
813
|
+
capabilities, and the package-management door answered
|
|
814
|
+
`403 FORBIDDEN` — "Reading packages requires the `studio.access` or
|
|
815
|
+
`setup.access` capability." That answer was measured byte-identical
|
|
816
|
+
(`JSON.stringify` equal, against a control that separates two answers which do
|
|
817
|
+
differ) to what a caller who genuinely holds nothing receives. An administrator
|
|
818
|
+
was told they lack a capability, during an outage of the store that holds the
|
|
819
|
+
capability.
|
|
820
|
+
|
|
821
|
+
Maintainer ruling 2026-08-30, verbatim 「第一批其余同意」: `tryFind` 区分「无行」
|
|
822
|
+
与「读失败」,读失败 fail-loud —— 权限库不可达时不再解析为「已认证零能力」,而是
|
|
823
|
+
响亮拒绝(与真实能力拒绝的 403 可区分)。
|
|
824
|
+
|
|
825
|
+
Second maintainer ruling the same day (第 5 场总监席决裁批 #9, verbatim 「同意」),
|
|
826
|
+
after implementing the first one showed that "the read failed" is two facts:
|
|
827
|
+
采**选项 A** —— 把 `isMissingTableError` 从 `@objectstack/metadata` 迁至
|
|
828
|
+
`@objectstack/types`(core 已依赖),metadata 保留 re-export 兼容;`tryFind` 仅对
|
|
829
|
+
**未被判定为「表未 provision」**的读失败抛 `AuthzStoreUnavailableError`。
|
|
830
|
+
|
|
831
|
+
**What changed.** A permission-store read that is issued and throws now raises
|
|
832
|
+
`AuthzStoreUnavailableError`, which carries the EXISTING ADR-0112 wire code
|
|
833
|
+
`SERVICE_UNAVAILABLE` and status `503`. No code is added to the closed wire
|
|
834
|
+
vocabulary and no response envelope gains or loses a key — only which declared
|
|
835
|
+
code an outage selects. Doors that map thrown errors through
|
|
836
|
+
`resolveThrownHttpError` answer 503 with no per-door change.
|
|
837
|
+
|
|
838
|
+
**What did NOT change**, and is pinned:
|
|
839
|
+
|
|
840
|
+
- A reachable, genuinely EMPTY store (reads return no rows) still resolves to
|
|
841
|
+
zero capabilities.
|
|
842
|
+
- A genuine capability denial still answers `403 FORBIDDEN` with its message.
|
|
843
|
+
- An ABSENT engine (`ql` unwired, so no read is ever issued) still resolves to
|
|
844
|
+
an empty-but-valid envelope.
|
|
845
|
+
- Anonymous requests never reach the store, so an outage cannot make them loud.
|
|
846
|
+
- A REAL engine whose `sys_*` tables were never provisioned resolves to zero
|
|
847
|
+
capabilities, quietly — pinned to be byte-identical to the empty-store
|
|
848
|
+
envelope, in every dialect spelling and in the production wrapper shape where
|
|
849
|
+
the driver's phrase is on `cause` rather than the outer message.
|
|
850
|
+
|
|
851
|
+
**The boundary between the two kinds of read failure.** An earlier revision of
|
|
852
|
+
this changeset claimed "embedders without a data plane are unaffected". That
|
|
853
|
+
claim was too broad; it is retracted here, and the gap it named is now closed
|
|
854
|
+
rather than merely disclosed. A read also throws when the table was never
|
|
855
|
+
PROVISIONED — a real engine, wired and reachable, whose `sys_*` tables were
|
|
856
|
+
never created — and that is a supported deployment shape, not an outage. There
|
|
857
|
+
"zero capabilities" is the TRUE answer rather than a fabrication: nothing is
|
|
858
|
+
provisioned, so nothing was withheld. Only an UNREACHABLE store — the ruling's
|
|
859
|
+
own word 不可达 — leaves the capability set unknown, and only an unknown answer
|
|
860
|
+
may not be reported as a denial.
|
|
861
|
+
|
|
862
|
+
Treating the two alike was measured, not theorised: it turned four CI suites
|
|
863
|
+
red, all from `no such table` on `sys_user` / `sys_member` /
|
|
864
|
+
`sys_user_position` / `sys_user_permission_set`. Ordinary CRUD in
|
|
865
|
+
`@objectstack/client` answered `503`; batch validation errors that owe `400`
|
|
866
|
+
answered `503`, because authorization refused before validation ran; runtime
|
|
867
|
+
notifications answered `401` where authenticated callers must be served `200`;
|
|
868
|
+
and two `.integration.test.ts` noise guards reported that the driver and engine
|
|
869
|
+
diagnostics for `sys_position` stopped being emitted — the eager throw aborted
|
|
870
|
+
the resolution before that later read was ever issued, so a change made to stop
|
|
871
|
+
a failed read being silent had made two other channels silent.
|
|
872
|
+
|
|
873
|
+
`tryFind` therefore raises `AuthzStoreUnavailableError` only for a read failure
|
|
874
|
+
that is NOT positively identified as an unprovisioned table.
|
|
875
|
+
|
|
876
|
+
**`isMissingTableError` moved to `@objectstack/types`.** The classifier that
|
|
877
|
+
draws that boundary already existed and was already right — driver-code based
|
|
878
|
+
rather than prose-sniffing, documented so that "cannot say" never means "be
|
|
879
|
+
loud". It lived in `@objectstack/metadata`, which DEPENDS ON `@objectstack/core`,
|
|
880
|
+
so the resolver could not import it. Rather than keep a second copy of a
|
|
881
|
+
security-relevant predicate, the ruling relocated the one classifier to
|
|
882
|
+
`@objectstack/types` — the package core already depends on, and the repo's own
|
|
883
|
+
stated Home rule for a cross-package error predicate ("every consumer of the
|
|
884
|
+
question already depends on it, so adopting the predicate never adds an edge",
|
|
885
|
+
`packages/types/src/unique-violation.ts`). `@objectstack/metadata/errors` still
|
|
886
|
+
exports `isMissingTableError`, re-exported from the new home, so no consumer of
|
|
887
|
+
that published subpath changes.
|
|
888
|
+
|
|
889
|
+
Its sibling `isSchemaAlreadyExistsError` moved with it — the two are not two
|
|
890
|
+
modules but two signatures over one matcher, and separating them would have
|
|
891
|
+
meant re-rolling the matcher, which is the duplication the module exists to
|
|
892
|
+
prevent. Both are now exported from `@objectstack/types`; the metadata subpath
|
|
893
|
+
deliberately still publishes only `isMissingTableError`, which is the only one
|
|
894
|
+
anything imports through it.
|
|
895
|
+
|
|
896
|
+
⚠️ **Signed-off risk, recorded because it is load-bearing.** Gating loudness on
|
|
897
|
+
a driver-error predicate was approved with its false-positive direction stated:
|
|
898
|
+
mis-reading a genuine outage as "table not provisioned" silently restores the
|
|
899
|
+
quiet 403 this change removes, with no thrown error and no other failing test.
|
|
900
|
+
That direction is accepted, not overlooked — the predicate keys on driver codes,
|
|
901
|
+
SQLSTATEs and errnos first, excludes the known superstring traps up front, and
|
|
902
|
+
returns `false` for anything it does not positively recognise, so an
|
|
903
|
+
unrecognised outage stays loud by default. The risk is written beside the
|
|
904
|
+
predicate in `resolve-authz-context.ts` and both directions are pinned by name
|
|
905
|
+
in `authz-store-unavailable.test.ts`. ⛔ Do not widen `isMissingTableError` to
|
|
906
|
+
make a first boot quieter: every widening moves outages into the quiet branch.
|
|
907
|
+
|
|
908
|
+
**All-transport, not just REST.** Every transport authorizing through
|
|
909
|
+
`resolveAuthzContext` inherits this. Six of the eight production transports
|
|
910
|
+
wrapped the call in a fail-closed `catch` that would have re-silenced the
|
|
911
|
+
outage — measured, not assumed: with the resolver loud but the nets untouched,
|
|
912
|
+
the package door answered `401`, i.e. the outage merely changed disguises. Those
|
|
913
|
+
`catch` blocks now re-raise via `isAuthzStoreUnavailableError` and keep their
|
|
914
|
+
previous behaviour for every other fault. The transport set is rebuilt from
|
|
915
|
+
source and audited for set equality on every test run, so a transport added
|
|
916
|
+
later cannot inherit the old silence unnoticed.
|
|
917
|
+
|
|
918
|
+
Callers that treat any throw from `resolveAuthzContext` as "anonymous" should
|
|
919
|
+
re-raise `isAuthzStoreUnavailableError(err)` instead: degrading it restores the
|
|
920
|
+
disguise this removes.
|
|
921
|
+
|
|
922
|
+
<!-- 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. -->
|
|
923
|
+
- d8024f0: feat(core): `Plugin.type` is the closed set the spec declares — a `PluginType` derived from `CORE_PLUGIN_TYPES` (#13925)
|
|
924
|
+
|
|
925
|
+
**BREAKING** accept-set narrowing on a published type, shipped as `minor`
|
|
926
|
+
under the repo's launch-window convention for breaking changes. `Plugin.type`
|
|
927
|
+
(and, through it, `PluginMetadata.type`) was declared `string`, so nothing
|
|
928
|
+
type-checked a plugin author against the eight values the platform accepts —
|
|
929
|
+
the TSDoc beside it carried the whole enumeration as prose, and prose drifted.
|
|
930
|
+
Maintainer ruling 2026-09-01: the Zod enum in `@objectstack/spec`
|
|
931
|
+
(`PluginSchema.type`, declared `z.enum(['standard', ...CORE_PLUGIN_TYPES])`)
|
|
932
|
+
is the authority and the contract was always a closed set; the `string` in
|
|
933
|
+
core was the mismatch, and narrowing it is core aligning to the declared
|
|
934
|
+
contract rather than a new restriction. Paid in one stroke — no warning window.
|
|
935
|
+
|
|
936
|
+
What changes:
|
|
937
|
+
|
|
938
|
+
- `@objectstack/core` now exports `PluginType`, derived from the spec's own
|
|
939
|
+
constant: `'standard' | (typeof CORE_PLUGIN_TYPES)[number]` — today
|
|
940
|
+
`standard`, `ui`, `driver`, `server`, `app`, `theme`, `agent`, `objectql`.
|
|
941
|
+
It is not re-spelled in core, so the compiler's accept set and the Zod gate's
|
|
942
|
+
cannot drift apart; a runtime parity test pins the two against each other.
|
|
943
|
+
- `Plugin.type` is typed `PluginType`. A literal outside the set, or a value
|
|
944
|
+
typed `string`, no longer compiles. Runtime behaviour is unchanged: the Zod
|
|
945
|
+
gate refused such a value before and still does (`invalid_value` at `type`).
|
|
946
|
+
|
|
947
|
+
**Migration.** A plugin that declares one of the eight members needs no change.
|
|
948
|
+
A plugin that assigned a computed or `string`-typed value narrows it at the
|
|
949
|
+
producer — declare the literal, or type the variable `PluginType` — rather than
|
|
950
|
+
casting at the assignment; a value that was never one of the eight was never a
|
|
951
|
+
valid plugin type and was already refused at parse time.
|
|
952
|
+
|
|
953
|
+
<!-- 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. -->
|
|
954
|
+
- 4635f3e: fix(spec,core): `HotReloadConfig.stateStrategy` refuses the two values it never implemented; `distributedConfig` retired (#12340, ADR-0049)
|
|
955
|
+
|
|
956
|
+
<!-- adr-0087: registered hot-reload-inert-state-strategies-retired -->
|
|
957
|
+
|
|
958
|
+
**BREAKING** accept-set narrowing + export removal, landing after the v17.0.0
|
|
959
|
+
cut (the lockstep launch-window convention ships it as `minor`; the
|
|
960
|
+
prescription is registered under protocol major 18 —
|
|
961
|
+
`RETIRED_DEFS_BY_MAJOR[18]` + the D3 semantic entry
|
|
962
|
+
`hot-reload-inert-state-strategies-retired` — where `os migrate meta` users
|
|
963
|
+
will look).
|
|
964
|
+
|
|
965
|
+
This is ADR-0049 applied one level INSIDE the library the 2026-08-25 #11825
|
|
966
|
+
ruling deliberately kept. That ruling retired the authorable lifecycle-config
|
|
967
|
+
container and kept `HotReloadConfigSchema` as a host-driven library parameter
|
|
968
|
+
type; this change measures the kept vocabulary's own remainder and finds the
|
|
969
|
+
same defect in it. The keep itself stands — `HotReloadConfigSchema`,
|
|
970
|
+
`PluginStateSnapshotSchema` and the health vocabularies still export, and
|
|
971
|
+
`HotReloadManager` / `PluginHealthMonitor` are untouched.
|
|
972
|
+
|
|
973
|
+
The `'disk'` and `'distributed'` arms of `PluginStateManager.saveState` both
|
|
974
|
+
wrote to the SAME in-memory `Map` as `'memory'` — the in-source comments said
|
|
975
|
+
"memory fallback" — and announced the substitution at DEBUG level only. A host
|
|
976
|
+
that asked for durable or cluster-replicated state got process-local memory
|
|
977
|
+
and no error: state that does not survive the restart it was configured to
|
|
978
|
+
survive. `distributedConfig` had ZERO readers anywhere, so an author could
|
|
979
|
+
name a Redis endpoint, a TTL and a replication factor and nothing ever opened
|
|
980
|
+
a connection.
|
|
981
|
+
|
|
982
|
+
FROM → TO:
|
|
983
|
+
|
|
984
|
+
- `stateStrategy: 'disk'` → `stateStrategy: 'memory'` — byte-identical runtime
|
|
985
|
+
behaviour, because `'disk'` already stored to memory. It is the spelling
|
|
986
|
+
that was false, not the behaviour.
|
|
987
|
+
- `stateStrategy: 'distributed'` → `stateStrategy: 'memory'` — same, or
|
|
988
|
+
`'none'` to disable state preservation outright.
|
|
989
|
+
- `distributedConfig: { … }` → *(removed)* — delete the key. It left with the
|
|
990
|
+
`'distributed'` value its own doc comment called it "required" for.
|
|
991
|
+
- `DistributedStateConfigSchema` / `DistributedStateConfig` /
|
|
992
|
+
`DistributedStateConfigParsed` → *(removed)* — the orphan value schema of
|
|
993
|
+
that one key.
|
|
994
|
+
|
|
995
|
+
One-line fix: replace `'disk'` or `'distributed'` with `'memory'` and delete
|
|
996
|
+
any `distributedConfig` — you were already getting in-memory state. There is
|
|
997
|
+
no in-tree replacement for durable or distributed plugin state; persist it in
|
|
998
|
+
the host, which owns the process lifetime these strategies pretended to
|
|
999
|
+
outlive. Real disk or distributed persistence returns only via the ENFORCE
|
|
1000
|
+
route of ADR-0049 — the implementation first, the declaration with it.
|
|
1001
|
+
|
|
1002
|
+
The retirement kit:
|
|
1003
|
+
|
|
1004
|
+
- **enum-value narrowing** (`['memory','disk','distributed','none']` →
|
|
1005
|
+
`['memory','none']`): invisible to all four ratchets by construction (the
|
|
1006
|
+
def still emits), so the prescription hangs on the enum's own `error` map
|
|
1007
|
+
dispatched by `issue.input` — the `crypto.hash` / `managedBy: 'system'`
|
|
1008
|
+
precedent. A value that was never legal still gets zod's own enum message,
|
|
1009
|
+
so a typo is not told it "was removed".
|
|
1010
|
+
- **whole-def deletion** (route 3 — `HotReloadConfig` is not an authorable
|
|
1011
|
+
surface: no metadata-type binding, stack collection or manifest embed ever
|
|
1012
|
+
carried it, and nothing in the tree parses `HotReloadConfigSchema` outside
|
|
1013
|
+
its own unit test, so there is no authored document to rewrite and nobody
|
|
1014
|
+
who could receive a parse-time tombstone): `kernel/DistributedStateConfig`
|
|
1015
|
+
in `RETIRED_DEFS_BY_MAJOR[18]` plus the D3 semantic entry. Ratchets moved as
|
|
1016
|
+
a def removal must — `api-surface` −3, `authorable-surface` −8,
|
|
1017
|
+
`json-schema.manifest` −1.
|
|
1018
|
+
- **runtime doors** in `@objectstack/core`, because route 3 leaves no
|
|
1019
|
+
parse-time prescription: `HotReloadManager.registerPlugin` now refuses an
|
|
1020
|
+
unhonoured `stateStrategy` and a leftover `distributedConfig` with an
|
|
1021
|
+
ADR-0112 envelope (`code: VALIDATION_ERROR`, `status: 400`) carrying the
|
|
1022
|
+
prescription. Refused BEFORE the `enabled` check, so a disabled config
|
|
1023
|
+
cannot smuggle the false declaration through. TypeScript hosts never reach
|
|
1024
|
+
it — `HotReloadConfigParsed['stateStrategy']` is now `'memory' | 'none'`, a
|
|
1025
|
+
compile error at the call site.
|
|
1026
|
+
- **pin move, declared**: `DistributedStateConfigSchema` was NAMED in the
|
|
1027
|
+
#11825 survivor list, so this reverses one line of that ruling on new
|
|
1028
|
+
evidence — #11825 measured the container's six groups, never this key's own
|
|
1029
|
+
readers. The pin in `kernel/plugin-lifecycle-advanced-retirement.test.ts`
|
|
1030
|
+
moves in the same commit with the reasoning recorded beside it, and asserts
|
|
1031
|
+
the surrounding keep is intact.
|
|
1032
|
+
- zero in-tree consumers passed `'disk'` or `'distributed'` (measured at
|
|
1033
|
+
cdbd9204b6 with a firing positive control; every live caller passes
|
|
1034
|
+
`'memory'` or `'none'`), so no in-repo source changes ride along.
|
|
1035
|
+
- ee3595c: fix(spec,core): `HotReloadManager.startWatching` refuses instead of reporting success; `HotReloadConfig.watchPatterns` retired (#12428, ADR-0049)
|
|
1036
|
+
|
|
1037
|
+
<!-- adr-0087: registered hot-reload-watch-placeholder-retired -->
|
|
1038
|
+
|
|
1039
|
+
**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep
|
|
1040
|
+
launch-window convention ships it as `minor`; the prescription is registered
|
|
1041
|
+
under protocol major 18 — `RETIRED_KEYS_BY_MAJOR[18]` + the D3 semantic entry
|
|
1042
|
+
`hot-reload-watch-placeholder-retired` — where `os migrate meta` users will
|
|
1043
|
+
look). Graded `minor` rather than `major` for the same reason #12340 was one
|
|
1044
|
+
day earlier, in this same module.
|
|
1045
|
+
|
|
1046
|
+
ADR-0049 applied one symbol over from #12340, in the same file and on the same
|
|
1047
|
+
per-key test. The #11825 keep still stands: `HotReloadConfigSchema` and
|
|
1048
|
+
`PluginStateSnapshotSchema` still export, and `HotReloadManager` /
|
|
1049
|
+
`PluginHealthMonitor` are untouched apart from the two doors below.
|
|
1050
|
+
|
|
1051
|
+
`HotReloadManager.startWatching` contained **no watcher**. Its whole body was a
|
|
1052
|
+
guard plus `logger.info('File watching started', { patterns })`, above an
|
|
1053
|
+
in-source note saying real watching "would require chokidar or similar". Where
|
|
1054
|
+
#12340's inert fallback at least announced itself at DEBUG, this claimed
|
|
1055
|
+
success at **INFO**: an operator who set `enabled: true` with `watchPatterns`
|
|
1056
|
+
and read that line had been told the opposite of the truth. `watchHandles` was
|
|
1057
|
+
only ever read, deleted, iterated and cleared and **never set**, so
|
|
1058
|
+
`stopWatching`'s cleanup branch and the teardown loop over its keys were
|
|
1059
|
+
structurally unreachable rather than merely untaken. `watchPatterns` therefore
|
|
1060
|
+
had no reader that acted on it — its only two uses were log lines.
|
|
1061
|
+
|
|
1062
|
+
FROM → TO:
|
|
1063
|
+
|
|
1064
|
+
- `watchPatterns: ['src/**/*.ts']` → *(removed)* — delete the key. Declare your
|
|
1065
|
+
globs wherever your own watcher reads them.
|
|
1066
|
+
- `manager.startWatching(name)` → `manager.scheduleReload(name, reloadFn)`,
|
|
1067
|
+
called from your own watcher's change handler. That is the debounced
|
|
1068
|
+
integration point this class does implement, and it is unchanged.
|
|
1069
|
+
|
|
1070
|
+
One-line fix: delete `watchPatterns`, and call `scheduleReload` from your own
|
|
1071
|
+
file watcher instead of `startWatching` — nothing was ever watched, so nothing
|
|
1072
|
+
that used to happen stops happening. File watching is the host's job in this
|
|
1073
|
+
host-driven library; `chokidar` is already a dependency of
|
|
1074
|
+
`@objectstack/metadata`, `@objectstack/metadata-fs` and `@objectstack/cli` —
|
|
1075
|
+
never of `@objectstack/core` — so a host has a working model to copy.
|
|
1076
|
+
|
|
1077
|
+
The retirement kit:
|
|
1078
|
+
|
|
1079
|
+
- **key tombstone**, and the build is what chose it: the plain deletion was
|
|
1080
|
+
tried first and `gen:schema` gate (a) refused it, because
|
|
1081
|
+
`HotReloadConfigSchema` is not `.strict()` and a bare deletion would be a
|
|
1082
|
+
silent strip (#3733, ADR-0104) — the very defect being retired, one layer
|
|
1083
|
+
down. #12340 could take route 3 because what left there was a whole *def*; a
|
|
1084
|
+
key leaving a *surviving* def has no such exit. So `watchPatterns` is
|
|
1085
|
+
`retiredKey()`-tombstoned, its surface line carries `[RETIRED]`, and
|
|
1086
|
+
`kernel/HotReloadConfig:watchPatterns` is registered by exact key in
|
|
1087
|
+
`RETIRED_KEYS_BY_MAJOR[18]`. A key tombstone on a surviving def moves
|
|
1088
|
+
`authorable-surface` only — the def still emits, so `api-surface` and
|
|
1089
|
+
`json-schema.manifest` do not.
|
|
1090
|
+
- **no D2 conversion**, deliberately: the chain walks a normalized stack, and
|
|
1091
|
+
`HotReloadConfig` is not an authorable surface — no metadata-type binding,
|
|
1092
|
+
stack collection or manifest embed ever carried it — so a conversion would be
|
|
1093
|
+
a transform with no seam that ever runs. For the same reason the prescription
|
|
1094
|
+
carries no `os migrate meta` sentence, exactly as its `stateStrategy` sibling
|
|
1095
|
+
in this module does not.
|
|
1096
|
+
- **runtime doors** in `@objectstack/core`, because nothing in the tree parses
|
|
1097
|
+
`HotReloadConfigSchema` outside its own unit test, so the tombstone alone
|
|
1098
|
+
reaches nobody: `startWatching` now throws an ADR-0112 envelope
|
|
1099
|
+
(`code: VALIDATION_ERROR`, `status: 400`) carrying the prescription, and
|
|
1100
|
+
`registerPlugin` refuses a leftover `watchPatterns` the same way — before the
|
|
1101
|
+
`enabled` check, so a disabled config cannot smuggle the false declaration
|
|
1102
|
+
through. `startWatching` is kept as a throwing door rather than deleted so
|
|
1103
|
+
that caller meets a prescription instead of a bare `TypeError`.
|
|
1104
|
+
- **dead code removed with a firing positive control**: `watchHandles` and both
|
|
1105
|
+
of its unreachable readers are gone. The zero was pinned first —
|
|
1106
|
+
`reloadTimers.set` resolves a real writer in the same file and the same scan,
|
|
1107
|
+
while `watchHandles.set` resolves nothing anywhere. `stopWatching` keeps the
|
|
1108
|
+
half that always did something (it cancels a pending debounced reload), and
|
|
1109
|
+
`shutdown` is unchanged in effect: the loop it lost iterated `watchHandles`
|
|
1110
|
+
and therefore ran zero times.
|
|
1111
|
+
- **ENFORCE and EXPERIMENTAL were both unavailable**, which is why this is a
|
|
1112
|
+
removal: no runtime composes `HotReloadManager`, so enforcing would build for
|
|
1113
|
+
a caller that does not exist; and a scan of every planning doc returned zero
|
|
1114
|
+
mentions of hot-reload file watching against 145 control hits in the same
|
|
1115
|
+
files, so there is no roadmap for `experimental` to point at.
|
|
1116
|
+
- af56546: feat(platform-objects): packaged disable works without the automation service, and the activation ledger has one implementation (#12359, #12350)
|
|
1117
|
+
|
|
1118
|
+
Two halves of ADR-0126's "ledger convergence", bundled by maintainer ruling
|
|
1119
|
+
(2026-08-26, verbatim and untranslated: 「同意」).
|
|
1120
|
+
|
|
1121
|
+
## The registration follows the declaration (#12359)
|
|
1122
|
+
|
|
1123
|
+
`sys_metadata_activation` is declared in `@objectstack/platform-objects`, but
|
|
1124
|
+
the only thing that REGISTERED it was the automation service's manifest —
|
|
1125
|
+
because flows were the ledger's first and, until packaged actions landed, only
|
|
1126
|
+
consumer. Packaged actions are a second consumer with a different owner: their
|
|
1127
|
+
consult and write path live on the ObjectQL engine, present in every
|
|
1128
|
+
composition that can execute an action.
|
|
1129
|
+
|
|
1130
|
+
So a deployment with actions and no automation service had no ledger table, and
|
|
1131
|
+
the activation door answered **503 SERVICE_UNAVAILABLE** on every flip —
|
|
1132
|
+
correctly (ADR-0126 §6 wall 3: a flip that cannot be made durable must not be
|
|
1133
|
+
reported as one) and permanently. Measured on a real boot; it is now this
|
|
1134
|
+
change's positive test, measured on the same boot:
|
|
1135
|
+
|
|
1136
|
+
```
|
|
1137
|
+
POST /api/v1/actions/_activation/showcase_task/showcase_mark_done {"enabled":false}
|
|
1138
|
+
before -> 503 SERVICE_UNAVAILABLE after -> 200, and dispatch refuses 409 ACTION_DISABLED
|
|
1139
|
+
```
|
|
1140
|
+
|
|
1141
|
+
`PlatformObjectsPlugin` registers it now, so every composition carrying
|
|
1142
|
+
platform-objects has the ledger and each future ADR-0126 §8 consumer (`tool`,
|
|
1143
|
+
`skill`, `position`) inherits it. **MOVE, not add** — the automation service no
|
|
1144
|
+
longer names the object. That was not a style choice: a second code package
|
|
1145
|
+
claiming one object throws `Object "…" is already owned by package "…"`
|
|
1146
|
+
(ADR-0029 D3/D7), measured, so adding a registrant would have been a boot
|
|
1147
|
+
failure rather than a duplicate.
|
|
1148
|
+
|
|
1149
|
+
**Upgrade is a no-op for existing data, and that is measured rather than
|
|
1150
|
+
asserted.** A manifest is also a ROUTING decision — `resolveDatasourceBinding`
|
|
1151
|
+
step 4 routes an object by its owning package's `defaultDatasource` — so the
|
|
1152
|
+
registrar carries the table's datasource with it:
|
|
1153
|
+
|
|
1154
|
+
```
|
|
1155
|
+
owner com.objectstack.service-automation (defaultDatasource:'cloud') -> 'cloud'
|
|
1156
|
+
owner com.objectstack.platform-objects (none) -> undefined (global default driver)
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
The ledger table already exists in live databases, so on any deployment
|
|
1160
|
+
carrying a `cloud` datasource that difference would leave the rows in one
|
|
1161
|
+
database and read another — every disabled artifact silently re-arming. The
|
|
1162
|
+
ledger therefore rides its own manifest from the same plugin, carrying the
|
|
1163
|
+
automation manifest's `scope` / `namespace` / `defaultDatasource` triple
|
|
1164
|
+
verbatim. The three siblings (`sys_migration`, `sys_migration_journal`,
|
|
1165
|
+
`sys_secret`) deliberately do not get it and keep riding the project database.
|
|
1166
|
+
|
|
1167
|
+
## One implementation of the §4 row contract (#12350)
|
|
1168
|
+
|
|
1169
|
+
ADR-0126 §4 declares one activation ledger; it had two independent
|
|
1170
|
+
implementations of that one row contract — `ObjectStoreFlowActivationStore`
|
|
1171
|
+
(service-automation) and `ObjectStoreActionActivationStore` (objectql). They
|
|
1172
|
+
agreed because the second was written from the first, and nothing structurally
|
|
1173
|
+
held them together; §8 pre-charts `tool`, `skill` and `position`, and a third
|
|
1174
|
+
and fourth copy is where the org-row skip and the `0`-is-false read get lost
|
|
1175
|
+
quietly, in the direction (an artifact re-arming) nothing else measures.
|
|
1176
|
+
|
|
1177
|
+
Neither consumer could import the other, so the contract now lives once in
|
|
1178
|
+
`@objectstack/core` — the package both already depend on — as
|
|
1179
|
+
`ObjectStoreMetadataActivationStore(engine, metadataType)`, exported alongside
|
|
1180
|
+
`InMemoryMetadataActivationStore`, `MetadataActivationRow`,
|
|
1181
|
+
`MetadataActivationStore`, `MetadataActivationStoreEngine` and
|
|
1182
|
+
`METADATA_ACTIVATION_TABLE`. Each consumer keeps its own name, its own
|
|
1183
|
+
one-argument constructor and its own docs, and fixes the discriminator.
|
|
1184
|
+
|
|
1185
|
+
**No behaviour change and no API break.** `ObjectStoreFlowActivationStore` /
|
|
1186
|
+
`InMemoryFlowActivationStore` / `FlowActivationStoreEngine` and
|
|
1187
|
+
`ObjectStoreActionActivationStore` / `InMemoryActionActivationStore` /
|
|
1188
|
+
`ActionActivationRow` / `ActionActivationStore` / `ActionActivationStoreEngine`
|
|
1189
|
+
/ `ACTION_ACTIVATION_TABLE` are exported from the same modules with the same
|
|
1190
|
+
shapes. Row semantics are byte-equivalent: deployment-level rows scoped only by
|
|
1191
|
+
the `metadata_type` discriminator, a driver `0` read as false, read-then-write
|
|
1192
|
+
rather than a blind upsert, and no `delete` in the engine slice because
|
|
1193
|
+
re-enabling rewrites the row.
|
|
1194
|
+
|
|
1195
|
+
Both existing pin suites stay green **unchanged**, which is what makes them the
|
|
1196
|
+
proof the consolidation lost nothing — verified by ablation: mutating the one
|
|
1197
|
+
shared implementation turns both of them red on their own assertions, so both
|
|
1198
|
+
really reach it.
|
|
1199
|
+
- a8c00e2: feat(core): cache successful `sys_setting` localization reads, invalidated synchronously on write (#11966)
|
|
1200
|
+
|
|
1201
|
+
Leg C (ship-first) of the accepted #11633 cross-request caching design
|
|
1202
|
+
(maintainer acceptance 2026-08-25, forks 1A / 2B / 3A / TTL-0).
|
|
1203
|
+
`resolveLocalizationContext` re-read `sys_setting` on **every** authenticated
|
|
1204
|
+
request to answer the same three keys — `timezone` / `locale` / `currency` —
|
|
1205
|
+
for a workspace whose values change roughly never. That read is now cached.
|
|
1206
|
+
|
|
1207
|
+
**Grade: `minor`, not `patch`.** It adds a deployment variable
|
|
1208
|
+
(`OS_LOCALIZATION_CACHE_TTL_MS`) and changes the query pattern of a shipped code
|
|
1209
|
+
path. Not `major`: the observable contract callers actually depend on — a
|
|
1210
|
+
settings write is visible to the very next read — is preserved, and pinned.
|
|
1211
|
+
|
|
1212
|
+
Caching this read was tried once before and reverted. #10221's first version
|
|
1213
|
+
memoized every outcome for 30s and CI went red on
|
|
1214
|
+
`analytics-timezone.dogfood.test.ts`, which writes a new org timezone and
|
|
1215
|
+
expects the very next analytics query to bucket under it; the cache was narrowed
|
|
1216
|
+
to memoize **failures** only. That verdict was on **TTL-only** caching and it
|
|
1217
|
+
still stands unamended. What changed is that the process now has invalidation
|
|
1218
|
+
seams it did not have then:
|
|
1219
|
+
|
|
1220
|
+
- **Primary — the settings change seam.** `SettingsService.subscribe(ns, handler)`
|
|
1221
|
+
dispatches synchronously and in-process from the write path, after the row is
|
|
1222
|
+
persisted. (⚠️ #11633 calls this a "settings change bus"; no such module
|
|
1223
|
+
exists — `subscribe()` is the seam. No change was needed in
|
|
1224
|
+
`@objectstack/service-settings`: the seam was already public and already does
|
|
1225
|
+
exactly this.)
|
|
1226
|
+
- **Backstop — the engine write epoch** from #11968's substrate. Needed because
|
|
1227
|
+
this resolver's own fallback reads `sys_setting` *directly*, so a seeder or
|
|
1228
|
+
any other direct engine write emits no settings event at all. It is read
|
|
1229
|
+
structurally rather than imported, because `@objectstack/objectql` depends on
|
|
1230
|
+
`@objectstack/core` and the substrate declared `WriteEpochLike` separately for
|
|
1231
|
+
exactly this consumer. A peer node's hint arrives as a local bump, so an
|
|
1232
|
+
attached `authz.invalidated` bridge narrows cross-node convergence for free.
|
|
1233
|
+
- **TTL** — the residual bound, for what neither seam can see. Default 30s,
|
|
1234
|
+
`0` disables the cache on a real path rather than a degenerate one.
|
|
1235
|
+
|
|
1236
|
+
Two rules carry the change and are pinned rather than merely documented:
|
|
1237
|
+
|
|
1238
|
+
1. **A success is cached only when the engine exposes the write epoch.** A `ql`
|
|
1239
|
+
with no seam is a `ql` whose writes the cache cannot observe, so rather than
|
|
1240
|
+
degrade to the TTL-only shape that was already reverted here once, the cache
|
|
1241
|
+
declines. A partial `{ current }` shape is not a seam either — a counter
|
|
1242
|
+
nothing can bump would read as a live invalidation source and pin the answer
|
|
1243
|
+
for a whole TTL.
|
|
1244
|
+
2. **Invalidation retires success entries only.** #10221's failure memo exists
|
|
1245
|
+
for an environment where `sys_setting` is missing; retiring it on a write
|
|
1246
|
+
would restart precisely the per-request driver log spam that memo removed,
|
|
1247
|
+
and no write can create a missing table. It stays TTL-bound and behaves
|
|
1248
|
+
exactly as #10221/#11877 shipped it.
|
|
1249
|
+
|
|
1250
|
+
`analytics-timezone.dogfood.test.ts` is unchanged and unweakened — it is this
|
|
1251
|
+
leg's acceptance test, and an ablation that reduces the cache to its TTL turns
|
|
1252
|
+
it red on the same assertion the original revert was recorded against.
|
|
1253
|
+
- 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).
|
|
1254
|
+
|
|
1255
|
+
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.
|
|
1256
|
+
|
|
1257
|
+
**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:
|
|
1258
|
+
|
|
1259
|
+
- an **expired** platform-admin grant no longer authorizes `/sso/register` or `/admin/impersonate-user`, and no longer appears in the session payload;
|
|
1260
|
+
- a **deactivated** `admin_full_access` permission set no longer confers platform standing anywhere — the deactivation dialog's promise now holds on these gates too;
|
|
1261
|
+
- 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.
|
|
1262
|
+
|
|
1263
|
+
**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.
|
|
1264
|
+
|
|
1265
|
+
**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.
|
|
1266
|
+
|
|
1267
|
+
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.
|
|
1268
|
+
- 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).
|
|
1269
|
+
|
|
1270
|
+
**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.
|
|
1271
|
+
|
|
1272
|
+
**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.
|
|
1273
|
+
|
|
1274
|
+
**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.
|
|
1275
|
+
|
|
1276
|
+
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`).
|
|
1277
|
+
|
|
1278
|
+
`@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.
|
|
1279
|
+
- b72db01: fix(spec,core): `PluginHealthMonitor` stops claiming a restart it never performed; the three `PluginHealthCheck` restart keys retired (#12032, ADR-0049)
|
|
1280
|
+
|
|
1281
|
+
<!-- adr-0087: registered plugin-auto-restart-never-reinitialised -->
|
|
1282
|
+
|
|
1283
|
+
**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep
|
|
1284
|
+
launch-window convention ships it as `minor`; the prescriptions are registered
|
|
1285
|
+
under protocol major 18 — three `RETIRED_KEYS_BY_MAJOR[18]` entries plus the D3
|
|
1286
|
+
semantic entry `plugin-auto-restart-never-reinitialised` — where
|
|
1287
|
+
`os migrate meta` users will look). Graded `minor` rather than `major` for the
|
|
1288
|
+
same reason #12340 and #12428 were, the day before, in this same module.
|
|
1289
|
+
|
|
1290
|
+
## What was measured
|
|
1291
|
+
|
|
1292
|
+
`PluginHealthMonitor.attemptRestart` called `plugin.destroy()` and stopped
|
|
1293
|
+
there. The comment above the call read *"Call destroy and init to restart"*,
|
|
1294
|
+
and `init` appeared in `health-monitor.ts` **only inside that comment**. So a
|
|
1295
|
+
plugin whose health checks crossed `failureThreshold` with `autoRestart: true`
|
|
1296
|
+
got: `destroy()`, a log line reading `Plugin restarted`, status `recovering`,
|
|
1297
|
+
and periodic health checks that carried on running against the destroyed
|
|
1298
|
+
instance. The default check when no `checkMethod` resolves is
|
|
1299
|
+
`{ name: 'plugin-loaded', status: 'passed' }`, which a destroyed object passes
|
|
1300
|
+
indefinitely — so the **terminal** report on a torn-down, never-re-initialised
|
|
1301
|
+
plugin was `healthy`.
|
|
1302
|
+
|
|
1303
|
+
Reproduced at `ee3595cefd` before anything was changed, with
|
|
1304
|
+
`successThreshold: 3`:
|
|
1305
|
+
|
|
1306
|
+
```
|
|
1307
|
+
round 1 (failing): status=failed destroyed=0 alive=true
|
|
1308
|
+
after backoff: status=recovering destroyed=1 alive=false
|
|
1309
|
+
recovery round 1: status=recovering destroyed=1 alive=false
|
|
1310
|
+
recovery round 2: status=recovering destroyed=1 alive=false
|
|
1311
|
+
recovery round 3: status=healthy destroyed=1 alive=false
|
|
1312
|
+
```
|
|
1313
|
+
|
|
1314
|
+
#11955 made that report *more* convincing rather than less: reaching `healthy`
|
|
1315
|
+
now costs `successThreshold` consecutive passing rounds, so a destroyed plugin
|
|
1316
|
+
has to earn a declared number of passes before it is misreported.
|
|
1317
|
+
`restartAttempts` was incremented as though a restart had occurred, and
|
|
1318
|
+
`maxRestartAttempts` / `restartBackoff` scheduled further "restarts" of a plugin
|
|
1319
|
+
that was never brought back up.
|
|
1320
|
+
|
|
1321
|
+
## Why REMOVE and not the other two ADR-0049 states
|
|
1322
|
+
|
|
1323
|
+
**ENFORCE** would have to build the restart, and the class cannot host one.
|
|
1324
|
+
`Plugin.init(ctx)` needs a `PluginContext`; the only two `plugin.init(...)` call
|
|
1325
|
+
sites in the tree are the kernel's own boot loops (`kernel-base.ts:202`,
|
|
1326
|
+
`kernel.ts:607`), both over the full plugin list, with a context that is
|
|
1327
|
+
`private` on `ObjectKernel` and `protected` on `KernelBase`. No host can obtain
|
|
1328
|
+
one, so a host-provided re-init hook would have had nothing to call. (Positive
|
|
1329
|
+
control for that scan: the same pass resolves five real non-test
|
|
1330
|
+
`plugin.destroy()` call sites, so it does see lifecycle drivers.) Building a
|
|
1331
|
+
per-plugin re-init API for a caller that does not exist — no runtime constructs
|
|
1332
|
+
`PluginHealthMonitor` (#11825) — is the speculation ADR-0049's staged decision
|
|
1333
|
+
names as the wrong default at this milestone, where the shippable liability is
|
|
1334
|
+
the false promise and not the missing feature.
|
|
1335
|
+
|
|
1336
|
+
**EXPERIMENTAL** requires a roadmap. A scan of the whole `docs/` planning + ADR
|
|
1337
|
+
corpus returned **zero** mentions of plugin auto-restart, against 118 control
|
|
1338
|
+
hits for "health" and 13 for "hot reload" in the same corpus.
|
|
1339
|
+
|
|
1340
|
+
`maxRestartAttempts` and `restartBackoff` leave with `autoRestart` rather than
|
|
1341
|
+
as a tidy-up: with no restart, *"Maximum restart attempts before giving up"* and
|
|
1342
|
+
*"Backoff strategy for restart delays"* have nothing left to be the vocabulary
|
|
1343
|
+
**of** — the test that took `distributedConfig` out with the `stateStrategy`
|
|
1344
|
+
value it was documented as requiring (#12340).
|
|
1345
|
+
|
|
1346
|
+
## What changes for a host
|
|
1347
|
+
|
|
1348
|
+
All three keys are **tombstoned**, not deleted: `PluginHealthCheckSchema` is not
|
|
1349
|
+
`.strict()`, so a bare deletion would be a silent strip (#3733, ADR-0104) — a
|
|
1350
|
+
milder form of the defect being retired. A TypeScript host gets a `tsc` error
|
|
1351
|
+
(the keys are typed `never`); a parse raises the prescription; and
|
|
1352
|
+
`PluginHealthMonitor.registerPlugin` refuses a hand-built config carrying any of
|
|
1353
|
+
them with an ADR-0112 envelope (`code: VALIDATION_ERROR`, `status: 400`), thrown
|
|
1354
|
+
before any state is stored so a refused config leaves no half-registered plugin
|
|
1355
|
+
behind.
|
|
1356
|
+
|
|
1357
|
+
`PluginHealthMonitor` no longer calls `plugin.destroy()` at all. A plugin that
|
|
1358
|
+
crosses `failureThreshold` is reported `degraded` / `unhealthy` / `failed` and
|
|
1359
|
+
left running; acting on that is the host's job in this host-driven library
|
|
1360
|
+
(#11825 route 2). Poll `getHealthStatus(pluginName)` / `getHealthReport(pluginName)`
|
|
1361
|
+
and restart at the level that owns the plugin's lifetime.
|
|
1362
|
+
|
|
1363
|
+
Everything else in the monitor is unchanged: registration, periodic checks, the
|
|
1364
|
+
`timeout` race and its refd-timer guard (#4875), both failure routes sharing the
|
|
1365
|
+
counters (#11852), and `successThreshold` binding from every status that records
|
|
1366
|
+
a failure (#11955). `recovering` is now written only by the success branch —
|
|
1367
|
+
the one writer that ever meant it.
|
|
1368
|
+
- 49f0dcf: feat(core): retire the inert `PluginMetadata` surfaces — `configSchema` with `PluginConfigValidator`, and `hotReloadable` (#11982, #12587, ADR-0049)
|
|
1369
|
+
|
|
1370
|
+
<!-- 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. -->
|
|
1371
|
+
|
|
1372
|
+
**BREAKING**: removes a published-but-inert capability from the `.` entry of
|
|
1373
|
+
`@objectstack/core`. Shipped as `minor` under the lockstep launch-window
|
|
1374
|
+
convention (a `major` bump is refused repo-wide by `check:changeset-no-major`).
|
|
1375
|
+
|
|
1376
|
+
Removed, each measured at zero live consumers with positive controls (the
|
|
1377
|
+
sibling `startupTimeout` is read live by the kernel's startup timeout guard);
|
|
1378
|
+
maintainer ruled retire under ADR-0049 enforce-or-remove, 2026-08-27,
|
|
1379
|
+
decision-inbox batch 5; recorded in ADR-0025 §3.7:
|
|
1380
|
+
|
|
1381
|
+
- `PluginMetadata.configSchema` — declared "Configuration schema for
|
|
1382
|
+
validation", but the mechanism could never run: the loader's only call
|
|
1383
|
+
passed no config, and no caller could — plugin factories close over their
|
|
1384
|
+
config, so the kernel never receives it. Every one of ~40 production
|
|
1385
|
+
`kernel.use()` compositions already passes config as constructor arguments
|
|
1386
|
+
and works.
|
|
1387
|
+
- `PluginConfigValidator` / `createPluginConfigValidator` — the validator
|
|
1388
|
+
behind that field: real code with zero reachable invocations, deleted along
|
|
1389
|
+
with its unit test and its export from the security barrel.
|
|
1390
|
+
- `PluginMetadata.hotReloadable` — declared "Whether plugin supports hot
|
|
1391
|
+
reload" with zero reads and zero declarations: `HotReloadManager.reloadPlugin`
|
|
1392
|
+
gates only on its own registered reload configs, so `hotReloadable: false`
|
|
1393
|
+
was hot-reloaded identically to `true`.
|
|
1394
|
+
- The `packages/core/ADVANCED_FEATURES.md` example whose inline comment
|
|
1395
|
+
promised "Config is validated before init is called" — false on the
|
|
1396
|
+
retired ref, and the retired surface's only in-repo declaration site.
|
|
1397
|
+
|
|
1398
|
+
One-line fixes, per symbol. If you declared `configSchema` on a plugin:
|
|
1399
|
+
delete the field and parse your config at the plugin's own seam —
|
|
1400
|
+
`MyConfigSchema.parse(options)` in the plugin factory or constructor, the
|
|
1401
|
+
pattern `packages/rest` uses. If you imported `PluginConfigValidator` or
|
|
1402
|
+
`createPluginConfigValidator`: delete the import and hold your own
|
|
1403
|
+
`schema.parse` call; the compiler (TS2305) locates every such site. If you
|
|
1404
|
+
declared `hotReloadable`: delete the field — it never gated anything, and
|
|
1405
|
+
hot-reload participation remains governed solely by
|
|
1406
|
+
`HotReloadManager.registerReloadConfig`.
|
|
1407
|
+
|
|
1408
|
+
Re-declaring a kernel-owned config-validation surface is a fresh decision for
|
|
1409
|
+
the day ADR-0025's plugin distribution layer lands, with #11982's zero-caller
|
|
1410
|
+
measurement as its starting evidence.
|
|
1411
|
+
- add4360: fix(core): tell "service never registered" apart from "service failed to construct" on the async path (#13905)
|
|
1412
|
+
|
|
1413
|
+
`PluginLoader.getService` — reached through `Kernel.getServiceAsync` — answered two
|
|
1414
|
+
different facts with the same bare `Error`. "Nothing ever registered this service" and
|
|
1415
|
+
"the service is registered and could not be built" arrived at a caller as one
|
|
1416
|
+
indistinguishable rejection, separated only by message text.
|
|
1417
|
+
|
|
1418
|
+
That was load-bearing one layer out. `RestServer.computeExecCtx`'s kernel branch absorbs a
|
|
1419
|
+
failed `getServiceAsync('objectql')` and degrades to "no engine is wired", and it must keep
|
|
1420
|
+
doing so — a kernel with no data plane is a supported configuration, declared by
|
|
1421
|
+
`rest-api-plugin.ts` as `optionalDependencies: ['com.objectstack.engine.objectql']`. So a
|
|
1422
|
+
multi-tenant host whose engine *failed to construct* reached the same resolver as "no
|
|
1423
|
+
engine is wired", degrading silently where it should have refused loudly. The branch could
|
|
1424
|
+
not be repaired from outside, because the fact it needed had been collapsed before it
|
|
1425
|
+
arrived.
|
|
1426
|
+
|
|
1427
|
+
The asynchronous path now carries the distinction the **synchronous** context accessor in
|
|
1428
|
+
`kernel.ts` has always drawn from the registry. `@objectstack/core` publishes exactly two
|
|
1429
|
+
new symbols for it:
|
|
1430
|
+
|
|
1431
|
+
- `isServiceNotRegisteredError(err)` — true only when nothing was ever registered under
|
|
1432
|
+
that name;
|
|
1433
|
+
- `SERVICE_NOT_REGISTERED_CODE` — the code the rejection carries.
|
|
1434
|
+
|
|
1435
|
+
The test is closed and its default is loud: exactly one rejection in `getService` means
|
|
1436
|
+
"never registered" and only that one is branded, so every other way it can fail — a factory
|
|
1437
|
+
that threw, a missing scope id, an unset loader context, a circular service dependency —
|
|
1438
|
+
stays unbranded, and a consumer that absorbs only the branded rejection is loud about
|
|
1439
|
+
everything else, including rejections added later.
|
|
1440
|
+
|
|
1441
|
+
⛔ Not message matching. Adding a second text classifier on a resolution path is the failure
|
|
1442
|
+
mode this change removes: reading "not found" off the async path once reported every
|
|
1443
|
+
missing service as `is async - use await` — the wrong fix, pointing at the wrong layer.
|
|
1444
|
+
|
|
1445
|
+
Nothing existing moves. The rejection keeps a byte-identical message and `name: 'Error'`;
|
|
1446
|
+
the only observable change is the two added own-properties.
|
|
1447
|
+
|
|
1448
|
+
### Patch Changes
|
|
1449
|
+
|
|
1450
|
+
- efb3513: fix(platform-objects,core): `sys_metadata_activation` ships tenant-less — drop the reserved organization column (#15024)
|
|
1451
|
+
|
|
1452
|
+
The ADR-0126 activation ledger records that **this environment** switched a
|
|
1453
|
+
packaged artifact off. That is deployment-level state, owned by no
|
|
1454
|
+
organization — so the table ships with no tenant column at all.
|
|
1455
|
+
|
|
1456
|
+
It briefly declared one: an `organization_id` marked "RESERVED", nullable, and
|
|
1457
|
+
written by nobody, held for a per-organization dimension ADR-0126 §5
|
|
1458
|
+
pre-charted. A reserved nullable tenant column is exactly the shape the
|
|
1459
|
+
total-organization-ownership record proposed in PR #14976 rules out, and this
|
|
1460
|
+
one had no reader either. **This is a plain removal, not a migration:** the
|
|
1461
|
+
table landed after the 17.2.0 tag, so no released version ever carried the
|
|
1462
|
+
column and no deployment has data in it. Should a per-organization dimension
|
|
1463
|
+
ever be wanted, it returns as a separate org-owned object — never as a column
|
|
1464
|
+
on this ledger.
|
|
1465
|
+
|
|
1466
|
+
What changed:
|
|
1467
|
+
|
|
1468
|
+
- **`sys_metadata_activation` declares `systemFields: { tenant: false }`** and
|
|
1469
|
+
no longer declares the column. Both halves are needed: the tenant anchor is
|
|
1470
|
+
INJECTED at registration, so deleting the field alone would have left the
|
|
1471
|
+
column exactly where it was. ⚠️ Deliberately NOT `tenancy: { enabled: false }`
|
|
1472
|
+
— that key is the ADR-0066 D2 platform-global *posture*, which the sibling
|
|
1473
|
+
`sys_sso_provider` uses for the opposite shape (a table that KEEPS its tenant
|
|
1474
|
+
column and needs the wall over it stood down). Here there is no column to
|
|
1475
|
+
wall. Both spellings reach `plugin-security`'s `tenancyDisabled`, which is
|
|
1476
|
+
required rather than incidental: a Layer 0 wall composing an equality on a
|
|
1477
|
+
column the table does not have denies every row.
|
|
1478
|
+
- **The declared unique index states `unique: 'global'`** over
|
|
1479
|
+
`(metadata_type, name)` instead of `'organization'`. ⚠️ The materialized DDL
|
|
1480
|
+
is unchanged: `normalizeDeclaredIndex` prepends the NULL-safe tenant key part
|
|
1481
|
+
only when the table HAS a tenant column, so `'organization'` already degraded
|
|
1482
|
+
to exactly these two columns. What changes is that the declaration now states
|
|
1483
|
+
the boundary it actually gets, rather than claiming a per-organization one
|
|
1484
|
+
that does not exist. Still explicit rather than bare `unique: true`, which
|
|
1485
|
+
lint `unique/unscoped-declared-index` warns on and protocol 18 rejects.
|
|
1486
|
+
- **`ObjectStoreMetadataActivationStore` drops its NULL filter and its
|
|
1487
|
+
org-row skip.** `list()` is now every activation row of its type, scoped by
|
|
1488
|
+
the `metadata_type` discriminator alone, and `setActive` takes the single row
|
|
1489
|
+
its keyed read returns instead of picking the NULL-organization one out of
|
|
1490
|
+
the result. Both guarded a column that no longer exists; the declared unique
|
|
1491
|
+
index over the two columns the lookup keys on is what makes that read
|
|
1492
|
+
single-valued. `ObjectStoreFlowActivationStore` and
|
|
1493
|
+
`ObjectStoreActionActivationStore` inherit the change.
|
|
1494
|
+
|
|
1495
|
+
Unchanged, and pinned: the operator gate on activation writes under walled
|
|
1496
|
+
postures (ADR-0126 D3), the `execute()`-time flow consult and the dispatch-time
|
|
1497
|
+
action consult, "absence of a row means ACTIVE", re-enabling UPDATES the row
|
|
1498
|
+
rather than deleting it, and a driver `0` reading as false. The pins that
|
|
1499
|
+
asserted the reserved column and the org-row skip are rewritten to pin the
|
|
1500
|
+
column's ABSENCE rather than deleted — including at the injection authority
|
|
1501
|
+
(`resolveInjectedSystemColumns`, which decides whether the column exists) and
|
|
1502
|
+
in a real booted stack, where the row's key set is a reading of the physical
|
|
1503
|
+
table.
|
|
1504
|
+
- e27583e: docs(core): the `AuthzStoreUnavailableError` brand doc states the measured `structuredClone` behaviour instead of claiming survival (#14006)
|
|
1505
|
+
|
|
1506
|
+
Documentation only — no runtime change, no type change, no accept/reject
|
|
1507
|
+
behaviour moves. It ships as a patch because the docblock is a **published
|
|
1508
|
+
byte**: `tsup`'s declaration rollup carries it into `dist/index.d.ts` and
|
|
1509
|
+
`dist/index.d.cts`, so it is what a consumer reads on hover.
|
|
1510
|
+
|
|
1511
|
+
The brand's docblock justified the string-keyed own property with two reasons
|
|
1512
|
+
joined by an `and`, of which only the second was true:
|
|
1513
|
+
|
|
1514
|
+
> A string-keyed own property (not a `Symbol.for` registry key) so it survives
|
|
1515
|
+
> `structuredClone`, and so a duplicated copy of this module still brands
|
|
1516
|
+
> identically.
|
|
1517
|
+
|
|
1518
|
+
Measured on Node 22.22.2: the structured-clone algorithm gives `Error` a
|
|
1519
|
+
dedicated serialization carrying `message`, `stack` and `cause` only, and drops
|
|
1520
|
+
every other own property — the brand, the ADR-0112 `code`, `status` and
|
|
1521
|
+
`object` alike (a subclass's own `name` returns as `'Error'`). The
|
|
1522
|
+
plain-object control is the half that proves it: `{ __brand: true, code: 'C' }`
|
|
1523
|
+
keeps **both** keys through the same call, so the loss is specific to `Error`,
|
|
1524
|
+
not general to `structuredClone`.
|
|
1525
|
+
|
|
1526
|
+
The property and the reason that actually earns it are kept — a duplicated copy
|
|
1527
|
+
of the module still brands identically, which is exactly what `instanceof`
|
|
1528
|
+
cannot do across two installed copies of `@objectstack/core`. The false half is
|
|
1529
|
+
replaced by the measured behaviour, carrying the reproducible script and the
|
|
1530
|
+
Node version rather than a second unsourced assertion, and phrased to match
|
|
1531
|
+
what `service-not-registered.ts` already records for its own brand (one
|
|
1532
|
+
phrasing across the two modules, not two).
|
|
1533
|
+
|
|
1534
|
+
⛔ The clone gap is deliberately NOT "fixed" with a `toJSON` or a custom
|
|
1535
|
+
serialization: no call site crosses a clone boundary today
|
|
1536
|
+
(`rethrowAuthzStoreUnavailable` on the rest rethrow paths,
|
|
1537
|
+
`isAuthzStoreUnavailableError` inside service `catch` blocks — all in-process),
|
|
1538
|
+
and adding one would widen the module's surface with nothing pulling on it. The
|
|
1539
|
+
docblock instead names the trap the false claim invited: branching on the brand
|
|
1540
|
+
across a worker or `postMessage` boundary would answer `false` and fail OPEN on
|
|
1541
|
+
a security path.
|
|
1542
|
+
- 983edf1: fix(core): `autoRestart` now fires for a health check that throws or times out, not only for one that returns a failure (#11852)
|
|
1543
|
+
|
|
1544
|
+
`PluginHealthMonitor.performHealthCheck` reaches its failure handling by two
|
|
1545
|
+
disjoint routes, and only one of them could ever restart the plugin.
|
|
1546
|
+
|
|
1547
|
+
A check that **returned** a failure (`false` or `{ status: 'unhealthy' }`)
|
|
1548
|
+
incremented `failureCounters`, cleared `successCounters`, and — once
|
|
1549
|
+
`failureThreshold` consecutive failures accumulated — consulted `autoRestart`
|
|
1550
|
+
and restarted the plugin. A check that **threw** took a separate `catch` block
|
|
1551
|
+
that incremented `failureCounters` and stopped there: it never cleared
|
|
1552
|
+
`successCounters` and never read `autoRestart`. Because `raceCheckTimeout`
|
|
1553
|
+
rejects rather than resolving, every `timeout` overrun lands in that `catch`,
|
|
1554
|
+
so a plugin that hung was marked `failed` and never restarted no matter how
|
|
1555
|
+
many rounds passed or what `autoRestart` said. The severer of the two failure
|
|
1556
|
+
modes was the one that could not trigger recovery.
|
|
1557
|
+
|
|
1558
|
+
Both routes now funnel into one `recordFailedRound` step that owns the
|
|
1559
|
+
counters, the `failureThreshold` comparison and the `autoRestart` decision, so
|
|
1560
|
+
a thrown or timed-out check is restart-eligible on exactly the same terms as a
|
|
1561
|
+
returned failure.
|
|
1562
|
+
|
|
1563
|
+
The per-route *status* label is deliberately unchanged: a throw is still the
|
|
1564
|
+
separate `failed` status applied immediately with no threshold, as
|
|
1565
|
+
`content/docs/protocol/kernel/lifecycle.mdx` documents. Only the counters and
|
|
1566
|
+
the restart decision are shared — those are what `failureThreshold` and
|
|
1567
|
+
`autoRestart` declare, and neither names a route.
|
|
1568
|
+
- 8120808: feat(tooling): `@objectstack/core` declares a `typecheck` script, and its test and examples layers enter the ratchet (#14613)
|
|
1569
|
+
|
|
1570
|
+
`packages/core/package.json` declared exactly `build`, `test` and `test:watch`.
|
|
1571
|
+
Around twenty sibling packages declare `typecheck`, and `turbo run typecheck`
|
|
1572
|
+
selects only packages that declare the task — so the lint workflow's typecheck
|
|
1573
|
+
job had no way to reach this package, and `pnpm --filter @objectstack/core
|
|
1574
|
+
typecheck` failed with `ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT` for anyone who tried
|
|
1575
|
+
it. The package's types ship anyway: `build` emits a 233 KB `dist/index.d.ts`,
|
|
1576
|
+
and rest, runtime, mcp, services and plugins all import it.
|
|
1577
|
+
|
|
1578
|
+
The state was tracked but not runnable. `check:type-check-coverage` carried
|
|
1579
|
+
`@objectstack/core` as a DEBT entry of 98 and had already re-measured it once
|
|
1580
|
+
(91 to 98), so nothing was invisible — but a ledger only the gate can read is
|
|
1581
|
+
not something a contributor working in the package can run, which is how a
|
|
1582
|
+
dispatched task came to assume the script existed.
|
|
1583
|
+
|
|
1584
|
+
**Measured at `84b8190ae`, dependency closure built first.** The undivided
|
|
1585
|
+
program (`tsc --noEmit -p tsconfig.json`, exactly as the DEBT entry measured it)
|
|
1586
|
+
reports 98 errors across 12 files, and every one of the 12 is a `.test.ts`. The
|
|
1587
|
+
same program restricted to the 63 non-test source files reports **zero**. So the
|
|
1588
|
+
build layer graduated as it stood, and the 98 did not have to be repaired before
|
|
1589
|
+
the script could exist.
|
|
1590
|
+
|
|
1591
|
+
**94 of the 98 were the check, not the code.** The repair is the split this
|
|
1592
|
+
repo already runs for `spec`, `rest`, `objectql` and `client`: `tsconfig.json`
|
|
1593
|
+
stays the build config and excludes the test layer; a new `tsconfig.test.json`
|
|
1594
|
+
compiles that layer under the module semantics vitest actually executes it with
|
|
1595
|
+
(`module: esnext`, `moduleResolution: bundler`), which retires 22 x TS2835, the
|
|
1596
|
+
TS2347 beside them and the share of 71 x TS7006 they cascade into — an import
|
|
1597
|
+
that does not resolve makes every symbol it names `any`. **No test file was
|
|
1598
|
+
edited.** Strictness is inherited and untouched. The residue is 4 errors over 4
|
|
1599
|
+
files, held per file and per signature in `test-typecheck-debt.json`, EXACT and
|
|
1600
|
+
shrink-only.
|
|
1601
|
+
|
|
1602
|
+
**The `examples/` half was found by the new script, not by the card.** Declaring
|
|
1603
|
+
`typecheck` flips the package from COVERED-BY-LEDGER to COVERED-BY-SCRIPT, and
|
|
1604
|
+
`check:type-check-coverage`'s SOURCES_COVERED invariant immediately reported
|
|
1605
|
+
`packages/core/examples` — 2 non-test source files in no tsc program at all.
|
|
1606
|
+
Neither had ever compiled: `kernel-features-example.ts` imported `../index.js`
|
|
1607
|
+
(above the package root, never existed) and `phase2-integration.ts` imported
|
|
1608
|
+
`@objectstack/core`, i.e. this package self-referencing by a name it declares in
|
|
1609
|
+
no dependency block. Collapsing that cascade exposed rather than removed errors,
|
|
1610
|
+
12 to 29, all of them real and none of them new: 20 reads of `ObjectKernel`'s
|
|
1611
|
+
**private** `logger`; four members of the security scan result that do not exist
|
|
1612
|
+
(`passed`, `score`, `summary.critical`, `summary.high`, where the type carries
|
|
1613
|
+
`status` and per-severity counts); and two config literals passing the unparsed
|
|
1614
|
+
shapes where `PluginHealthMonitor.registerPlugin` and
|
|
1615
|
+
`HotReloadManager.registerPlugin` are declared over the `Parsed` ones. That last
|
|
1616
|
+
pair is retirement drift — this file was edited by two retirements (restart keys,
|
|
1617
|
+
`watchPatterns`) while no tsc program could check the result. Every correction is
|
|
1618
|
+
pinned to this package's own signatures; `packages/spec` was not touched.
|
|
1619
|
+
|
|
1620
|
+
`packages/core` therefore leaves the DEBT ledger: the coverage gate now reads
|
|
1621
|
+
70/79 packages type-checked with 9 ledgered, where it read 68/78 with 10.
|
|
1622
|
+
- f658793: Restore the #10096 standing invariant (「浏览器可达的 spec 导出面必须
|
|
1623
|
+
schema-free」) for `@objectstack/core`'s plural→singular store-key fold.
|
|
1624
|
+
|
|
1625
|
+
`@objectstack/spec`: the `defineStack()` manifest-collection vocabulary
|
|
1626
|
+
(`PLURAL_TO_SINGULAR`, `SINGULAR_TO_PLURAL`, `pluralToSingular`,
|
|
1627
|
+
`singularToPlural`) moved to a schema-free module and is now ALSO exported
|
|
1628
|
+
from the sanctioned schema-free entry `@objectstack/spec/meta-spelling`
|
|
1629
|
+
(widened per the #10096 ruling's reference pattern). `@objectstack/spec/shared`
|
|
1630
|
+
keeps the same four symbols as re-exports — no consumer-visible removal. The
|
|
1631
|
+
manifest map and `META_URL_TO_SINGULAR` remain deliberately distinct contracts
|
|
1632
|
+
(#8424).
|
|
1633
|
+
|
|
1634
|
+
`@objectstack/core`: `canonicalMetadataServiceType`'s one value import moves
|
|
1635
|
+
from `@objectstack/spec/shared` to `@objectstack/spec/meta-spelling`, so
|
|
1636
|
+
browser consumers of `@objectstack/core` (every `@objectstack/client` bundle)
|
|
1637
|
+
no longer link the zod schema closure through the store-key fold.
|
|
1638
|
+
- 0a8ebf3: Scope the legacy platform-admin deprecation pointer to walled tenancy postures
|
|
1639
|
+
|
|
1640
|
+
The request-side notice that tells an operator their unscoped `admin_full_access`
|
|
1641
|
+
grant row is the OLD anchor — "it is removed in a later release", "re-anchor this
|
|
1642
|
+
deployment by declaring its administrators in configuration" — was emitted without
|
|
1643
|
+
regard to the deployment's tenancy posture, so it fired on `single` rigs too.
|
|
1644
|
+
|
|
1645
|
+
`single` is the DEFAULT posture, and on a `single` rig that row is not legacy at
|
|
1646
|
+
all: the boot-time `bootstrapPlatformAdmin` mints it to promote the first human
|
|
1647
|
+
user, and that promotion is ruled correct and unchanged. Such a deployment was
|
|
1648
|
+
therefore being told, once per process, to migrate off an anchor that is not
|
|
1649
|
+
scheduled to go away, toward a variable its own promotion is pinned never to read.
|
|
1650
|
+
|
|
1651
|
+
The pointer is now gated on `postureEnforcesWall(resolveTenancyPosture())`, the
|
|
1652
|
+
same predicate and the same source the boot-side detector already reads, so the
|
|
1653
|
+
migration window's loudness is scoped to the walled postures actually in it.
|
|
1654
|
+
Walled rigs are unaffected and still receive the notice.
|
|
1655
|
+
|
|
1656
|
+
⛔ Standing is not touched: this is a log-line trigger, not access control. Every
|
|
1657
|
+
deployment resolves exactly the `PLATFORM_ADMIN` it resolved before.
|
|
1658
|
+
- 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.
|
|
1659
|
+
- 2d5cee3: docs(core,service-cluster): retire the two docblocks left stale by `IPubSub`'s corrected delivery guarantee (#12836)
|
|
1660
|
+
|
|
1661
|
+
#12651 corrected `IPubSub`'s contract docblock: delivery is whatever the
|
|
1662
|
+
configured driver declares, no shipped driver exceeds at-most-once, a missed
|
|
1663
|
+
message is EXPECTED, and handlers must be idempotent **and** tolerate loss.
|
|
1664
|
+
Two docblocks elsewhere still described the world before that correction.
|
|
1665
|
+
|
|
1666
|
+
**`@objectstack/core` — `security/authz-invalidation-channel.ts`.** It carried a
|
|
1667
|
+
paragraph asserting, in the present tense, that the interface docblock "still
|
|
1668
|
+
says" *At-least-once delivery*, and that repairing it was a `packages/spec`
|
|
1669
|
+
change filed separately. That filing was #12651 and it has landed, so the
|
|
1670
|
+
paragraph is now false rather than merely stale — it sends the next reader
|
|
1671
|
+
looking for a live disagreement between the interface and the drivers that no
|
|
1672
|
+
longer exists. Replaced with a plain pointer to the interface docblock.
|
|
1673
|
+
Everything else in that docblock is unchanged: the at-most-once reasoning, the
|
|
1674
|
+
TTL-is-the-bound rule, and the best-effort-at-the-publish-site note all still
|
|
1675
|
+
hold.
|
|
1676
|
+
|
|
1677
|
+
**`@objectstack/service-cluster` — `memory/pubsub.ts`.** The line "At-least-once
|
|
1678
|
+
semantics held vacuously (a single in-process delivery)" was wrong on its own
|
|
1679
|
+
terms even before #12651: the same docblock states that handler errors are
|
|
1680
|
+
swallowed and logged via `onError`, so a handler that throws loses the message
|
|
1681
|
+
with no retry and no persistence. That is not at-least-once in any sense, and
|
|
1682
|
+
"vacuously" does not save it. Replaced with the honest statement — one
|
|
1683
|
+
synchronous in-process delivery attempt per subscriber, no persistence, no
|
|
1684
|
+
retry, no replay.
|
|
1685
|
+
|
|
1686
|
+
Prose only. No behaviour change, and no test changed.
|
|
1687
|
+
- a17da05: fix(core): only a backend fault populates `resolveLocalizationContext`'s failure memo (#11877)
|
|
1688
|
+
|
|
1689
|
+
`resolveLocalizationContext` memoizes an outcome for 30s whenever the read
|
|
1690
|
+
"failed" (#10221 — so a repeatedly-failing `sys_setting` query does not re-run,
|
|
1691
|
+
and the driver does not re-log it, on every request). The write condition was
|
|
1692
|
+
wider than the cache's own docblock: six legs set the flag and only **one** of
|
|
1693
|
+
them is the backend fault the docblock describes (the direct `ql.find` throw).
|
|
1694
|
+
The other five are the **settings service refusing** — a thrown `getMany`, each
|
|
1695
|
+
of the three older per-key `get`s, and the whole-block "service unavailable"
|
|
1696
|
+
handler.
|
|
1697
|
+
|
|
1698
|
+
Those five legs are reachable inside the settings engine's **bind window**
|
|
1699
|
+
(`SettingsService.getMany` refuses all-or-nothing for a `localization`
|
|
1700
|
+
namespace whose manifest is not yet registered), so:
|
|
1701
|
+
|
|
1702
|
+
- A caller that deliberately re-reads **after** the bind — the #11580 stdio
|
|
1703
|
+
repair re-resolves at `kernel:bootstrapped` for exactly this reason — was
|
|
1704
|
+
answered from the memo taken **inside** the window for up to 30s. The
|
|
1705
|
+
correction silently did not happen, with nothing in the output saying so.
|
|
1706
|
+
- A settings refusal standing alongside a perfectly **successful** direct read
|
|
1707
|
+
memoized that successful value — the staleness the docblock forbids outright
|
|
1708
|
+
and that `analytics-timezone.dogfood.test.ts` (#1982/#2018) exists to catch.
|
|
1709
|
+
|
|
1710
|
+
The memo is now written only for the direct-read fault. **#10221's protection
|
|
1711
|
+
is unchanged for the legs it was built for**: its environment (table not
|
|
1712
|
+
migrated yet) still memoizes, because the direct read throws there whether or
|
|
1713
|
+
not a settings refusal stands in front of it — pinned in both directions. And
|
|
1714
|
+
nothing is lost on the narrowed legs: those refusals throw out of an in-memory
|
|
1715
|
+
registry check *before* any query and *before* any log line, so memoizing them
|
|
1716
|
+
suppressed neither.
|
|
1717
|
+
|
|
1718
|
+
No signature, export or accepted-input change — the flag is internal to the
|
|
1719
|
+
module.
|
|
1720
|
+
- 7c41693: fix(core,plugin-auth,plugin-security): every `OS_PLATFORM_OWNER_EMAIL` reader asks the ONE list-aware parser (#13147)
|
|
1721
|
+
|
|
1722
|
+
`OS_PLATFORM_OWNER_EMAIL` accepts one address **or a comma-separated list** of
|
|
1723
|
+
them (#11663 Choice 2B). The list parse landed in a single home
|
|
1724
|
+
(`@objectstack/core`'s `platform-admin.ts`) and the authorization derivation
|
|
1725
|
+
consumed it — but every other reader kept calling `resolvePlatformOwnerEmail()`,
|
|
1726
|
+
which returns the operator's value trimmed and otherwise verbatim, and kept
|
|
1727
|
+
treating that whole string as ONE address.
|
|
1728
|
+
|
|
1729
|
+
An operator who configured a list therefore entered a self-contradictory state:
|
|
1730
|
+
authorization recognised them as a platform administrator, while four separate
|
|
1731
|
+
capabilities silently did nothing. Every direction failed **closed** — no
|
|
1732
|
+
privilege escalation existed at any point — but a declared capability vanished
|
|
1733
|
+
with no error anywhere:
|
|
1734
|
+
|
|
1735
|
+
- `bootstrap-platform-admin` promoted **nobody**, logging "will be promoted when
|
|
1736
|
+
that account registers" on every boot forever;
|
|
1737
|
+
- the walled operator stamp (`plugin-auth`) stamped **no** list member verified,
|
|
1738
|
+
so the account it should have provisioned was then refused elevation as
|
|
1739
|
+
`walled_owner_not_verified`;
|
|
1740
|
+
- `isVerifiedPlatformOwnerSession` / `platform-owner-wall-bypass` let **nobody**
|
|
1741
|
+
across the Layer 0 organization wall — the largest of the affected surfaces;
|
|
1742
|
+
- the walled boot diagnostic printed the raw list in the slot where an operator
|
|
1743
|
+
reads one address, and its dev-seed silence clause never matched.
|
|
1744
|
+
|
|
1745
|
+
All six readers now ask the same parser. `@objectstack/core` gains
|
|
1746
|
+
`isConfiguredPlatformAdminEmail(email, config)` — the membership half of
|
|
1747
|
+
`matchesConfiguredPlatformAdmin`, spelled once and shared, for the readers that
|
|
1748
|
+
hold a bare address rather than a `sys_user` row (the elevation gate keeps its
|
|
1749
|
+
two halves apart so `walled_owner_not_registered` and `walled_owner_not_verified`
|
|
1750
|
+
stay distinct answers; the stamp is handed an email before any row exists; the
|
|
1751
|
+
wall takes a fast negative before spending a row read). `PlatformAdminEmailConfig`
|
|
1752
|
+
gains `declaredSpellings`, the entries as the operator typed them, so the by-email
|
|
1753
|
+
`sys_user` lookup and the boot diagnostic get the as-typed form **from the one
|
|
1754
|
+
parse** instead of splitting the raw value a second time.
|
|
1755
|
+
|
|
1756
|
+
Behaviour for a single declared address is unchanged, including the
|
|
1757
|
+
case-insensitive match and the verbatim-spelling store lookup. A **refused**
|
|
1758
|
+
list (Choice 2B fails the whole variable closed on one unparseable entry) now
|
|
1759
|
+
reaches these readers as "zero administrators", which is the same answer they
|
|
1760
|
+
already gave for an unset variable — never a silently narrower set.
|
|
1761
|
+
|
|
1762
|
+
Two readers deliberately keep reading the raw value: the walled-boot refusal and
|
|
1763
|
+
the verification-path probe guard in `auth-plugin.ts` both use it as a pure
|
|
1764
|
+
truthiness test ("did the operator declare anything at all?"), which is
|
|
1765
|
+
grammar-independent. A census pin now enumerates the raw readers across both
|
|
1766
|
+
plugin packages and fails on a seventh.
|
|
1767
|
+
- 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.
|
|
1768
|
+
- 556ebc1: docs(core): correct `Plugin.type`'s TSDoc enumeration — it omitted `objectql` (#13762)
|
|
1769
|
+
|
|
1770
|
+
`Plugin.type` is typed `string` in `@objectstack/core`, so its TSDoc is the only
|
|
1771
|
+
enumeration a plugin author reading the interface ever sees; nothing type-checks
|
|
1772
|
+
them against it. That comment listed seven values while the declared set is
|
|
1773
|
+
eight: `PluginSchema.type` in `@objectstack/spec` is
|
|
1774
|
+
`z.enum(['standard', ...CORE_PLUGIN_TYPES])`, and `CORE_PLUGIN_TYPES` carries
|
|
1775
|
+
`objectql` — the type `packages/objectql/src/plugin.ts` declares on the engine
|
|
1776
|
+
plugin essentially every runtime loads first.
|
|
1777
|
+
|
|
1778
|
+
The comment now lists all eight and names `CORE_PLUGIN_TYPES` as the
|
|
1779
|
+
authoritative set. The same omission in the hand-written
|
|
1780
|
+
`content/docs/plugins/anatomy.mdx` transcription of this interface is corrected
|
|
1781
|
+
in the same change.
|
|
1782
|
+
- f7b25c5: fix(core): `successThreshold` now binds from every status that records a failure, so a declared count above 2 stops being unreachable (#11955)
|
|
1783
|
+
|
|
1784
|
+
`PluginHealthMonitor` consulted `successThreshold` only while a plugin's status
|
|
1785
|
+
was `unhealthy` or `degraded`. The first success in a recovery wrote
|
|
1786
|
+
`recovering` — a status that gate did not name — so the **second** success took
|
|
1787
|
+
the outer `else` and went straight to `healthy` without the counter being read
|
|
1788
|
+
at all. `failed` was in neither set either, so a plugin whose check threw
|
|
1789
|
+
recovered on its **first** success.
|
|
1790
|
+
|
|
1791
|
+
The declared value was therefore capped in practice:
|
|
1792
|
+
|
|
1793
|
+
| Status when the successes start | Consecutive successes actually required |
|
|
1794
|
+
| :--- | :--- |
|
|
1795
|
+
| `unhealthy` / `degraded` | 2, whatever `successThreshold` said |
|
|
1796
|
+
| `failed` / `recovering` | 1, whatever `successThreshold` said |
|
|
1797
|
+
|
|
1798
|
+
A declared `successThreshold: 5` was indistinguishable from `2`. The default is
|
|
1799
|
+
`1`, which is exactly the value at which the defect is invisible — every
|
|
1800
|
+
declared value above it was the one that misbehaved.
|
|
1801
|
+
|
|
1802
|
+
The counter is now consulted on the way out of every status that records an
|
|
1803
|
+
observed failure — `degraded`, `unhealthy`, `failed` and `recovering` — so
|
|
1804
|
+
`successThreshold: N` requires N consecutive successes from each of them, as
|
|
1805
|
+
its declaration says ("Consecutive successes needed to mark healthy"). The gate
|
|
1806
|
+
is a map that is exhaustive over `PluginHealthStatus`, so a status added to the
|
|
1807
|
+
spec fails to compile until it is placed on one side or the other; that is what
|
|
1808
|
+
`recovering` slipped through before.
|
|
1809
|
+
|
|
1810
|
+
`healthy` and `unknown` still promote on the first success, deliberately: the
|
|
1811
|
+
count is declared as a **recovery** criterion ("Number of consecutive successes
|
|
1812
|
+
to recover from unhealthy state") and neither of those records a failure to
|
|
1813
|
+
recover from — `unknown` is the status `registerPlugin` writes before any check
|
|
1814
|
+
has run.
|
|
1815
|
+
|
|
1816
|
+
**Behaviour change, only for configs that declare `successThreshold` above 1.**
|
|
1817
|
+
At the default `1` every route is byte-for-byte what it was: one success has
|
|
1818
|
+
always been enough and still is. A plugin declaring a higher count now takes
|
|
1819
|
+
the number of consecutive successes it asked for before it is reported
|
|
1820
|
+
`healthy`, including after a `failed` round and after an `autoRestart`.
|
|
1821
|
+
|
|
1822
|
+
This also makes #11852's `successCounters` reset load-bearing. That fix cleared
|
|
1823
|
+
the counter on the thrown failure route, and could not be pinned: the counter's
|
|
1824
|
+
only read site was unreachable with a stale non-zero value, so any test would
|
|
1825
|
+
have passed for the wrong reason. With `failed` gated on the counter, a throw
|
|
1826
|
+
that interrupts a recovery now demonstrably starts the count over.
|
|
1827
|
+
- Updated dependencies [809d417]
|
|
1828
|
+
- Updated dependencies [387e231]
|
|
1829
|
+
- Updated dependencies [f794e4e]
|
|
1830
|
+
- Updated dependencies [cae2169]
|
|
1831
|
+
- Updated dependencies [b812a54]
|
|
1832
|
+
- Updated dependencies [2d4fa75]
|
|
1833
|
+
- Updated dependencies [0e4e51b]
|
|
1834
|
+
- Updated dependencies [e84bbf6]
|
|
1835
|
+
- Updated dependencies [effae80]
|
|
1836
|
+
- Updated dependencies [d62f990]
|
|
1837
|
+
- Updated dependencies [c45d8e6]
|
|
1838
|
+
- Updated dependencies [2e3e8c7]
|
|
1839
|
+
- Updated dependencies [e621291]
|
|
1840
|
+
- Updated dependencies [40a93b5]
|
|
1841
|
+
- Updated dependencies [101ad2c]
|
|
1842
|
+
- Updated dependencies [d5b330d]
|
|
1843
|
+
- Updated dependencies [dda969c]
|
|
1844
|
+
- Updated dependencies [1f45690]
|
|
1845
|
+
- Updated dependencies [277948f]
|
|
1846
|
+
- Updated dependencies [8bdd955]
|
|
1847
|
+
- Updated dependencies [f3bbbef]
|
|
1848
|
+
- Updated dependencies [4f24e9d]
|
|
1849
|
+
- Updated dependencies [6a180e4]
|
|
1850
|
+
- Updated dependencies [474242f]
|
|
1851
|
+
- Updated dependencies [63cd487]
|
|
1852
|
+
- Updated dependencies [bd4aa4e]
|
|
1853
|
+
- Updated dependencies [803eaab]
|
|
1854
|
+
- Updated dependencies [f8e8f03]
|
|
1855
|
+
- Updated dependencies [eae824e]
|
|
1856
|
+
- Updated dependencies [f6fa22c]
|
|
1857
|
+
- Updated dependencies [8a483b3]
|
|
1858
|
+
- Updated dependencies [97bcd99]
|
|
1859
|
+
- Updated dependencies [df59de0]
|
|
1860
|
+
- Updated dependencies [96e25a8]
|
|
1861
|
+
- Updated dependencies [f75a38a]
|
|
1862
|
+
- Updated dependencies [7a25e7d]
|
|
1863
|
+
- Updated dependencies [1fa05a6]
|
|
1864
|
+
- Updated dependencies [c85a265]
|
|
1865
|
+
- Updated dependencies [dcb10a5]
|
|
1866
|
+
- Updated dependencies [773a999]
|
|
1867
|
+
- Updated dependencies [35dffea]
|
|
1868
|
+
- Updated dependencies [776a098]
|
|
1869
|
+
- Updated dependencies [5060877]
|
|
1870
|
+
- Updated dependencies [4f6325d]
|
|
1871
|
+
- Updated dependencies [52954c0]
|
|
1872
|
+
- Updated dependencies [2aa8456]
|
|
1873
|
+
- Updated dependencies [93809a3]
|
|
1874
|
+
- Updated dependencies [7c0d0c3]
|
|
1875
|
+
- Updated dependencies [daae7aa]
|
|
1876
|
+
- Updated dependencies [8dc22d6]
|
|
1877
|
+
- Updated dependencies [279431e]
|
|
1878
|
+
- Updated dependencies [948dd6b]
|
|
1879
|
+
- Updated dependencies [3b4c56c]
|
|
1880
|
+
- Updated dependencies [ae8edd2]
|
|
1881
|
+
- Updated dependencies [e25403c]
|
|
1882
|
+
- Updated dependencies [a81aa9d]
|
|
1883
|
+
- Updated dependencies [64baa68]
|
|
1884
|
+
- Updated dependencies [9fa70d7]
|
|
1885
|
+
- Updated dependencies [09db64a]
|
|
1886
|
+
- Updated dependencies [92916e7]
|
|
1887
|
+
- Updated dependencies [a84f3ea]
|
|
1888
|
+
- Updated dependencies [f2eaae8]
|
|
1889
|
+
- Updated dependencies [56c093c]
|
|
1890
|
+
- Updated dependencies [c09451b]
|
|
1891
|
+
- Updated dependencies [ba64877]
|
|
1892
|
+
- Updated dependencies [7345308]
|
|
1893
|
+
- Updated dependencies [79b6a22]
|
|
1894
|
+
- Updated dependencies [30d96ab]
|
|
1895
|
+
- Updated dependencies [f658793]
|
|
1896
|
+
- Updated dependencies [c95ad19]
|
|
1897
|
+
- Updated dependencies [e58ea8b]
|
|
1898
|
+
- Updated dependencies [4a17645]
|
|
1899
|
+
- Updated dependencies [3795c5f]
|
|
1900
|
+
- Updated dependencies [8ab926b]
|
|
1901
|
+
- Updated dependencies [7317cf2]
|
|
1902
|
+
- Updated dependencies [e25e839]
|
|
1903
|
+
- Updated dependencies [5997207]
|
|
1904
|
+
- Updated dependencies [8b13cc8]
|
|
1905
|
+
- Updated dependencies [4a4a35d]
|
|
1906
|
+
- Updated dependencies [86e765a]
|
|
1907
|
+
- Updated dependencies [1d7e76a]
|
|
1908
|
+
- Updated dependencies [53dc739]
|
|
1909
|
+
- Updated dependencies [fd289be]
|
|
1910
|
+
- Updated dependencies [03bf7b1]
|
|
1911
|
+
- Updated dependencies [f90e820]
|
|
1912
|
+
- Updated dependencies [18d816a]
|
|
1913
|
+
- Updated dependencies [e8bd715]
|
|
1914
|
+
- Updated dependencies [b91c351]
|
|
1915
|
+
- Updated dependencies [a28a3c0]
|
|
1916
|
+
- Updated dependencies [daeaaf9]
|
|
1917
|
+
- Updated dependencies [c459da6]
|
|
1918
|
+
- Updated dependencies [e914733]
|
|
1919
|
+
- Updated dependencies [f887e52]
|
|
1920
|
+
- Updated dependencies [881f8d8]
|
|
1921
|
+
- Updated dependencies [3bfa1e6]
|
|
1922
|
+
- Updated dependencies [901355c]
|
|
1923
|
+
- Updated dependencies [34ce8e7]
|
|
1924
|
+
- Updated dependencies [33681ea]
|
|
1925
|
+
- Updated dependencies [bfe13c8]
|
|
1926
|
+
- Updated dependencies [0fb3044]
|
|
1927
|
+
- Updated dependencies [4635f3e]
|
|
1928
|
+
- Updated dependencies [ee3595c]
|
|
1929
|
+
- Updated dependencies [b2eab95]
|
|
1930
|
+
- Updated dependencies [93940d4]
|
|
1931
|
+
- Updated dependencies [3a04b01]
|
|
1932
|
+
- Updated dependencies [45b9051]
|
|
1933
|
+
- Updated dependencies [b9e9227]
|
|
1934
|
+
- Updated dependencies [d395692]
|
|
1935
|
+
- Updated dependencies [5894d30]
|
|
1936
|
+
- Updated dependencies [a3765f6]
|
|
1937
|
+
- Updated dependencies [e22158f]
|
|
1938
|
+
- Updated dependencies [7404925]
|
|
1939
|
+
- Updated dependencies [0c2334f]
|
|
1940
|
+
- Updated dependencies [778c59f]
|
|
1941
|
+
- Updated dependencies [d2619fd]
|
|
1942
|
+
- Updated dependencies [6acb11a]
|
|
1943
|
+
- Updated dependencies [33c5fd3]
|
|
1944
|
+
- Updated dependencies [20b0fdb]
|
|
1945
|
+
- Updated dependencies [905019b]
|
|
1946
|
+
- Updated dependencies [a286411]
|
|
1947
|
+
- Updated dependencies [98c0d33]
|
|
1948
|
+
- Updated dependencies [368a82e]
|
|
1949
|
+
- Updated dependencies [a3d5724]
|
|
1950
|
+
- Updated dependencies [93ea19b]
|
|
1951
|
+
- Updated dependencies [9ee2dcf]
|
|
1952
|
+
- Updated dependencies [8cb96ec]
|
|
1953
|
+
- Updated dependencies [8f10a79]
|
|
1954
|
+
- Updated dependencies [6269a55]
|
|
1955
|
+
- Updated dependencies [22e5236]
|
|
1956
|
+
- Updated dependencies [0fb8760]
|
|
1957
|
+
- Updated dependencies [e5ce2ed]
|
|
1958
|
+
- Updated dependencies [be21955]
|
|
1959
|
+
- Updated dependencies [bc56e18]
|
|
1960
|
+
- Updated dependencies [be21955]
|
|
1961
|
+
- Updated dependencies [a9ee989]
|
|
1962
|
+
- Updated dependencies [4d0d944]
|
|
1963
|
+
- Updated dependencies [15d58db]
|
|
1964
|
+
- Updated dependencies [d63b014]
|
|
1965
|
+
- Updated dependencies [9abe4e4]
|
|
1966
|
+
- Updated dependencies [2cc7122]
|
|
1967
|
+
- Updated dependencies [50d6c92]
|
|
1968
|
+
- Updated dependencies [9e0ba21]
|
|
1969
|
+
- Updated dependencies [311433f]
|
|
1970
|
+
- Updated dependencies [3e5ad08]
|
|
1971
|
+
- Updated dependencies [9abe4e4]
|
|
1972
|
+
- Updated dependencies [b7131f3]
|
|
1973
|
+
- Updated dependencies [e5812fa]
|
|
1974
|
+
- Updated dependencies [7085f90]
|
|
1975
|
+
- Updated dependencies [dee4dd4]
|
|
1976
|
+
- Updated dependencies [ce7e497]
|
|
1977
|
+
- Updated dependencies [51ecb2f]
|
|
1978
|
+
- Updated dependencies [9086761]
|
|
1979
|
+
- Updated dependencies [42a117b]
|
|
1980
|
+
- Updated dependencies [1401ae7]
|
|
1981
|
+
- Updated dependencies [4297fe7]
|
|
1982
|
+
- Updated dependencies [e398863]
|
|
1983
|
+
- Updated dependencies [d16df74]
|
|
1984
|
+
- Updated dependencies [f11fc61]
|
|
1985
|
+
- Updated dependencies [e808890]
|
|
1986
|
+
- Updated dependencies [8f79379]
|
|
1987
|
+
- Updated dependencies [e6ca40e]
|
|
1988
|
+
- Updated dependencies [0c77ea4]
|
|
1989
|
+
- Updated dependencies [52954c0]
|
|
1990
|
+
- Updated dependencies [89eb997]
|
|
1991
|
+
- Updated dependencies [aa5994e]
|
|
1992
|
+
- Updated dependencies [be93457]
|
|
1993
|
+
- Updated dependencies [a65db76]
|
|
1994
|
+
- Updated dependencies [2cf5a96]
|
|
1995
|
+
- Updated dependencies [15eb2c9]
|
|
1996
|
+
- Updated dependencies [5691b07]
|
|
1997
|
+
- Updated dependencies [2a6122b]
|
|
1998
|
+
- Updated dependencies [225e769]
|
|
1999
|
+
- Updated dependencies [8af88dd]
|
|
2000
|
+
- Updated dependencies [fb5fbb8]
|
|
2001
|
+
- Updated dependencies [d7b3963]
|
|
2002
|
+
- Updated dependencies [b72db01]
|
|
2003
|
+
- Updated dependencies [dce5cd4]
|
|
2004
|
+
- Updated dependencies [177ebdc]
|
|
2005
|
+
- Updated dependencies [8d237b4]
|
|
2006
|
+
- Updated dependencies [2d2e6f0]
|
|
2007
|
+
- Updated dependencies [2d8dd8d]
|
|
2008
|
+
- Updated dependencies [22d573e]
|
|
2009
|
+
- Updated dependencies [b5a2398]
|
|
2010
|
+
- Updated dependencies [348860c]
|
|
2011
|
+
- Updated dependencies [5383fa6]
|
|
2012
|
+
- Updated dependencies [5b3ff63]
|
|
2013
|
+
- Updated dependencies [1a6a19c]
|
|
2014
|
+
- Updated dependencies [527e050]
|
|
2015
|
+
- Updated dependencies [dd33bf9]
|
|
2016
|
+
- Updated dependencies [4cb2a90]
|
|
2017
|
+
- Updated dependencies [74a7804]
|
|
2018
|
+
- Updated dependencies [53d3689]
|
|
2019
|
+
- Updated dependencies [b3a63d3]
|
|
2020
|
+
- Updated dependencies [033a34c]
|
|
2021
|
+
- Updated dependencies [4d25d22]
|
|
2022
|
+
- Updated dependencies [1ffee51]
|
|
2023
|
+
- Updated dependencies [5ae4303]
|
|
2024
|
+
- Updated dependencies [ece4dad]
|
|
2025
|
+
- Updated dependencies [e9b377e]
|
|
2026
|
+
- Updated dependencies [146f448]
|
|
2027
|
+
- Updated dependencies [735f5c7]
|
|
2028
|
+
- Updated dependencies [a7e18de]
|
|
2029
|
+
- Updated dependencies [366f895]
|
|
2030
|
+
- Updated dependencies [dc75ba8]
|
|
2031
|
+
- Updated dependencies [cce0aa9]
|
|
2032
|
+
- Updated dependencies [e764507]
|
|
2033
|
+
- Updated dependencies [cff17af]
|
|
2034
|
+
- Updated dependencies [39404f3]
|
|
2035
|
+
- Updated dependencies [ca1965f]
|
|
2036
|
+
- Updated dependencies [8619f95]
|
|
2037
|
+
- Updated dependencies [b706af9]
|
|
2038
|
+
- Updated dependencies [db8c288]
|
|
2039
|
+
- Updated dependencies [0e5fe7f]
|
|
2040
|
+
- Updated dependencies [fc9ba76]
|
|
2041
|
+
- Updated dependencies [0f94cc7]
|
|
2042
|
+
- Updated dependencies [a11c1a5]
|
|
2043
|
+
- Updated dependencies [71f9cd1]
|
|
2044
|
+
- Updated dependencies [ee17d86]
|
|
2045
|
+
- Updated dependencies [cdbd920]
|
|
2046
|
+
- Updated dependencies [18c432e]
|
|
2047
|
+
- Updated dependencies [3c418c4]
|
|
2048
|
+
- Updated dependencies [fa8715a]
|
|
2049
|
+
- Updated dependencies [a933ed7]
|
|
2050
|
+
- Updated dependencies [b3ca463]
|
|
2051
|
+
- Updated dependencies [a933ed7]
|
|
2052
|
+
- Updated dependencies [0d4a6a8]
|
|
2053
|
+
- Updated dependencies [518d5e5]
|
|
2054
|
+
- Updated dependencies [6643ba1]
|
|
2055
|
+
- Updated dependencies [eeba2ef]
|
|
2056
|
+
- Updated dependencies [ec4c4d2]
|
|
2057
|
+
- Updated dependencies [424f73c]
|
|
2058
|
+
- Updated dependencies [cccbe51]
|
|
2059
|
+
- Updated dependencies [a8d6b1d]
|
|
2060
|
+
- Updated dependencies [e4a7695]
|
|
2061
|
+
- Updated dependencies [87075b1]
|
|
2062
|
+
- Updated dependencies [fc58a99]
|
|
2063
|
+
- Updated dependencies [14cfc00]
|
|
2064
|
+
- Updated dependencies [1c6f7b4]
|
|
2065
|
+
- Updated dependencies [e854a53]
|
|
2066
|
+
- Updated dependencies [dfebfc8]
|
|
2067
|
+
- Updated dependencies [d028b37]
|
|
2068
|
+
- Updated dependencies [122ef38]
|
|
2069
|
+
- Updated dependencies [4a37870]
|
|
2070
|
+
- Updated dependencies [428f9b2]
|
|
2071
|
+
- Updated dependencies [aa7ff56]
|
|
2072
|
+
- Updated dependencies [c41b42e]
|
|
2073
|
+
- Updated dependencies [c4db311]
|
|
2074
|
+
- Updated dependencies [750fff5]
|
|
2075
|
+
- Updated dependencies [c19035e]
|
|
2076
|
+
- Updated dependencies [ececf7a]
|
|
2077
|
+
- Updated dependencies [d173125]
|
|
2078
|
+
- Updated dependencies [8eeca27]
|
|
2079
|
+
- Updated dependencies [8425c17]
|
|
2080
|
+
- Updated dependencies [a5ef1d8]
|
|
2081
|
+
- Updated dependencies [87ad30c]
|
|
2082
|
+
- Updated dependencies [772d5de]
|
|
2083
|
+
- Updated dependencies [ce80ec2]
|
|
2084
|
+
- Updated dependencies [b372318]
|
|
2085
|
+
- Updated dependencies [97a2263]
|
|
2086
|
+
- Updated dependencies [29d0676]
|
|
2087
|
+
- Updated dependencies [0169d49]
|
|
2088
|
+
- Updated dependencies [6bd3231]
|
|
2089
|
+
- Updated dependencies [d2b5ba8]
|
|
2090
|
+
- Updated dependencies [b799ac5]
|
|
2091
|
+
- Updated dependencies [8f74307]
|
|
2092
|
+
- Updated dependencies [d23dc08]
|
|
2093
|
+
- Updated dependencies [644ad50]
|
|
2094
|
+
- Updated dependencies [9735662]
|
|
2095
|
+
- Updated dependencies [4d5b4f8]
|
|
2096
|
+
- Updated dependencies [0da7cd2]
|
|
2097
|
+
- Updated dependencies [28a5c3e]
|
|
2098
|
+
- Updated dependencies [4bc18e5]
|
|
2099
|
+
- @objectstack/spec@17.3.0
|
|
2100
|
+
- @objectstack/types@17.3.0
|
|
2101
|
+
|
|
3
2102
|
## 17.2.0
|
|
4
2103
|
|
|
5
2104
|
### Minor Changes
|