Repository navigation
ObjectPermissionSchema's retired allowRestore/allowPurge: only literal false parses (not a truthy/falsy split), and no post-parse guard can ever see either key #17425
Description
Split off #16277 by the dispatch brief — triage judged this measurement "worth its own card" but did not re-run it, so did not file it. I re-ran it and confirmed, with a more precise characterization than the original report.
Original claim (from #16277)
Measured on the installed
@objectstack/spec17.3.0 (ObjectPermissionSchema.safeParse): onlytrueis refused.allowRestore: false/allowPurge: falseparse successfully, and the parsed output carries neither key — the retired-default residue tolerance of #12840. Consumers writing a guard against these bits therefore need=== true, not a presence or truthiness check.
What I measured (origin/main @ cef399be82's base, packages/spec/src/security/permission.zod.ts, ObjectPermissionSchema.safeParse, run via pnpm --filter @objectstack/spec exec tsx against a minimal valid base object)
allowRestore=false success=true carriesKey=false (dropped — #12840 residue tolerance)
allowRestore=true success=false code=invalid_type ("expected": "never", ADR-0049 removal message)
allowRestore="true" success=false code=invalid_type (same — NOT boolean-specific)
allowRestore="false" success=false code=invalid_type (same — a string "false" is ALSO refused)
allowRestore=0 success=false code=invalid_type (same — a falsy non-boolean is ALSO refused)
allowRestore=1 success=false code=invalid_type (same)
allowRestore=null success=false code=invalid_type (same)
key entirely absent success=true carriesKey=false (never added)
allowPurge behaves identically (spot-checked true/false).
The correction to the original framing
The original report says "only true is refused" — that undersells it. The schema accepts exactly one value, the boolean literal false; every other value of any type is refused with the same ADR-0049 removal message (code: 'invalid_type', expected: 'never'), not just true. A string "false", the number 0, or null are refused exactly like true is — this is not a truthy/falsy check, it is a z.literal(false)-shaped tombstone gate (see retiredKey() in packages/spec/src/shared/retired-key.ts for the general shape).
The consumer-facing asymmetry, stated precisely
A successfully parsed ObjectPermissionSchema object can never carry allowRestore or allowPurge at all: the only value that survives parsing (false) is stripped from the output, and every other value throws before a parsed object exists. So:
- Post-parse,
permissions.allowRestoreis alwaysundefined— a presence check ('allowRestore' in permissions) or truthiness check (if (permissions.allowRestore)) against parsed/validated output is not merely imprecise, it is dead code: the condition can never be true, on any input that survived validation. - The original report's suggested remedy ("write
=== true, not a presence/truthiness check") does not fix this for parsed output either —permissions.allowRestore === trueis also alwaysfalsepost-parse, because a rawtruenever survivessafeParse/parsein the first place (it throws). - The distinction only has a live consumer on pre-parse / raw author input (e.g. a linter or migration tool reading
objectstack.jsonsource before validation): there,false(legacy no-op) andtrue/other (hard ADR-0049 violation) are different facts an author-facing tool should say different things about, and a presence/truthiness check on raw input conflates them.
Ask
Whichever key's doc/prompt surface discusses this (ObjectPermissionSchema's .describe() text, the retired-key migration guidance, or downstream consumer docs) should state the parse-time behavior precisely: only false parses, and is stripped; every other value refuses at parse with the ADR-0049 message; consumers of parsed/validated data will never see either key, so no post-parse guard is meaningful — pre-parse/raw-source tooling is the only place a false-vs-other distinction is observable.
Provenance
- Split from
@objectstack/metadata17.3.0's CHANGELOG says "spec 17.2.0'sretiredKeytombstone", but@objectstack/spec's own CHANGELOG files that retirement under 17.3.0 #16277 (objectstack-ai/objectstack#16277), which citesobjectstack-ai/hotcrm#1634as the original downstream report of the asymmetry. - Refs: spec: retire the
allowRestore/allowPurgepermission props (ruled 2026-08-26; M2 anchor stays open, keys return with M2) #12497 (ADR-0049 enforce-or-remove), feat(spec): retired-defaulted-key tolerance — the retired default parses as inert residue and strips; non-default values keep the loud refusal (#12497 class rule) #12840 (retired-default residue tolerance).
Activity