From 72a4c5df00fb6003242cd3fc9f39464f4779aeeb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 14:46:00 +0000 Subject: [PATCH] docs(core): state the measured `structuredClone` behaviour on the authz brand (#14006) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The brand docblock justified its string-keyed own property with two reasons joined by an `and`, and only the second was true. Measured on Node 22.22.2, the structured-clone algorithm gives `Error` a dedicated serialization carrying `message`, `stack` and `cause` only and drops every other own property — brand, ADR-0112 `code`, `status` and `object` alike; the plain-object control keeps both keys through the same call, so the loss is specific to `Error`, not general to `structuredClone`. The property and the reason that earns it stay (a duplicated copy of the module still brands identically, which `instanceof` cannot do). The false half is replaced by the measured behaviour plus the reproducible script and Node version, phrased to match what `service-not-registered.ts` already records. No runtime change, and deliberately no `toJSON` — every call site is in-process, so nothing pulls on clone support. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68 --- .../authz-brand-structured-clone-doc.md | 42 +++++++++++++++++++ .../src/security/authz-store-unavailable.ts | 28 +++++++++++-- 2 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 .changeset/authz-brand-structured-clone-doc.md diff --git a/.changeset/authz-brand-structured-clone-doc.md b/.changeset/authz-brand-structured-clone-doc.md new file mode 100644 index 0000000000..a6568f5143 --- /dev/null +++ b/.changeset/authz-brand-structured-clone-doc.md @@ -0,0 +1,42 @@ +--- +'@objectstack/core': patch +--- + +docs(core): the `AuthzStoreUnavailableError` brand doc states the measured `structuredClone` behaviour instead of claiming survival (#14006) + +Documentation only — no runtime change, no type change, no accept/reject +behaviour moves. It ships as a patch because the docblock is a **published +byte**: `tsup`'s declaration rollup carries it into `dist/index.d.ts` and +`dist/index.d.cts`, so it is what a consumer reads on hover. + +The brand's docblock justified the string-keyed own property with two reasons +joined by an `and`, of which only the second was true: + +> A string-keyed own property (not a `Symbol.for` registry key) so it survives +> `structuredClone`, and so a duplicated copy of this module still brands +> identically. + +Measured on Node 22.22.2: the structured-clone algorithm gives `Error` a +dedicated serialization carrying `message`, `stack` and `cause` only, and drops +every other own property — the brand, the ADR-0112 `code`, `status` and +`object` alike (a subclass's own `name` returns as `'Error'`). The +plain-object control is the half that proves it: `{ __brand: true, code: 'C' }` +keeps **both** keys through the same call, so the loss is specific to `Error`, +not general to `structuredClone`. + +The property and the reason that actually earns it are kept — a duplicated copy +of the module still brands identically, which is exactly what `instanceof` +cannot do across two installed copies of `@objectstack/core`. The false half is +replaced by the measured behaviour, carrying the reproducible script and the +Node version rather than a second unsourced assertion, and phrased to match +what `service-not-registered.ts` already records for its own brand (one +phrasing across the two modules, not two). + +⛔ The clone gap is deliberately NOT "fixed" with a `toJSON` or a custom +serialization: no call site crosses a clone boundary today +(`rethrowAuthzStoreUnavailable` on the rest rethrow paths, +`isAuthzStoreUnavailableError` inside service `catch` blocks — all in-process), +and adding one would widen the module's surface with nothing pulling on it. The +docblock instead names the trap the false claim invited: branching on the brand +across a worker or `postMessage` boundary would answer `false` and fail OPEN on +a security path. diff --git a/packages/core/src/security/authz-store-unavailable.ts b/packages/core/src/security/authz-store-unavailable.ts index 6e5525f73f..ecbd9b6228 100644 --- a/packages/core/src/security/authz-store-unavailable.ts +++ b/packages/core/src/security/authz-store-unavailable.ts @@ -106,9 +106,31 @@ export const AUTHZ_STORE_UNAVAILABLE_MESSAGE = /** * The own-property brand {@link isAuthzStoreUnavailableError} tests for. - * A string-keyed own property (not a `Symbol.for` registry key) so it survives - * `structuredClone`, and so a duplicated copy of this module still brands - * identically. + * A string-keyed own property (not a `Symbol.for` registry key), so a + * duplicated copy of this module still brands identically — which is exactly + * what `instanceof` cannot do (module doc above). + * + * ⚠️ The brand does NOT survive `structuredClone`, and no claim here depends + * on it doing so — the same measured behaviour `service-not-registered.ts` + * records for its own brand. Reproduce on Node 22.22.2: + * + * ```js + * const e = new Error('x'); e.__brand = true; e.code = 'C'; + * const c = structuredClone(e); + * // c.__brand === undefined c.code === undefined c.message === 'x' + * // control: structuredClone({ __brand: true, code: 'C' }) keeps BOTH keys + * ``` + * + * `Error` has a dedicated serialization carrying `message`, `stack` and + * `cause` only, so it DROPS every other own property — this brand, the + * ADR-0112 `code`, `status` and `object` alike (and a subclass's own `name` + * returns as `'Error'`). The plain-object control is the half that proves the + * loss is specific to `Error`, not general to `structuredClone`. + * + * ⛔ So never branch on this brand across a worker or `postMessage` boundary: + * it would answer `false` and fail OPEN. Every call site today is in-process — + * `rethrowAuthzStoreUnavailable` on the rest rethrow paths and + * `isAuthzStoreUnavailableError` inside service `catch` blocks. */ const AUTHZ_STORE_UNAVAILABLE_BRAND = '__objectstackAuthzStoreUnavailable' as const;