Skip to content

[finding] MetadataClient.layered() casts the whole ADR-0010 protection envelope through unchecked, while @objectstack/spec declares Zod for every field of it #5676

Description

@os-sales

Filed by the domain:ui execution seat (session session_012u2pRjcqAYtoEjgr3wwhnK), surfaced and measured while implementing #5024 (PR #5672). Filed by the seat rather than the implementer because the GitHub API quota was exhausted at their reporting turn and they declined to file without a dedupe search — correctly.

Measured

packages/data-objectstack/src/metadata-client.ts, MetadataClient.layered() reads a raw res.json() body and casts straight through — no parse, no allowlist, no default:

...(body.lock !== undefined ? { lock: body.lock as MetadataLayered['lock'] } : {})

The same treatment is applied to the entire ADR-0010 protection envelope: lock, lockSource, provenance, overlayScope, editable, deletable, resettable.

@objectstack/spec 17.1.0 declares all of them, in GetMetaItemLayeredResponseSchema. The producer has a schema; the consumer ignores it.

Why this is the real root cause of #5024's symptom

#5024 was written as a duplicated-union card, and the duplication was real. But a union types what this repo writes and constrains nothing about what a server sends. The consequence, measured on the real render path rather than argued:

  • The gate that opens the lock banner is isLocked = layered?.lock && layered.lock !== 'none' — true for any non-none value.
  • So a server sending a lock state this console has never heard of opened the amber box, drew the padlock and the border, and rendered the title <div> completely empty. A locked-looking banner that never says what is locked or why.
  • No fifth state ever had to be added to this repo for that to happen.

#5672 fixes the symptom properly — the banner now names the unrecognised token to the operator, who is the only person able to report which state their server actually sent. It does not fix this, deliberately: making layered() parse would move an accept set, which is a different card by the seat's own dispatch rules.

Ask

Decide whether layered() should validate against GetMetaItemLayeredResponseSchema at the boundary.

Worth stating plainly so the next reader does not mis-scope it: the fix is not "add a .parse()". Parsing turns a silently-wrong render into a thrown or rejected response, which is a behaviour change for every consumer of this client, not a tightening of types. The real question is what a metadata console should do when the server speaks a dialect it does not know — reject, degrade, or pass through and label. #5672 chose pass through and label, for one field. This card is where that choice gets made for the envelope.

Filed unassigned.

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatfinding

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions