Skip to content
15 changes: 15 additions & 0 deletions .changeset/21716-lock-org-axis-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@objectstack/metadata-protocol': minor
---

fix(metadata-protocol)!: the ADR-0010 `_lock` gate reads the row the read serves for the request's organization, so an env-wide row's lock binds an organization with no row of its own (#21716)

Clause-②: no (narrowing)

<!-- adr-0087: not-required (no-migration-prescription) a write-door verdict that now reads one more stored row: the per-item _lock gate (save, publish, rollback, delete) used to read only the stored row whose organization equals the request's, and now reads the row both metadata reads serve for that organization, which is the env-wide row when the organization has none of its own. No authorable key, spelling, export or stored shape is retired or renamed: MetadataLock keeps its four states, every stored row keeps parsing and is neither read differently by the reads nor rewritten by an upgrade, and which items an operator meant to keep writable for an organization is not something a ledger entry can rewrite. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers a lock verdict (not already-registered); and the change is a door verdict, not a TypeScript declaration (not runtime-interface-only or type-surface-only). -->

**BREAKING**: an organization-scoped `/meta` write that used to be accepted can now be refused. The item reads (`getMetaItem`, `getMetaItemLayered`) serve an organization its own stored row, and the env-wide row when it has none (ADR-0005). The item-level `_lock` gate read only the organization's own row. So when the env-wide row declared a lock, an organization with no row of its own read `lock: "full"` and `editable: false`, while its `saveMetaItem`, `publishMetaItem`, `rollbackMetaItem` and `deleteMetaItem` were admitted. The gate now reads the row the reads serve, through the same resolution, and answers `403 ITEM_LOCKED` where the envelope says the write is not allowed. It ships as `minor` under the launch-window convention for accept-set narrowings. No export is added or removed.

**What is refused now.** On every kernel topology, a save, publish or rollback with an organization of an item whose env-wide stored row declares `_lock: "no-overlay"` or `"full"`, and a delete with an organization of one whose env-wide row declares `"no-delete"` or `"full"`, when that organization has no stored row of its own for the item. Each refusal writes its `denied` row to `sys_metadata_audit` under the requesting organization. Over the wire this reaches the five per-org overridable types (`view`, `dashboard`, `report`, `translation`, `email_template`): the REST and dispatcher write doors already send no organization for any other type. For those other types the reads never serve an organization-scoped row, and now neither does the gate: an in-process removal of a pre-#6190 organization-scoped row of such a type (`deletePackage`, `discardPackageDrafts`) is judged by the env-wide row's lock, the row both reads serve. If a deployment relied on writing such an item per organization: change or remove the env-wide row's lock (a `no-delete` row can still be saved, a `no-overlay` row deleted), or keep the lock and author the organization's variant under a new name.

**Unchanged.** When the organization has a stored row of its own, that row is the one both reads serve, and its `_lock` decides, whatever the env-wide row declares (ADR-0005 precedence, never a merge). A request with no organization. The packaged artifact's lock, which still wins when it declares one. Both reads, apart from one case in `getMetaItemLayered`: it now serves an organization's own stored row whose body is JSON `null`, as `getMetaItem` already did, instead of falling back to the env-wide row, because the two reads now share one row resolution. Only residue can reach it: no live writer stores a `null` body (measured: `SysMetadataRepository.put`, the writer behind every `/meta` save, stores `{}` for an absent body; `saveMetaItem` refuses a `null` item with `400 INVALID_REQUEST`; and the only other `sys_metadata` writer, the datasource admin plugin, stores an object env-wide). The gate addresses the canonical type spelling only; the reads' last-resort read of a row stored under the type's other spelling is not extended to the write path.
Loading
Loading