docs(core): state the measured structuredClone behaviour on the authz brand - #14614
Conversation
…hz brand (#14006) 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
📓 Docs Drift Check
What this run could not see
Coarse fallback — 24 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
|
Landing provenance (engine execution seat, session
Generated by Claude Code |
Fixes #14006
Prose-only. The brand docblock in
packages/core/src/security/authz-store-unavailable.tsjustified its string-keyed own property with two reasons joined by anand, and only the second was true. The false half is replaced by the measured behaviour; the property and the reason that actually earns it are kept.The claim, before
Reproduction, run on this box before the edit
Node v22.22.2, the card's three lines plus the plain-object control:
The control is the load-bearing half: the same two keys both survive on
{ __brand: true, code: 'C' }and both vanish on anError, so the loss is specific toError's dedicated serialization, not general tostructuredClone.Measured again on this module's actual class shape (own
name, owncode/status/object, owncause), because the class is not a bareError:Two things that measurement changed in the wording.
causedoes survive, and a subclass's ownnamedoes not — it returns as'Error'. So the docblock says the clone carriesmessage,stackandcause, rather than repeating the sibling module's "name,message,stackandcause" list verbatim: that list is true where it is written (service-not-registered.tsdeliberately does not overridename, and says so) but would have been false here. Same phrasing, corrected for the one place this class differs — not a second competing claim.Wording alignment
packages/core/src/service-not-registered.tsalready records the measured behaviour for its own brand ("The brand does NOT survivestructuredClone, and no claim here depends on it doing so"). This docblock opens with that same sentence and then points at the reproducible script and the Node version rather than adding a fresh unsourced prose assertion. One phrasing across the two modules. #13905 is referenced here only as context; nothing in this PR touches that module.Call sites: why no runtime change is owed
git grepoverpackagesandappsforrethrowAuthzStoreUnavailable/isAuthzStoreUnavailableErrorfinds every call site in-process —.catch(rethrowAuthzStoreUnavailable)on the rest rethrow paths (packages/rest/src/rest-server.ts,packages/rest/src/package-routes.ts) andif (isAuthzStoreUnavailableError(err)) throw err;inside service and plugincatchblocks (service-datasource, service-settings, service-storage, plugin-sharing, cloud-connection).git grepforpostMessage/new Worker/worker_threadsunderpackages/core/srcreturns nothing. No caller crosses a clone boundary, so nothing is broken today and no behaviour needs to change.Per the triage fence, the clone gap is deliberately not "fixed" with a
toJSONor custom serialization — that would widen the module's surface with nothing pulling on it. Instead the docblock names the trap the false claim invited: branching on the brand across a worker orpostMessageboundary would answerfalseand fail OPEN on a security path.Changeset decision: measured, PRESENT, so a changeset is owed
The brand constant is module-private, so whether its docblock ships was a real question rather than an assumption. Built the package and grepped the emitted declarations:
dist/index.d.tsdist/index.d.ctsNode 22.22.2(unique to the new text)fail OPEN(unique to the new text)so it survives(the removed claim)HTTP status an unreachable authorization store answers with(positive control)Raised when a permission-store read FAILED(positive control)tsup's DTS rollup inlines the private brand constant into the exported class type, carrying the docblock with it — the emitted block sits at
dist/index.d.ts:2047-2069, directly abovedeclare const AUTHZ_STORE_UNAVAILABLE_BRAND. A comment reaching the published declarations is a published byte, so.changeset/authz-brand-structured-clone-doc.mddeclares@objectstack/core: patch. Noskip-changesetlabel.Clause-② self-reading: no
Read from this diff, not from the dispatch. All 28 changed lines in the
.tsfile begin with*— every one is a block-comment line, zero non-comment lines moved:No accept/reject behaviour, no public type change, no gate moves.
Verification, at
72a4c5df0(git rev-parse --short HEADafter the last commit)Gate set re-derived on the final tree with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(no path list — the script took its own change set: 2 paths vs merge based15ecd855). 30 commands derived; all exit codes captured after a redirect, never through a pipe.check:doc-authoring,check:published-files,check:test-source-alias,check:type-source-resolution,check:empty-changeset,check:changeset-no-major,check:objectui-changeset,check:dispatcher-error-vocabulary.check-test-completeness.mjs("no local log to hand it... record this gate as NOT MEASURED"),pm/check-half-states.mjs(needs repo-scoped REST, which answers 403 in this container), andcheck:dual-build-cjs-loads("PREREQUISITE NOT MET — this gate reads built output... 72 more" packages withoutdist/; a whole-repo build, which CI does and this seat did not).27 + 3 = 30, counted from the captured logs (
grep -c 'EXIT=0'/'EXIT=3'across the three batch files) rather than by hand.Always-runs:
pnpm lint(whole repo,eslint . --no-inline-config) — exit 0, full run, no narrowing.pnpm check:nul-bytes— exit 0 (7981 text files scanned, no raw control bytes). Also self-scanned both changed files withgrep -naPbefore committing.pnpm check:error-status-conformance— exit 0.pnpm --filter @objectstack/core exec vitest run --maxWorkers=2 src/security/authz-store-unavailable.test.ts— exit 0, 1 file, 37 tests passed.pnpm --workspace-concurrency=2 --filter '@objectstack/core^...' --filter '@objectstack/core' build— exit 0 (the build the d.ts measurement above reads).No ablation is owed: the diff is comment-only, so there is no guard whose removal could be shown to turn anything red.
Two things this seat could not measure, stated rather than glossed
pnpm --filter @objectstack/core typecheckdoes not exist. The package declares onlybuild,testandtest:watch, and pnpm said so loudly (ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT, exit 1) rather than silently matching zero scripts. Substitutedtsc --noEmit -p tsconfig.json, which exits 2 with 98 pre-existing errors across 12 files, every one a.test.tsand none in this module — the sole log line namingauthz-store-unavailableis the command echo. That red is baseline, not this diff. Filed as a separate finding: [finding]@objectstack/coredeclares notypecheckscript, so no CI job ever type-checks it — and its own tsconfig program is currently red #14613.d15ecd855, three commits behindorigin/mainat derivation time, and the tool flagged one derived-from file as changed in that range:scripts/check-skills-token-ratchet.mjs. Re-derived aftergit fetch origin main— byte-identical command list. That script governsskills/**only (SKILLS_DIR = join(REPO_ROOT, 'skills')), and this diff touchespackages/core/**and.changeset/**, so it cannot enter this change set; it is absent from the derived 30. No rebase performed.🤖 Generated with Claude Code
https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68