Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .changeset/authz-brand-structured-clone-doc.md
Original file line number Diff line number Diff line change
@@ -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.
28 changes: 25 additions & 3 deletions packages/core/src/security/authz-store-unavailable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading