Skip to content

meta.deleteItem sends none of the three carriers the REST reset door reads — no If-Match, no ?state=draft, no ?dropStorage, on either declaration #12181

Description

@os-trump

Measured while implementing #11713 (the meta.saveItemIf-Match half). Same defect shape, one method over — the SDK's write surface cannot reach parameters its own server door implements and its sibling first-party client already sends — so it is reported rather than folded in: #11713's ruling is scoped to saveItem and its SaveMetaItemOptions bag, and widening deleteItem is a contract decision of its own.

What was measured

On origin/main at 22c42c9b.

Both deleteItem declarations take exactly (type, name) — no options bag, no header, nothing that becomes one:

$ grep -n "deleteItem: async" packages/client/src/index.ts
955: deleteItem: async (type: string, name: string): Promise<{ type: string; name: string; deleted: boolean }> => {
5600: deleteItem: async (type: string, name: string): Promise<{ type: string; name: string; deleted: boolean }> => {

(955 = unscoped ObjectStackClient.meta; 5600 = the environment-scoped ScopedProjectClient.meta twin. Both build a bare path and a method: 'DELETE'.)

The door reads three carriers.DELETE /meta/:type/:name — "Reset metadata item to artifact default", registered at packages/rest/src/rest-server.ts:5766:

// the ADR-0008 OCC pin, rest-server.ts:5831
const ifMatchHeader = req.headers?.['if-match'] ?? req.headers?.['If-Match'];
const parentVersion = typeof ifMatchHeader === 'string'
? ifMatchHeader.replace(/^"|"$/g, '')
: undefined;
// [#6877] `?state=` and the destructive `?dropStorage=`
if (refuseRepeatedQueryParams(req, res, ['state', 'dropStorage'])) return;
const stateParam = typeof req.query?.state === 'string'
&& req.query.state.toLowerCase() === 'draft'
const dropStorage = req.query?.dropStorage === 'true' || req.query?.dropStorage === '1';

parentVersion is threaded into the protocol call at rest-server.ts:5895.

The spec names the header on this exact door, in DeleteMetaItemRequest's parentVersion (packages/spec/src/api/protocol.zod.ts:1150):

ADR-0008 optimistic-concurrency pin: the version token the caller believes is current (on the REST door, the If-Match request header). Present, a concurrent edit is reported as a 409 conflict instead of silently reset; absent = last-write-wins against the current row (Studio's "Reset" button is unpinned).

The sibling first-party client already sends it.MetadataClient.reset in @object-ui/data-objectstack (objectui packages/data-objectstack/src/metadata-client.ts, measured at 090927f) carries both the header and the state parameter:

constqs=options.state==='draft' ? '?state=draft' : '';constheaders: Record<string,string>={ ...this.headers};if(options.ifMatch)headers['If-Match']=options.ifMatch;

So a first-party @objectstack/client caller cannot:

  1. pin a reset — a concurrent edit is silently destroyed instead of answering 409, on a verb whose whole job is destroying an overlay row;
  2. discard only the pending draft (?state=draft) — the SDK's only reachable reset is the full one, which also drops the published overlay;
  3. reach ?dropStorage, whose destructiveness is the reason #6877 gave it a repeated-parameter refusal.

Item 1 is the sharpest: the reset door is the one write where last-write-wins costs the most, and it is the one the spec text explicitly describes as pinnable.

Not taken here

#11713's maintainer ruling (2026-08-25) enumerates ifMatch joining SaveMetaItemOptions on the two saveItem declarations. deleteItem is a different method with no bag at all, and the three carriers above are three separate widenings of the published SDK surface — plausibly rulable separately (a header pin is a data-protection gap; ?state=draft is a feature). Reported rather than taken, exactly as #11713 was reported out of #11391.

Related: #11713 (the saveItem header half), #11391 (the saveItem query half), ADR-0008 for the OCC contract, #6877 for the repeated-parameter refusals on this door.


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions