@objectstack/types 17.1.0 → 17.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,1031 @@
1
1
  # @objectstack/types
2
2
 
3
+ ## 17.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6a180e4: fix(core,rest,services)!: a permission-store read failure now fails LOUD instead of resolving as an authenticated caller holding zero capabilities (#13279)
8
+
9
+ **BREAKING** runtime behaviour change on the shared authorization resolver,
10
+ shipped as `minor` under the repo's launch-window convention.
11
+
12
+ `resolveAuthzContext`'s per-read helper `tryFind` answered a THROWN read exactly
13
+ the way it answered an EMPTY one: `[]`. So an outage of the permission store
14
+ resolved as a well-formed context for an authenticated principal holding no
15
+ capabilities, and the package-management door answered
16
+ `403 FORBIDDEN` — "Reading packages requires the `studio.access` or
17
+ `setup.access` capability." That answer was measured byte-identical
18
+ (`JSON.stringify` equal, against a control that separates two answers which do
19
+ differ) to what a caller who genuinely holds nothing receives. An administrator
20
+ was told they lack a capability, during an outage of the store that holds the
21
+ capability.
22
+
23
+ Maintainer ruling 2026-08-30, verbatim 「第一批其余同意」: `tryFind` 区分「无行」
24
+ 与「读失败」,读失败 fail-loud —— 权限库不可达时不再解析为「已认证零能力」,而是
25
+ 响亮拒绝(与真实能力拒绝的 403 可区分)。
26
+
27
+ Second maintainer ruling the same day (第 5 场总监席决裁批 #9, verbatim 「同意」),
28
+ after implementing the first one showed that "the read failed" is two facts:
29
+ 采**选项 A** —— 把 `isMissingTableError` 从 `@objectstack/metadata` 迁至
30
+ `@objectstack/types`(core 已依赖),metadata 保留 re-export 兼容;`tryFind` 仅对
31
+ **未被判定为「表未 provision」**的读失败抛 `AuthzStoreUnavailableError`。
32
+
33
+ **What changed.** A permission-store read that is issued and throws now raises
34
+ `AuthzStoreUnavailableError`, which carries the EXISTING ADR-0112 wire code
35
+ `SERVICE_UNAVAILABLE` and status `503`. No code is added to the closed wire
36
+ vocabulary and no response envelope gains or loses a key — only which declared
37
+ code an outage selects. Doors that map thrown errors through
38
+ `resolveThrownHttpError` answer 503 with no per-door change.
39
+
40
+ **What did NOT change**, and is pinned:
41
+
42
+ - A reachable, genuinely EMPTY store (reads return no rows) still resolves to
43
+ zero capabilities.
44
+ - A genuine capability denial still answers `403 FORBIDDEN` with its message.
45
+ - An ABSENT engine (`ql` unwired, so no read is ever issued) still resolves to
46
+ an empty-but-valid envelope.
47
+ - Anonymous requests never reach the store, so an outage cannot make them loud.
48
+ - A REAL engine whose `sys_*` tables were never provisioned resolves to zero
49
+ capabilities, quietly — pinned to be byte-identical to the empty-store
50
+ envelope, in every dialect spelling and in the production wrapper shape where
51
+ the driver's phrase is on `cause` rather than the outer message.
52
+
53
+ **The boundary between the two kinds of read failure.** An earlier revision of
54
+ this changeset claimed "embedders without a data plane are unaffected". That
55
+ claim was too broad; it is retracted here, and the gap it named is now closed
56
+ rather than merely disclosed. A read also throws when the table was never
57
+ PROVISIONED — a real engine, wired and reachable, whose `sys_*` tables were
58
+ never created — and that is a supported deployment shape, not an outage. There
59
+ "zero capabilities" is the TRUE answer rather than a fabrication: nothing is
60
+ provisioned, so nothing was withheld. Only an UNREACHABLE store — the ruling's
61
+ own word 不可达 — leaves the capability set unknown, and only an unknown answer
62
+ may not be reported as a denial.
63
+
64
+ Treating the two alike was measured, not theorised: it turned four CI suites
65
+ red, all from `no such table` on `sys_user` / `sys_member` /
66
+ `sys_user_position` / `sys_user_permission_set`. Ordinary CRUD in
67
+ `@objectstack/client` answered `503`; batch validation errors that owe `400`
68
+ answered `503`, because authorization refused before validation ran; runtime
69
+ notifications answered `401` where authenticated callers must be served `200`;
70
+ and two `.integration.test.ts` noise guards reported that the driver and engine
71
+ diagnostics for `sys_position` stopped being emitted — the eager throw aborted
72
+ the resolution before that later read was ever issued, so a change made to stop
73
+ a failed read being silent had made two other channels silent.
74
+
75
+ `tryFind` therefore raises `AuthzStoreUnavailableError` only for a read failure
76
+ that is NOT positively identified as an unprovisioned table.
77
+
78
+ **`isMissingTableError` moved to `@objectstack/types`.** The classifier that
79
+ draws that boundary already existed and was already right — driver-code based
80
+ rather than prose-sniffing, documented so that "cannot say" never means "be
81
+ loud". It lived in `@objectstack/metadata`, which DEPENDS ON `@objectstack/core`,
82
+ so the resolver could not import it. Rather than keep a second copy of a
83
+ security-relevant predicate, the ruling relocated the one classifier to
84
+ `@objectstack/types` — the package core already depends on, and the repo's own
85
+ stated Home rule for a cross-package error predicate ("every consumer of the
86
+ question already depends on it, so adopting the predicate never adds an edge",
87
+ `packages/types/src/unique-violation.ts`). `@objectstack/metadata/errors` still
88
+ exports `isMissingTableError`, re-exported from the new home, so no consumer of
89
+ that published subpath changes.
90
+
91
+ Its sibling `isSchemaAlreadyExistsError` moved with it — the two are not two
92
+ modules but two signatures over one matcher, and separating them would have
93
+ meant re-rolling the matcher, which is the duplication the module exists to
94
+ prevent. Both are now exported from `@objectstack/types`; the metadata subpath
95
+ deliberately still publishes only `isMissingTableError`, which is the only one
96
+ anything imports through it.
97
+
98
+ ⚠️ **Signed-off risk, recorded because it is load-bearing.** Gating loudness on
99
+ a driver-error predicate was approved with its false-positive direction stated:
100
+ mis-reading a genuine outage as "table not provisioned" silently restores the
101
+ quiet 403 this change removes, with no thrown error and no other failing test.
102
+ That direction is accepted, not overlooked — the predicate keys on driver codes,
103
+ SQLSTATEs and errnos first, excludes the known superstring traps up front, and
104
+ returns `false` for anything it does not positively recognise, so an
105
+ unrecognised outage stays loud by default. The risk is written beside the
106
+ predicate in `resolve-authz-context.ts` and both directions are pinned by name
107
+ in `authz-store-unavailable.test.ts`. ⛔ Do not widen `isMissingTableError` to
108
+ make a first boot quieter: every widening moves outages into the quiet branch.
109
+
110
+ **All-transport, not just REST.** Every transport authorizing through
111
+ `resolveAuthzContext` inherits this. Six of the eight production transports
112
+ wrapped the call in a fail-closed `catch` that would have re-silenced the
113
+ outage — measured, not assumed: with the resolver loud but the nets untouched,
114
+ the package door answered `401`, i.e. the outage merely changed disguises. Those
115
+ `catch` blocks now re-raise via `isAuthzStoreUnavailableError` and keep their
116
+ previous behaviour for every other fault. The transport set is rebuilt from
117
+ source and audited for set equality on every test run, so a transport added
118
+ later cannot inherit the old silence unnoticed.
119
+
120
+ Callers that treat any throw from `resolveAuthzContext` as "anonymous" should
121
+ re-raise `isAuthzStoreUnavailableError(err)` instead: degrading it restores the
122
+ disguise this removes.
123
+
124
+ <!-- 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. -->
125
+ - 0fb3044: fix(types): load a declared ESM-only host package through `createHostImporter`, and split its failure kind (#14041)
126
+
127
+ The declared leg's finder is `hostRequire.resolve(pkg)` — a **CommonJS**
128
+ resolution. A host-app package publishing only an `import` condition
129
+ (`{"exports": {".": {"import": "./dist/index.js"}}}`, ordinary for pure-ESM
130
+ publishes outside this workspace) made that resolve throw
131
+ `ERR_PACKAGE_PATH_NOT_EXPORTED`, and the leg classified **every** resolver
132
+ throw as `declared-unresolvable`: the load hard-failed, worded as an INSTALL
133
+ problem — `pnpm install`, un-prune, rebuild — about an install that was fine.
134
+ Nothing the message prescribed could help.
135
+
136
+ **The finder.** When — and only when — `hostRequire.resolve` throws, the leg
137
+ now consults exactly one directory: `<hostRoot>/node_modules/<name>` (name
138
+ verified against the package's own manifest, then realpath'd, so its
139
+ transitive imports resolve from its real location exactly as on the succeeding
140
+ path). If that package's `exports` names an existing `import`-condition target
141
+ for the requested subpath, it is imported. The fallback is **strictly
142
+ tighter** than the CJS resolution it backs up — no `NODE_PATH`, no walk above
143
+ `hostRoot`, no bare `require`, and Node's invalid-segment refusal mirrored
144
+ before exports resolution (a subpath carrying `''`, `.`, `..` or
145
+ `node_modules` segments is refused exactly as both of Node's resolvers refuse
146
+ it, so a pattern key can never substitute a traversal span into its target) —
147
+ so it cannot reopen the #4719 declaration-gate hole and cannot bypass the
148
+ package encapsulation Node's resolvers enforce: a package reachable only
149
+ through a hoisted store or a parent directory stays refused, even though CJS
150
+ resolution can see it there, and a traversal specifier keeps the hard failure
151
+ it has today. And because the fallback runs only inside a catch that was a
152
+ hard failure before, no currently-succeeding load changes behaviour.
153
+
154
+ **The split.** When the fallback cannot help either, the failure kind is
155
+ decided by whether any install action could: a package absent from the host's
156
+ `node_modules`, or one whose manifest names a runtime target whose file is
157
+ missing (a dist never built, a partial publish), keeps `declared-unresolvable`
158
+ and the existing INSTALL wording — it is right for both. A package that is
159
+ installed and whose manifest names **no** runtime entry for the subpath under
160
+ either the `require` or the `import` conditions (a `types`-only or
161
+ `browser`-only publish, an unexported subpath) now fails as the new
162
+ `HostImportFailureKind` value **`declared-no-loadable-entry`**, with a message
163
+ about the package's own published shape — the remedy lives in the package,
164
+ and an operator is no longer sent to re-run `pnpm install` against a correct
165
+ install. The new error still carries `code: 'MODULE_NOT_FOUND'`, so every
166
+ existing caller's missing-vs-crashed classification is unchanged, and an
167
+ evaluation crash still propagates untouched with no kind.
168
+
169
+ Consumers that branch on `HostImportFailureKind` should add an arm for
170
+ `declared-no-loadable-entry`: a two-way branch written against the old
171
+ two-member union will fall into its else leg for the new kind, whose wording
172
+ ("declare it") is wrong for a package that is declared and installed.
173
+ - db8c288: fix(types): let `sendError`'s `extra` carry `declaredCode`, so a nested-envelope route can emit the ADR-0112 open channel (#11719)
174
+
175
+ `ApiErrorSchema` has declared `declaredCode` since #9106 — the open,
176
+ author-authored channel that carries a metadata app's own `.code` verbatim when
177
+ the spelling is not a member of the closed `code` vocabulary. ADR-0112's
178
+ 2026-08-17 amendment rules that demote **platform-wide**, and #9232 extended it
179
+ to the flat `/data` door, which emits the pair today.
180
+
181
+ The shared nested-envelope writer could not. `sendError`'s `extra` was typed
182
+ `Pick<ApiError, 'category' | 'httpStatus' | 'details' | 'requestId'>`, so
183
+ passing a demoted spelling was a **compile error** and every route answering the
184
+ nested envelope dropped it. Nothing invalid shipped — the closed `code` still
185
+ carried the member derived from the status — which is exactly what made the loss
186
+ silent and one-directional: the author's spelling gone, and a consumer told by
187
+ the ADR to read `declaredCode` finding nothing there. Declared-but-unemittable
188
+ is a `declared = enforced` gap, closed here at the one writer rather than per
189
+ module.
190
+
191
+ Additive: `declaredCode` joins the `Pick`. No existing call site changes, no
192
+ wire byte moves for any body already being emitted, and the contract's accept
193
+ set is untouched — the schema has always permitted the field.
194
+
195
+ ⛔ Presence still MEANS demotion, and the writer does not re-derive that. The
196
+ caller passes `demotedDeclaredCode(thrown)` (`@objectstack/types`), exactly as
197
+ the flat door's `thrownCodeFields` does; that helper answers `undefined` when
198
+ the producer's spelling is already the vocabulary member sitting in `code`, so a
199
+ registered refusal never carries two spellings of one fact. Vocabulary and
200
+ position stay two decisions (#9232).
201
+
202
+ Pinned in `response-envelope.test.ts` by driving the real pipeline — a
203
+ sandbox-shaped throw carrying a tenant-authored `.code` through
204
+ `resolveThrownHttpError` and `demotedDeclaredCode` — and by parsing the emitted
205
+ body with the real `ApiErrorSchema`, asserting the field is still on it *after*
206
+ the parse. `ApiErrorSchema` is a plain `z.object` that strips undeclared keys,
207
+ so a `.success` assertion alone would have passed against a schema declaring
208
+ nothing.
209
+ - 0e5fe7f: fix(types): let `sendError`'s `extra` carry `userMessage`, so a nested-envelope route can emit the #9934 user-facing channel (#12404)
210
+
211
+ `ApiErrorSchema` declares `userMessage` — the producer-side opt-in for "this
212
+ exact text is addressed to the END USER" (#9934; maintainer ruling 2026-08-19 on
213
+ objectui#5210, option 1), where **presence IS the marking** and a consumer that
214
+ sees the field renders it verbatim instead of substituting its generic string
215
+ (#3821 preserved by construction, for everything unmarked).
216
+
217
+ Two of the three doors already emit it: the flat `/data` door through
218
+ `withDeclaredUserMessage` (`@objectstack/rest`) and the dispatcher door through
219
+ `thrown.userMessage` (`@objectstack/runtime`). The shared nested-envelope writer
220
+ could not — `sendError`'s `extra` was typed
221
+ `Pick<ApiError, 'category' | 'httpStatus' | 'details' | 'requestId' | 'declaredCode'>`,
222
+ so passing the field was a **compile error** and every route answering the
223
+ nested envelope dropped it. Nothing invalid shipped, which is what made the loss
224
+ silent and one-directional: the author's deliberate, localized refusal text
225
+ gone, and a consumer told to read `userMessage` finding nothing there.
226
+ Declared-but-unemittable is a `declared = enforced` gap, closed here at the one
227
+ writer rather than per module.
228
+
229
+ Additive: `userMessage` joins the `Pick`. No existing call site changes, no wire
230
+ byte moves for any body already being emitted, and the contract's accept set is
231
+ untouched — the schema has always declared the field.
232
+
233
+ The channel is live on both ends, which is what makes this a repair rather than
234
+ a new declared-but-dead surface: a hook opts in at throw time (host-side, or a
235
+ metadata app's sandboxed body whose `e.userMessage` crosses the QuickJS boundary
236
+ through `SANDBOX_ERROR_PASSTHROUGH`), and `resolveThrownHttpError` already
237
+ carries it onto `ThrownHttpError` for every caller of the shared resolver.
238
+
239
+ ⛔ Unlike `declaredCode`, this field hands the caller **no invariant to
240
+ re-derive**. `declaredCode`'s presence means demotion, so its caller passes
241
+ `demotedDeclaredCode(thrown)`; `userMessage`'s presence means only that the
242
+ producer opted in, which `declaredUserMessage` has already decided (a non-empty
243
+ string, or nothing). The caller passes `thrown.userMessage` straight through,
244
+ exactly as the dispatcher door does. That difference is why `extra` stays an
245
+ explicit `Pick` rather than being derived from `ApiError`'s optional fields: a
246
+ derivation would admit every future optional on the day it lands, with nobody
247
+ asked what obligation the channel hands the caller — and these two fields needed
248
+ opposite answers to exactly that question.
249
+
250
+ Pinned in `response-envelope.test.ts` by driving the real pipeline — a hook
251
+ refusal shaped like the one `hook-refusal-user-facing-marking.dogfood.test.ts`
252
+ drives, through `resolveThrownHttpError` — and by parsing the emitted body with
253
+ the real `ApiErrorSchema`, asserting the field is still on it *after* the parse,
254
+ paired with a control showing an undeclared sibling being stripped from the same
255
+ body. `ApiErrorSchema` is a plain `z.object` that strips undeclared keys, so a
256
+ `.success` assertion alone would have passed against a schema declaring nothing.
257
+ A blank marking is pinned ABSENT: the writer never invents a marked message for
258
+ a producer that wrote none.
259
+ - 87ad30c: fix(types): `isMissingTableError` prefers the table a driver declared it targeted over the caller-supplied `readObject` — new `DRIVER_TARGETED_TABLE` / `declareTargetedTable` / `targetedTableOf` (#13438)
260
+
261
+ `minor` because the public entry gains three exports; the predicate's signature
262
+ `(error, readObject?)` is **unchanged**, and every existing caller compiles and
263
+ behaves as before unless the error it holds carries a declaration.
264
+
265
+ **The residual #13324 left behind.** `readObject` lets a caller say which table it
266
+ read, so a phrase naming a *different* relation no longer earns the benign "not
267
+ provisioned yet" verdict. But a caller names its **object**, and a driver compiles
268
+ the statement against the **physical** table — for a federated object (ADR-0015,
269
+ `external.remoteName`) two different names. A genuinely absent remote therefore
270
+ raised a phrase naming `legacy_orders` against a caller naming `crm_order`, and the
271
+ comparison read a real missing table as loud. The mapping lives on the driver
272
+ instance; no call site can fold it away.
273
+
274
+ **The channel (maintainer ruling 2026-09-01, option 2).** A driver that knows the
275
+ table it targeted declares it on the error it composes:
276
+
277
+ - `DRIVER_TARGETED_TABLE` — `Symbol.for('objectstack.driver.targetedTable')`, the
278
+ well-known key, from the global registry so a duplicated package resolves it;
279
+ - `declareTargetedTable(error, table)` — the producer's half: defines the name
280
+ **non-enumerable and non-writable** (invisible to `JSON.stringify`, `{ ...err }`,
281
+ `Object.keys`), first declaration wins, an empty or non-string name declares
282
+ nothing;
283
+ - `targetedTableOf(error)` — the reading half, `string | null`.
284
+
285
+ `isMissingTableError` now compares the phrase against the **declared** table at
286
+ any node of the `cause` chain that carries one — the nearest declaration to the
287
+ dialect phrase wins — and ignores the caller-supplied `readObject` from that node
288
+ down. Without a declaration the comparison is the #13324 one, byte-for-byte. The
289
+ callers stay as they are: `crm_order` is still what they pass, and they never
290
+ learn a federated object's remote name.
291
+
292
+ **Two consequences, both pinned.** A genuinely absent federated remote reads
293
+ benign again. And because a declaration is evidence the caller did not have, an
294
+ envelope whose phrase names a relation *other* than its declared table reads
295
+ **not benign even through the one-argument published form** — the #13324 verdict,
296
+ reached without the caller's help, in the direction the module docblock calls
297
+ cheap (one error line, never silent data loss). The #13324 narrowing itself does
298
+ not reopen: a different relation's error — a view over a dropped base, a join
299
+ target, a `sys_*` table hit inside the same statement — stays loud with the
300
+ declaration present, on every dialect fixture the existing pins carry.
301
+
302
+ `@objectstack/driver-sql` adopts the channel in the same release; the pattern is
303
+ one call at any future driver's envelope. `isSchemaAlreadyExistsError` is
304
+ untouched.
305
+ - 9735662: fix(security): walled postures elevate only the env-declared platform owner, never the first registrant (#11184, the framework leg of cloud#1509)
306
+
307
+ **BREAKING** for walled deployments (`OS_TENANCY_POSTURE=group` or
308
+ `isolated`), shipped as `minor` under the repo's launch-window convention for
309
+ breaking changes. Single-org deployments are byte-for-byte unchanged.
310
+
311
+ Measured defect (cloud#1509): on a walled multi-tenant SaaS with
312
+ `OS_TENANCY_POSTURE=isolated` and `OS_AUTH_MEMBERSHIP_POLICY=invite-only`, the
313
+ FIRST self-registrant received the cross-tenant `admin_full_access` grant
314
+ (`platform_admin`, `isPlatformAdmin: true`) and — because the default-org
315
+ bootstrap binds "the platform admin" — was merged into the deployment's
316
+ Default Organization as its owner. Whoever curls the public sign-up endpoint
317
+ first owned the platform.
318
+
319
+ Per the maintainer ruling of 2026-08-23 (verbatim:
320
+ 「1509 选择 env 指定 owner 邮箱」):
321
+
322
+ - **Walled postures: platform admin comes ONLY from the env-declared owner.**
323
+ `bootstrapPlatformAdmin` (plugin-security) no longer promotes the oldest
324
+ human user when the requested posture is walled; it promotes exactly the
325
+ account whose email matches the new `OS_PLATFORM_OWNER_EMAIL` variable
326
+ (case-insensitive, matched whenever that account registers — arrival order
327
+ is irrelevant). Self-registrants are never promoted and, since the shared
328
+ `ensureDefaultOrganization` helper binds only the platform admin, are never
329
+ auto-merged into the Default Organization either.
330
+ - **Fail-closed startup refusal.** A walled posture with no
331
+ `OS_PLATFORM_OWNER_EMAIL` declared refuses to boot from `AuthPlugin.init()`
332
+ with a message naming the variable — never a silent fallback to
333
+ first-registrant elevation. The elevation site itself also refuses
334
+ (`reason: 'walled_owner_email_undeclared'`, logged at `error`) as
335
+ defense-in-depth for compositions that reach the bootstrap without
336
+ plugin-auth (`os meta resync`, bare embeddings).
337
+ - **Single-org posture unchanged.** "First user is owner" stays as ruled
338
+ reasonable there; the new variable is never consulted under `single`.
339
+ - The requested posture (`resolveTenancyPosture()`) is deliberately the input,
340
+ so a walled-requested deployment running degraded
341
+ (`OS_ALLOW_DEGRADED_TENANCY=1`) still refuses first-registrant elevation.
342
+
343
+ Operator action for walled deployments: set `OS_PLATFORM_OWNER_EMAIL` to the
344
+ operator account's email address before upgrading. Deployments that already
345
+ hold a human platform admin are untouched (the bootstrap remains a no-op once
346
+ any human holds the cross-tenant grant); the variable governs installs that
347
+ have not yet minted their admin. `@objectstack/types` gains the
348
+ `resolvePlatformOwnerEmail()` resolver and the `PLATFORM_OWNER_EMAIL_ENV`
349
+ constant; the verify harness declares the owner email (defaulting to its dev
350
+ admin) for walled fixtures.
351
+
352
+ <!-- adr-0087: not-required (no-migration-prescription) nothing authorable is removed, renamed or narrowed: no spec key, no metadata spelling and no stored row changes shape, so there is nothing for `os migrate meta` to rewrite and no ledger entry to make. The prescription above is a deployment-environment requirement (declare an env var before boot), which the ADR-0087 ledger does not carry — the refusal itself names the variable at startup. -->
353
+ - 4d5b4f8: feat(auth): walled deployment's declared owner is email-verified at operator-provisioned creation (#12751)
354
+
355
+ On a **walled** deployment (`OS_TENANCY_POSTURE` in the wall-enforcing
356
+ family), the account whose email equals the declared platform owner
357
+ (`OS_PLATFORM_OWNER_EMAIL`) is stamped `emailVerified` **at creation** when
358
+ it comes into existence through an **operator provisioning path** — extending
359
+ the #11343 dev-boot seeded-admin precedent to production walled boots
360
+ (maintainer ruling 2026-08-28, cloud#1677: 「运营方创建即视为已验证」; the
361
+ trust anchor is the operator's env-var declaration plus the
362
+ operator-executed creation, not a mailbox round-trip; SMTP stays required
363
+ only for inviting others).
364
+
365
+ **Which creation paths qualify** (the [#11739] audience taxonomy, not a
366
+ second classification):
367
+
368
+ - the **bootstrap carve-out** — the very first account on a fresh install
369
+ (zero human users), the one self-serve creation a walled boot admits;
370
+ - **admin create-user / bulk import** (`method: 'admin'`) — an act only an
371
+ authenticated admin session can perform;
372
+ - **SCIM** (`method: 'scim'`) — provisioning executed by the
373
+ operator-registered directory.
374
+
375
+ **Never**: non-bootstrap self-registration (including an
376
+ invitation-admitted registration typing the owner address), provider-class
377
+ JIT (the IdP asserts its own `emailVerified` at insert), any non-owner
378
+ address, any unwalled posture, and a later email **update** to the owner
379
+ address (the stamp is staged at the admission gate and consumed once by the
380
+ `user.create` before-hook — a seam an update cannot traverse). Dev-boot
381
+ behaviour (#11343) is unchanged.
382
+
383
+ The `WALLED_OWNER_NO_VERIFICATION_PATH` boot warning now probes the owner
384
+ account's state: a fresh walled boot with no transport and no federated
385
+ sign-in is **silent** (the operator's own first-account creation arrives
386
+ verified — the case this closes), while an owner account that already
387
+ exists **unverified**, a populated store whose bootstrap window is spent,
388
+ and an unanswerable probe keep warning. A settled deployment whose owner is
389
+ verified stops re-warning on every boot.
390
+
391
+ `@objectstack/types` gains `isEmailVerifiedUserRow` — the [#11343]
392
+ fail-closed verified-representation allow-list, moved from
393
+ `plugin-security`'s private copy so the elevation gate and the boot
394
+ diagnostic read ONE resolution (`plugin-security` now consumes it; no
395
+ behaviour change there).
396
+
397
+ ### Patch Changes
398
+
399
+ - 101ad2c: fix(types): an aliased install (`"foo": "npm:bar@1"`) is now found by the host importer's ESM-only fallback
400
+
401
+ `createHostImporter`'s #14041 fallback finder verifies the one directory it
402
+ consults — `<hostRoot>/node_modules/<key>` — by matching that directory's
403
+ `package.json` `name` against the declared package name. An aliased install
404
+ fails that check by construction: `{ "dependencies": { "foo": "npm:bar@1" } }`
405
+ puts a manifest named `bar` at `node_modules/foo`. The finder answered
406
+ `absent`, and an ESM-only aliased package therefore kept the pre-#14041 INSTALL
407
+ wording — a confidently-wrong remedy sending an operator to run `pnpm install`
408
+ against an install that is already correct, on a declaration shape
409
+ `packageNameFromSpecifier`'s own documentation blesses.
410
+
411
+ The declaration is now parsed for the name it promises: `npm:bar@1`,
412
+ `npm:@acme/x@^2` and the aliased `workspace:bar@*` name the package installed
413
+ under the key, so that is the manifest name the finder expects there. An
414
+ aliased ESM-only package is rescued exactly as a plain one is, and an aliased
415
+ install publishing nothing loadable gets the message about the PACKAGE's own
416
+ shape instead of the INSTALL message.
417
+
418
+ ⚠️ The manifest-name check itself is NOT loosened — that check is what keeps
419
+ the fallback strictly tighter than the CJS resolution it backs up (#4719's
420
+ declaration gate, from the fallback side). What moved is the EXPECTATION, still
421
+ authored by the host and still read out of the host's own `package.json`: an
422
+ alias naming one package refuses a directory holding another, a non-aliased
423
+ declaration is unchanged, and a value that is not a bare package name — a
424
+ `workspace:` range, an alias carrying a subpath — yields no expectation to move
425
+ to, so the key stays and today's refusal is kept. `link:` and `file:` name a
426
+ LOCATION rather than a package, so no name is derivable from them at all; they
427
+ keep the key expectation, and with it the conservative direction the finder had
428
+ before.
429
+ - a81aa9d: fix(types): a DEMOTED `declaredCode` is withheld on a 5xx the producer did not declare (#12509, ADR-0112)
430
+
431
+ **Wire change, for undeclared server faults only.** When a 5xx has its prose
432
+ withheld, the producer's demoted `declaredCode` is now withheld with it —
433
+ but only when the fallback-to-500 picked that code up from a producer that
434
+ declared no HTTP answer. An author-declared code is untouched at every status.
435
+
436
+ FROM (`origin/main`, measured through the real routes):
437
+
438
+ ```
439
+ POST /api/v1/packages/publish → 500 {"error":{"code":"INTERNAL_ERROR",
440
+ "message":"Internal server error","declaredCode":"SQLITE_ERROR"}}
441
+ POST /api/v1/analytics/query → 500 {"error":{"code":"INTERNAL_ERROR",
442
+ "message":"Internal server error","httpStatus":500,"declaredCode":"42P01"}}
443
+ ```
444
+
445
+ TO:
446
+
447
+ ```
448
+ POST /api/v1/packages/publish → 500 {"error":{"code":"INTERNAL_ERROR",
449
+ "message":"Internal server error"}}
450
+ POST /api/v1/analytics/query → 500 {"error":{"code":"INTERNAL_ERROR",
451
+ "message":"Internal server error","httpStatus":500}}
452
+ ```
453
+
454
+ UNCHANGED — the author-authored channel the ADR-0112 amendment wrote
455
+ `declaredCode` for:
456
+
457
+ ```
458
+ { status: 503, code: 'ACME_LEDGER_OFFLINE' }
459
+ → 503 {"error":{"code":"SERVICE_UNAVAILABLE",…,"declaredCode":"ACME_LEDGER_OFFLINE"}}
460
+ ```
461
+
462
+ `SQLITE_ERROR` vs `42P01` names the backend, which is one of the two
463
+ disclosures the 5xx message withhold exists to prevent (the other,
464
+ identifiers, was already covered). Maintainer ruling 2026-08-27, option D.
465
+
466
+ **What a consumer must know.** A `declaredCode` on a 5xx now means the
467
+ producer declared that fault itself, which is a stronger guarantee than the
468
+ field carried before; nothing that was a *registered* code moves, and no 4xx
469
+ moves. A producer that spells a code but declares no status loses that code
470
+ on a 5xx — declare the status the refusal means and the spelling is kept.
471
+
472
+ The distinction lives in ONE place, `serverFaultProvenance`
473
+ (`packages/types/src/thrown-http-error.ts`), read by `demotedDeclaredCode` —
474
+ the read every door already makes — so all five emitting exits inherit it and
475
+ no registrar carries a variant. The prose axis of the same ruling (the
476
+ dispatcher door adopting the structural withhold for every declared 5xx
477
+ message) is #12281 and is deliberately not applied here.
478
+ - 56c093c: fix(driver-memory): enforce field-level `unique`, so a colliding write is refused instead of landing silently (#13197)
479
+
480
+ `InMemoryDriver` enforced **no uniqueness at all**. `create` was a
481
+ `table.push()` and `syncSchema` allocated an array, so a `unique: true` field
482
+ was declared-and-not-enforced — the ADR-0078 / Prime-Directive-#10 shape the
483
+ platform refuses everywhere else. A colliding write did not fail; it landed, and
484
+ a read returned both rows.
485
+
486
+ The motivating instance is the worst-shaped one. The engine's
487
+ `createWithAutonumberResync` re-seeds the counter and re-issues a record number
488
+ when the STORE rejects it as a duplicate, so on a store that rejected nothing
489
+ the whole branch was unreachable: an autonumber allocated out of process
490
+ duplicated an existing business identifier with **no error anywhere**. The
491
+ remedy's location was already ruled in-tree at that method — «uniqueness
492
+ enforcement in the driver, NOT a pre-issue existence probe here» — and this is
493
+ that remedy. Nothing in the new code knows what an autonumber is; the defect was
494
+ that the driver constrained nothing.
495
+
496
+ **The refusal** carries the ADR-0112 envelope the SQL family answers a conflict
497
+ with: `code: 'UNIQUE_VIOLATION'`, `status: 409`, no `[driver-memory]` prefix. So
498
+ a suite that swaps this driver for SQLite sees one envelope — the parity
499
+ `memory-filter-refusal-envelope.test.ts` already states for the filter family,
500
+ now held for the constraint family. It is checked before the row is written, so
501
+ a refused write leaves the table exactly as it found it, and `updateMany`
502
+ prepares and checks the whole batch before mutating any of it.
503
+
504
+ **The scoping is `driver-sql`'s, measured — not a simpler invention.** Read off
505
+ `uniqueIndexesFromFields` (ADR-0120 D1/D3) and reproduced arm for arm:
506
+ `unique: 'global'` is platform-wide; bare `true` and `'organization'` are
507
+ per-organization (bare `true` is the POSITIONAL spelling of `'organization'` at
508
+ FIELD level — reading it as `'global'` is the #4986 trap and would make two
509
+ organizations' identical values collide on a constraint neither can see); both
510
+ degrade to a single column when the object has no tenant column, and a `unique`
511
+ declaration on the tenant column itself stays single-column. NULL values stay
512
+ NULL-DISTINCT, exactly as under SQL `UNIQUE`. The D3 NULL-organization fold
513
+ needs no `'__global__'` token here — that sentinel is a SQL-expression artefact,
514
+ and a JavaScript key holds `null` directly.
515
+
516
+ **Not** widened into: object-level declared `indexes[]` (composite uniques),
517
+ primary keys, or row-level tenant isolation. This driver still refuses to boot
518
+ multi-tenant (#6915) and that guard is untouched.
519
+
520
+ `@objectstack/types` (`patch`): `isUniqueViolationError` now reads the
521
+ platform's own registered `UNIQUE_VIOLATION` code on the `code` channel. Not
522
+ cosmetic — a conflict that predicate does not recognise leaves the autonumber
523
+ resync unable to re-seed, so the counter stays warm and every following insert
524
+ collides too (#5495's PROBE3 storm), i.e. a silent duplicate traded for a
525
+ non-converging insert loop. It is a tautology rather than a widened heuristic
526
+ (the code already MEANS this condition), and no existing in-repo producer's
527
+ classification changes: `@objectstack/rest`'s own response body is the only
528
+ other site carrying that string, and it is downstream of the predicate.
529
+
530
+ **Grade.** `minor` for the driver, not `patch`: a write that previously
531
+ succeeded is now refused (`409`), which is an accept-set narrowing under the
532
+ repo's launch-window convention for breaking changes, and the package also gains
533
+ public exports (`UNIQUE_VIOLATION_CODE`, `uniqueConstraintsFromFields`,
534
+ `tenantFieldOf`, `uniqueKeyOf`, `assertNoUniqueViolation`,
535
+ `uniqueViolationError`). `patch` for `@objectstack/types`: no API added or
536
+ removed and no in-repo verdict changes — the limb exists to serve the new
537
+ producer. Fixtures that relied on duplicates landing on a declared-unique field
538
+ must stop declaring `unique`, or stop writing the duplicate; the repo's own
539
+ suites were measured and none did.
540
+ - bfe13c8: fix(types,cli): resolve host-declared packages through the `import` condition, and read the cluster registry instead of assuming it (#13330)
541
+
542
+ `createHostImporter`'s declared leg resolved with `hostRequire.resolve(pkg)` — a
543
+ **CommonJS** resolution, which answers the `require` condition. Every `tsup`
544
+ dual build publishes `{ "import": "./dist/index.js", "require": "./dist/index.cjs" }`,
545
+ so a package loaded through that leg evaluated as its **CommonJS** build while
546
+ the callers (`packages/cli` is `"type": "module"`) held the **ESM** build of the
547
+ same package. The process ended up with two instances of everything the loaded
548
+ package shares with its caller, each with its own module-scope state.
549
+
550
+ Measured consequence, on the shipped EE multi-node path (ADR-0018): `os serve`
551
+ loaded `@objectstack/service-cluster-redis` through this leg, the driver's
552
+ load-time `registerClusterDriver('redis', …)` ran against the CommonJS copy of
553
+ `@objectstack/service-cluster`, and the ESM `Runtime` read the ESM copy and
554
+ found nothing — `OS_CLUSTER_DRIVER=redis` died at `defineCluster()` with
555
+ `Cluster driver "redis" is not registered`, about a package that was installed,
556
+ declared and resolvable. Any module-scope registry crossing this seam had the
557
+ same defect; the cluster driver is the instance that shipped.
558
+
559
+ **The seam.** The declared leg now imports the entry the `import` condition
560
+ names. The host anchor is untouched — the CJS resolver still answers *where*
561
+ the package is, because no flagless Node API resolves a bare specifier against
562
+ an arbitrary parent; only the *condition* is re-decided, by reading that
563
+ package's own `exports` map. Deliberately narrow at the **resolution** level —
564
+ no load that works today resolves differently unless the package itself
565
+ publishes a valid, existing import-condition target: a package with no
566
+ `exports` map is untouched (CJS resolution already returned `main`), a package
567
+ publishing no import-condition target is untouched, and anything unreadable or
568
+ absent on disk falls back to the CJS-resolved path. That narrowness does not
569
+ extend to **evaluation**: a dual-published package whose `import` build exists
570
+ but throws while its `require` build works used to mask that break by silently
571
+ loading the CJS build, and now surfaces it — arguably the correct reading of a
572
+ broken published build, but a behaviour change, not a no-op.
573
+
574
+ **The reading.** A residual split is still possible above the seam — two
575
+ *physical* copies of one package are two instances in any module system, and no
576
+ resolver condition merges them — so `os serve` no longer assumes the driver
577
+ registered. `@objectstack/service-cluster` exports `listClusterDrivers()`, the
578
+ registry `defineCluster()` itself consults, and `serve` queries it after the
579
+ load. The silent `catch` is gone: a driver that loaded but stayed invisible, one
580
+ that could not be resolved, and one that resolved and then crashed now read as
581
+ three different diagnoses instead of arriving as `not registered` one line
582
+ later. An app on an older `@objectstack/service-cluster` has no accessor to
583
+ call; that case is silent — `serve` declines to claim either answer rather
584
+ than printing one.
585
+
586
+ No behaviour downstream of the diagnosis changed: an absent driver still reaches
587
+ `defineCluster()`'s documented error (`cluster.mdx` §8.1) rather than silently
588
+ downgrading to the in-memory cluster, and the only documented downgrade here —
589
+ a multi-node gate denial — is untouched.
590
+ - 22e5236: fix(types,runtime): log every 5xx at `error` level instead of answering it silently (#14310)
591
+
592
+ A 500 that leaves no server-side line is diagnosed from the browser or not at
593
+ all. Measured on `main`, through the real plugin and the real route handlers: a
594
+ plain `Error` thrown out of a dispatcher route answered `500 INTERNAL_ERROR`
595
+ with **zero** log records at any level — the only evidence was the client's
596
+ console and the response body. That is AGENTS.md "Route & surface ownership §3
597
+ — absence must be loud" inverted, and it is why a `/api/v1/packages` regression
598
+ stayed invisible for a week.
599
+
600
+ The reporting that already existed was not a substitute, for two independent
601
+ reasons:
602
+
603
+ - `ErrorReporter.captureException` defaults to `NoopErrorReporter`. A dev
604
+ server — the surface an operator actually watches — wires no APM, so the
605
+ capture was a no-op every time. A log line is the operator's floor; APM is
606
+ opt-in telemetry on top of it.
607
+ - It is fed by `res.__obsRecordedError`, which only the THROWN exit sets. A
608
+ route that catches its own fault and RETURNS a 5xx envelope — how every
609
+ `/packages` handler answers, via `deps.errorFromThrown` — recorded nothing,
610
+ so even a wired reporter never saw those.
611
+
612
+ **The rule now has one definition.** `logServerFault` (new, in
613
+ `@objectstack/types`) emits exactly one `error`-level record carrying method,
614
+ path, request id, the message and — where the door still holds the throw — the
615
+ stack. It shares a home with `resolveThrownHttpError` for the same reason that
616
+ rule was moved there in #8016: a rule two doors must agree on cannot live
617
+ inside one of them, because `@objectstack/runtime` depends on
618
+ `@objectstack/rest` and an import could only ever point one way.
619
+
620
+ Wired at each transport's single exit, so a fault costs one line and never two:
621
+
622
+ - `sendError` — the one writer for every nested-envelope error in the repo. The
623
+ REST direct-mount registrars (the `/api/v1/packages` door that mounts first
624
+ in production) become loud through it with no per-door call, so a door added
625
+ later cannot forget one.
626
+ - The dispatcher's thrown exit (`errorResponseBase`), its returned exit
627
+ (`sendResultBase`) and the AI-route mount that writes its own result.
628
+
629
+ `packages/rest`'s `/data` doors were already loud via `logUnexpectedRouteError`
630
+ and are untouched.
631
+
632
+ `error` level is load-bearing: the CLI's default is `warn` and `error` (40)
633
+ outranks `warn` (30), so the record clears `--log-level`'s default without
634
+ bypassing the level system. `--log-level silent` still silences it, which is a
635
+ deliberate instruction rather than the default this fixes.
636
+
637
+ **4xx stays quiet**, decided once inside the helper rather than at each call
638
+ site — client mistakes are already explained by the response, and logging them
639
+ is how a `?state=draft` probe once printed 45 stack traces in one browsing
640
+ session. The wire body is byte-identical at every door: this adds a side
641
+ effect, never a field.
642
+
643
+ ⚠️ Behaviour change worth knowing before upgrading: a deployment that answers
644
+ a *declared* 5xx on a polled route — `501 NOT_IMPLEMENTED` from an uninstalled
645
+ optional service, say — now prints one `error` line per request where it
646
+ previously printed none. The band is the one the issue specifies ("4xx may stay
647
+ quiet; 5xx never"); narrowing it for declared capability-absence would be a
648
+ separate contract decision.
649
+ - 2cf5a96: An organization no longer stops accepting members at 100 — membership is not a
650
+ limited axis, and the ceiling nobody chose is now stated explicitly
651
+
652
+ A customer adding users was refused with `Organization membership limit
653
+ reached`. Nothing in this codebase set that ceiling: better-auth's organization
654
+ plugin substitutes a vendor default of **100** for an absent `membershipLimit`
655
+ (`count >= (membershipLimit || 100)` in `routes/crud-members`), and
656
+ `auth-manager` passed `organizationLimit` — how many organizations one user may
657
+ CREATE — while never passing `membershipLimit`, which is a different question.
658
+
659
+ The two read almost identically in a config block and mean nothing alike, which
660
+ is why the gap survived: the option that WAS set looked like the option that
661
+ was not. In the field the refusal is worse than merely wrong — it arrives while
662
+ an operator is looking at licences and seat counts, and reads as an entitlement
663
+ problem on an axis that carries no entitlement at all. Seats are metered on AI
664
+ usage; plain membership has never been billed.
665
+
666
+ - `membershipLimit` is now passed explicitly, defaulting to unbounded.
667
+ - `OS_ORG_MEMBERSHIP_LIMIT` is the opt-in for a deployment that DOES want a
668
+ ceiling (a pilot, a trial tenant). Unusable values (empty, non-numeric,
669
+ zero, negative) read as unset rather than as a cap — a typo must not be the
670
+ thing that locks an organization, which is exactly the failure mode being
671
+ fixed.
672
+ - The decision lives in `resolveMembershipLimitOption()` rather than inside the
673
+ plugin-construction expression, so it is testable: the unset case, the
674
+ explicit ceiling, the unusable-value direction, and — deliberately — that the
675
+ chosen value clears the vendor's 100 by a wide margin. If a future
676
+ better-auth changes that default, the test says so instead of leaving an
677
+ unexplained constant behind.
678
+
679
+ The unbounded value is `Number.MAX_SAFE_INTEGER`, not `Infinity`: the option is
680
+ compared numerically but also travels through option plumbing that may assume a
681
+ finite value, and nine quadrillion members is unlimited by any measure that
682
+ reaches a real deployment.
683
+ - Updated dependencies [809d417]
684
+ - Updated dependencies [387e231]
685
+ - Updated dependencies [f794e4e]
686
+ - Updated dependencies [cae2169]
687
+ - Updated dependencies [b812a54]
688
+ - Updated dependencies [2d4fa75]
689
+ - Updated dependencies [0e4e51b]
690
+ - Updated dependencies [e84bbf6]
691
+ - Updated dependencies [effae80]
692
+ - Updated dependencies [d62f990]
693
+ - Updated dependencies [c45d8e6]
694
+ - Updated dependencies [2e3e8c7]
695
+ - Updated dependencies [e621291]
696
+ - Updated dependencies [40a93b5]
697
+ - Updated dependencies [d5b330d]
698
+ - Updated dependencies [dda969c]
699
+ - Updated dependencies [1f45690]
700
+ - Updated dependencies [277948f]
701
+ - Updated dependencies [8bdd955]
702
+ - Updated dependencies [f3bbbef]
703
+ - Updated dependencies [4f24e9d]
704
+ - Updated dependencies [474242f]
705
+ - Updated dependencies [63cd487]
706
+ - Updated dependencies [bd4aa4e]
707
+ - Updated dependencies [803eaab]
708
+ - Updated dependencies [f8e8f03]
709
+ - Updated dependencies [eae824e]
710
+ - Updated dependencies [f6fa22c]
711
+ - Updated dependencies [8a483b3]
712
+ - Updated dependencies [97bcd99]
713
+ - Updated dependencies [df59de0]
714
+ - Updated dependencies [96e25a8]
715
+ - Updated dependencies [f75a38a]
716
+ - Updated dependencies [7a25e7d]
717
+ - Updated dependencies [1fa05a6]
718
+ - Updated dependencies [c85a265]
719
+ - Updated dependencies [dcb10a5]
720
+ - Updated dependencies [773a999]
721
+ - Updated dependencies [35dffea]
722
+ - Updated dependencies [776a098]
723
+ - Updated dependencies [5060877]
724
+ - Updated dependencies [4f6325d]
725
+ - Updated dependencies [52954c0]
726
+ - Updated dependencies [2aa8456]
727
+ - Updated dependencies [93809a3]
728
+ - Updated dependencies [7c0d0c3]
729
+ - Updated dependencies [daae7aa]
730
+ - Updated dependencies [8dc22d6]
731
+ - Updated dependencies [279431e]
732
+ - Updated dependencies [948dd6b]
733
+ - Updated dependencies [3b4c56c]
734
+ - Updated dependencies [ae8edd2]
735
+ - Updated dependencies [e25403c]
736
+ - Updated dependencies [64baa68]
737
+ - Updated dependencies [9fa70d7]
738
+ - Updated dependencies [09db64a]
739
+ - Updated dependencies [92916e7]
740
+ - Updated dependencies [a84f3ea]
741
+ - Updated dependencies [f2eaae8]
742
+ - Updated dependencies [c09451b]
743
+ - Updated dependencies [ba64877]
744
+ - Updated dependencies [7345308]
745
+ - Updated dependencies [79b6a22]
746
+ - Updated dependencies [30d96ab]
747
+ - Updated dependencies [f658793]
748
+ - Updated dependencies [c95ad19]
749
+ - Updated dependencies [e58ea8b]
750
+ - Updated dependencies [4a17645]
751
+ - Updated dependencies [3795c5f]
752
+ - Updated dependencies [8ab926b]
753
+ - Updated dependencies [7317cf2]
754
+ - Updated dependencies [e25e839]
755
+ - Updated dependencies [5997207]
756
+ - Updated dependencies [8b13cc8]
757
+ - Updated dependencies [4a4a35d]
758
+ - Updated dependencies [86e765a]
759
+ - Updated dependencies [1d7e76a]
760
+ - Updated dependencies [53dc739]
761
+ - Updated dependencies [fd289be]
762
+ - Updated dependencies [03bf7b1]
763
+ - Updated dependencies [f90e820]
764
+ - Updated dependencies [18d816a]
765
+ - Updated dependencies [e8bd715]
766
+ - Updated dependencies [b91c351]
767
+ - Updated dependencies [a28a3c0]
768
+ - Updated dependencies [daeaaf9]
769
+ - Updated dependencies [c459da6]
770
+ - Updated dependencies [e914733]
771
+ - Updated dependencies [f887e52]
772
+ - Updated dependencies [881f8d8]
773
+ - Updated dependencies [3bfa1e6]
774
+ - Updated dependencies [901355c]
775
+ - Updated dependencies [34ce8e7]
776
+ - Updated dependencies [33681ea]
777
+ - Updated dependencies [4635f3e]
778
+ - Updated dependencies [ee3595c]
779
+ - Updated dependencies [b2eab95]
780
+ - Updated dependencies [93940d4]
781
+ - Updated dependencies [3a04b01]
782
+ - Updated dependencies [45b9051]
783
+ - Updated dependencies [b9e9227]
784
+ - Updated dependencies [d395692]
785
+ - Updated dependencies [5894d30]
786
+ - Updated dependencies [a3765f6]
787
+ - Updated dependencies [e22158f]
788
+ - Updated dependencies [7404925]
789
+ - Updated dependencies [0c2334f]
790
+ - Updated dependencies [778c59f]
791
+ - Updated dependencies [d2619fd]
792
+ - Updated dependencies [6acb11a]
793
+ - Updated dependencies [33c5fd3]
794
+ - Updated dependencies [20b0fdb]
795
+ - Updated dependencies [905019b]
796
+ - Updated dependencies [a286411]
797
+ - Updated dependencies [98c0d33]
798
+ - Updated dependencies [368a82e]
799
+ - Updated dependencies [a3d5724]
800
+ - Updated dependencies [93ea19b]
801
+ - Updated dependencies [9ee2dcf]
802
+ - Updated dependencies [8cb96ec]
803
+ - Updated dependencies [8f10a79]
804
+ - Updated dependencies [6269a55]
805
+ - Updated dependencies [0fb8760]
806
+ - Updated dependencies [e5ce2ed]
807
+ - Updated dependencies [be21955]
808
+ - Updated dependencies [bc56e18]
809
+ - Updated dependencies [be21955]
810
+ - Updated dependencies [a9ee989]
811
+ - Updated dependencies [4d0d944]
812
+ - Updated dependencies [15d58db]
813
+ - Updated dependencies [d63b014]
814
+ - Updated dependencies [9abe4e4]
815
+ - Updated dependencies [2cc7122]
816
+ - Updated dependencies [50d6c92]
817
+ - Updated dependencies [9e0ba21]
818
+ - Updated dependencies [311433f]
819
+ - Updated dependencies [3e5ad08]
820
+ - Updated dependencies [9abe4e4]
821
+ - Updated dependencies [b7131f3]
822
+ - Updated dependencies [e5812fa]
823
+ - Updated dependencies [7085f90]
824
+ - Updated dependencies [dee4dd4]
825
+ - Updated dependencies [ce7e497]
826
+ - Updated dependencies [51ecb2f]
827
+ - Updated dependencies [9086761]
828
+ - Updated dependencies [42a117b]
829
+ - Updated dependencies [1401ae7]
830
+ - Updated dependencies [4297fe7]
831
+ - Updated dependencies [e398863]
832
+ - Updated dependencies [d16df74]
833
+ - Updated dependencies [f11fc61]
834
+ - Updated dependencies [e808890]
835
+ - Updated dependencies [8f79379]
836
+ - Updated dependencies [e6ca40e]
837
+ - Updated dependencies [0c77ea4]
838
+ - Updated dependencies [52954c0]
839
+ - Updated dependencies [89eb997]
840
+ - Updated dependencies [aa5994e]
841
+ - Updated dependencies [be93457]
842
+ - Updated dependencies [a65db76]
843
+ - Updated dependencies [15eb2c9]
844
+ - Updated dependencies [5691b07]
845
+ - Updated dependencies [2a6122b]
846
+ - Updated dependencies [225e769]
847
+ - Updated dependencies [8af88dd]
848
+ - Updated dependencies [fb5fbb8]
849
+ - Updated dependencies [d7b3963]
850
+ - Updated dependencies [b72db01]
851
+ - Updated dependencies [dce5cd4]
852
+ - Updated dependencies [177ebdc]
853
+ - Updated dependencies [8d237b4]
854
+ - Updated dependencies [2d2e6f0]
855
+ - Updated dependencies [2d8dd8d]
856
+ - Updated dependencies [22d573e]
857
+ - Updated dependencies [b5a2398]
858
+ - Updated dependencies [348860c]
859
+ - Updated dependencies [5383fa6]
860
+ - Updated dependencies [5b3ff63]
861
+ - Updated dependencies [1a6a19c]
862
+ - Updated dependencies [527e050]
863
+ - Updated dependencies [dd33bf9]
864
+ - Updated dependencies [4cb2a90]
865
+ - Updated dependencies [74a7804]
866
+ - Updated dependencies [53d3689]
867
+ - Updated dependencies [b3a63d3]
868
+ - Updated dependencies [033a34c]
869
+ - Updated dependencies [4d25d22]
870
+ - Updated dependencies [1ffee51]
871
+ - Updated dependencies [5ae4303]
872
+ - Updated dependencies [ece4dad]
873
+ - Updated dependencies [e9b377e]
874
+ - Updated dependencies [146f448]
875
+ - Updated dependencies [735f5c7]
876
+ - Updated dependencies [a7e18de]
877
+ - Updated dependencies [366f895]
878
+ - Updated dependencies [dc75ba8]
879
+ - Updated dependencies [cce0aa9]
880
+ - Updated dependencies [e764507]
881
+ - Updated dependencies [cff17af]
882
+ - Updated dependencies [39404f3]
883
+ - Updated dependencies [ca1965f]
884
+ - Updated dependencies [8619f95]
885
+ - Updated dependencies [b706af9]
886
+ - Updated dependencies [fc9ba76]
887
+ - Updated dependencies [0f94cc7]
888
+ - Updated dependencies [a11c1a5]
889
+ - Updated dependencies [71f9cd1]
890
+ - Updated dependencies [ee17d86]
891
+ - Updated dependencies [cdbd920]
892
+ - Updated dependencies [18c432e]
893
+ - Updated dependencies [3c418c4]
894
+ - Updated dependencies [fa8715a]
895
+ - Updated dependencies [a933ed7]
896
+ - Updated dependencies [b3ca463]
897
+ - Updated dependencies [a933ed7]
898
+ - Updated dependencies [0d4a6a8]
899
+ - Updated dependencies [518d5e5]
900
+ - Updated dependencies [6643ba1]
901
+ - Updated dependencies [eeba2ef]
902
+ - Updated dependencies [ec4c4d2]
903
+ - Updated dependencies [424f73c]
904
+ - Updated dependencies [cccbe51]
905
+ - Updated dependencies [a8d6b1d]
906
+ - Updated dependencies [e4a7695]
907
+ - Updated dependencies [87075b1]
908
+ - Updated dependencies [fc58a99]
909
+ - Updated dependencies [14cfc00]
910
+ - Updated dependencies [1c6f7b4]
911
+ - Updated dependencies [e854a53]
912
+ - Updated dependencies [dfebfc8]
913
+ - Updated dependencies [d028b37]
914
+ - Updated dependencies [122ef38]
915
+ - Updated dependencies [4a37870]
916
+ - Updated dependencies [428f9b2]
917
+ - Updated dependencies [aa7ff56]
918
+ - Updated dependencies [c41b42e]
919
+ - Updated dependencies [c4db311]
920
+ - Updated dependencies [750fff5]
921
+ - Updated dependencies [c19035e]
922
+ - Updated dependencies [ececf7a]
923
+ - Updated dependencies [d173125]
924
+ - Updated dependencies [8eeca27]
925
+ - Updated dependencies [8425c17]
926
+ - Updated dependencies [a5ef1d8]
927
+ - Updated dependencies [772d5de]
928
+ - Updated dependencies [ce80ec2]
929
+ - Updated dependencies [b372318]
930
+ - Updated dependencies [97a2263]
931
+ - Updated dependencies [29d0676]
932
+ - Updated dependencies [0169d49]
933
+ - Updated dependencies [6bd3231]
934
+ - Updated dependencies [d2b5ba8]
935
+ - Updated dependencies [b799ac5]
936
+ - Updated dependencies [8f74307]
937
+ - Updated dependencies [d23dc08]
938
+ - Updated dependencies [644ad50]
939
+ - Updated dependencies [0da7cd2]
940
+ - Updated dependencies [28a5c3e]
941
+ - Updated dependencies [4bc18e5]
942
+ - @objectstack/spec@17.3.0
943
+
944
+ ## 17.2.0
945
+
946
+ ### Minor Changes
947
+
948
+ - 46d34ab: `createHostImporter`: resolve the undeclared fallback from the CALLER, not from `@objectstack/types`
949
+
950
+ The helper's documented contract said the undeclared case "falls back to the importing
951
+ package's own resolution". It did not. The fallback was a bare `import()` written inside
952
+ `@objectstack/types`, and Node ESM resolves a bare specifier against the module that
953
+ CONTAINS the call — so it resolved from `@objectstack/types`, which under a pnpm-isolated
954
+ layout can see only its own single dependency, `@objectstack/spec`. Measured from an app
955
+ declaring nothing: `@objectstack/plugin-auth`, `@objectstack/plugin-audit` and `chalk` all
956
+ resolve from `packages/cli` and all failed through the helper. Under a hoisted npm/yarn
957
+ layout the same fallback usually does find the caller's dependencies, so the claim was
958
+ green in some installs and absent in others.
959
+
960
+ `createHostImporter(hostRoot, options)` now takes the caller's resolution base as
961
+ `options.fallbackImport` — the caller's own `import()`, written in the calling module:
962
+
963
+ ```ts
964
+ createHostImporter(hostRoot, { fallbackImport: (s) => import(s) })
965
+ ```
966
+
967
+ **minor, not patch, and not major.** New exported API (`HostImporterOptions`,
968
+ `FallbackImport`, a second parameter) makes it additive rather than a fix-only patch. It
969
+ is not a breaking change because the parameter is optional and omitting it keeps the
970
+ previous resolution base exactly — an existing caller compiles and behaves as before. The
971
+ `undeclared` failure text now names that retained default when a caller has not passed a
972
+ base, so the gap reports itself instead of being rediscovered by measurement.
973
+
974
+ `@objectstack/verify` (patch) passes its own base from `bootStack`. Measured: this changes
975
+ nothing for `@objectstack/organizations`, the only specifier it routes through the helper —
976
+ that package is cloud-private and resolves from nowhere in the framework workspace. It is
977
+ what stops the next app-supplied package added to that path from silently missing
978
+ `packages/verify`'s own dependencies.
979
+
980
+ A string `parentURL` / `import.meta.url` base was measured on Node v22.22.2 and rejected in
981
+ both spellings: `import.meta.resolve`'s parent argument is silently ignored without
982
+ `--experimental-import-meta-resolve` (a change that would have compiled, run, and pinned
983
+ green while ignoring the base), and `createRequire(parentURL)` is CJS resolution, which
984
+ honours `NODE_PATH` — the hole the declaration gate exists to close, re-opened on the
985
+ fallback path.
986
+
987
+ ### Patch Changes
988
+
989
+ - Updated dependencies [6936d07]
990
+ - Updated dependencies [59eb04d]
991
+ - Updated dependencies [9f05b7d]
992
+ - Updated dependencies [7d2d112]
993
+ - Updated dependencies [5fa0d72]
994
+ - Updated dependencies [02b3b07]
995
+ - Updated dependencies [914c413]
996
+ - Updated dependencies [55809a0]
997
+ - Updated dependencies [52db1d1]
998
+ - Updated dependencies [5649efb]
999
+ - Updated dependencies [2306a76]
1000
+ - Updated dependencies [e5ea701]
1001
+ - Updated dependencies [a40dcc1]
1002
+ - Updated dependencies [def0d3e]
1003
+ - Updated dependencies [8d0bb79]
1004
+ - Updated dependencies [5acb58d]
1005
+ - Updated dependencies [2e3cf95]
1006
+ - Updated dependencies [4c93387]
1007
+ - Updated dependencies [a037f7c]
1008
+ - Updated dependencies [3ee8ddf]
1009
+ - Updated dependencies [16cef97]
1010
+ - Updated dependencies [a79bd35]
1011
+ - Updated dependencies [6ceaa4b]
1012
+ - Updated dependencies [15ea214]
1013
+ - Updated dependencies [de19489]
1014
+ - Updated dependencies [c684d00]
1015
+ - Updated dependencies [923c424]
1016
+ - Updated dependencies [1ec36b7]
1017
+ - Updated dependencies [5f2e54c]
1018
+ - Updated dependencies [189373b]
1019
+ - Updated dependencies [35ad101]
1020
+ - Updated dependencies [ceb33a9]
1021
+ - Updated dependencies [73d9795]
1022
+ - Updated dependencies [8012960]
1023
+ - Updated dependencies [f34f56b]
1024
+ - Updated dependencies [f399618]
1025
+ - Updated dependencies [75e9301]
1026
+ - Updated dependencies [2810695]
1027
+ - @objectstack/spec@17.2.0
1028
+
3
1029
  ## 17.1.0
4
1030
 
5
1031
  ### Minor Changes