Skip to content

Commit ec4c4d2

Browse files
feat(spec): declare MetadataProtocol.historyMetaItem and de-cast the REST history door (#13521)
* feat(spec): declare MetadataProtocol.historyMetaItem and de-cast the REST history door The history door schemas mirror the implementation's parameter and return types member for member (the #11006 pattern, carried one door over exactly as #11678 carried it to the audit twin). The REST door literal now compiles against the declared contract through TransportScopedMetaRequest; wire payload byte-identical. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KX8wnyjStaZcuMyAMNsy3N * docs(spec): strip internal issue ids from the two new describe() strings check:doc-authoring measured them in the customer-facing describe population; the reasoning stays in the TSDoc comments, which are internal. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KX8wnyjStaZcuMyAMNsy3N * docs(permissions): re-anchor the eight rest-server.ts isSystem citations the history-door diff shifted check-system-context-census (CI 'Lint & Repo Gates') caught pure line rot: the new import line shifted every site below it by +1 and the de-cast door block shifted the two sites below it by +19. The gate's --fix refused (its population sanity compares page anchors against census reads without folding in the two NON_READ_ANCHORS-excused seams), so the eight anchors are rewritten by hand to the lines the census and ledger already resolve to. Census now: 109 sites all anchored, 145 anchors resolve, 27 declared non-read. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KX8wnyjStaZcuMyAMNsy3N * docs(spec): regenerate protocol.mdx from the merged tree Discharges the regeneration the merge commit deferred (os-regen). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KX8wnyjStaZcuMyAMNsy3N --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ae8edd2 commit ec4c4d2

14 files changed

Lines changed: 494 additions & 18 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
"@objectstack/rest": patch
3+
---
4+
5+
refactor(rest): the history door call site is compiled against the declared contract (#12005)
6+
7+
The `GET /meta/:type/:name/history` door in `packages/rest/src/rest-server.ts`
8+
reached its protocol method through `(p as any)` — once for the
9+
feature-detection guard, once for the call — so the compiler checked nothing
10+
about the request literal it built. The cast was load-bearing on **member
11+
existence** (`historyMetaItem` was undeclared in `packages/spec` entirely —
12+
removing the cast answered `TS2339`), the same half the audit twin's cast
13+
carried before #11678.
14+
15+
With `MetadataProtocol.historyMetaItem` declared (the spec half of this
16+
landing), the guard is now `if (!p.historyMetaItem)` and the request is a named
17+
const typed as `TransportScopedMetaRequest<HistoryMetaItemRequest>` — the
18+
reset-door spelling, not the audit door's plain request type, because this door
19+
still spreads the transport-level `environmentId` (long-standing wire shape,
20+
deliberately unchanged; the #9741 ruling keeps it layered on by the wrapper
21+
rather than becoming a protocol key).
22+
23+
**No behaviour change of any kind, and nothing about the wire moves.** The
24+
outgoing payload is byte-identical (same keys, same conditional spreads, same
25+
`Number.isFinite` drops); the 501 refusal is untouched (its bare-string
26+
envelope remains the #7035-family ratcheted debt it already was — converging it
27+
is a behaviour change this declaration must not smuggle). The guard survives
28+
with identical truthiness semantics: the member is declared **optional** (a
29+
kernel may implement neither door), and the guard is also what narrows it to
30+
callable at the call site. An undeclared key in the literal is now a compile
31+
error instead of a payload member no contract has ever seen.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
**`MetadataProtocol` declares the optional `historyMetaItem` member, and the history door's request/response schemas join the spec** (#12005 — the #11006 maintainer-ruled pattern, 2026-08-22 option B, carried one door over exactly as #11678/PR #12003 carried it to the audit twin).
6+
7+
`GET /api/v1/meta/:type/:name/history` — the durable change-log behind Studio's History tab — was the last undeclared read door of the audit/history pair: `historyMetaItem` appeared nowhere in `packages/spec`, so the REST door reached the verb through `(p as any)` twice (feature-detection guard + call) and its request literal was compiled against nothing.
8+
9+
Additive, not breaking:
10+
11+
- `HistoryMetaItemRequestSchema` / `HistoryMetaItemRequest``{ type, name, organizationId?, sinceSeq?, limit? }`, mirroring the implementation's parameter type in `@objectstack/metadata-protocol` member for member. `organizationId` is a plain optional `string` (not nullable like the audit twin's) because that is this implementation's declared type — and this door currently sends no organization at all (the #8747-family tenant-scoping question stays a separate measurement, deliberately unanswered here). `sinceSeq` is the exclusive lower bound on `seq` for pagination. `limit` declares no bounds because the implementation forwards it unclamped with no default (unlike the audit twin's [1, 500] clamp). `environmentId` stays out by the #9741 ruling (transport-level routing key); this door still spreads it on the wire, and that member rides the REST `TransportScopedMetaRequest` wrapper, never the protocol schema.
12+
- `HistoryMetaItemResponseSchema` / `HistoryMetaItemResponse` — the `{ events: [...] }` body, oldest first, transcribing `MetadataEventSchema` from `@objectstack/metadata-core` (ADR-0008 §2.4) with the closed `op` vocabulary (create/update/delete/rename/publish/revert). Two deliberate widenings against the source schema so the contract cannot refuse bodies the shipped verb yields: `ref.type` is a plain string rather than the static registry enum (plugin runtime-create types flow through this door — the #12038 1C anti-freezing reasoning), and `ref.name` carries no spelling regex. `{ events: [] }` is the honest answer for a clean change log or a non-overlay type — never for a missing capability (501 before the call).
13+
- `MetadataProtocol.historyMetaItem?(request: HistoryMetaItemRequest): Promise<HistoryMetaItemResponse>` — optional like its `auditMetaItem` / `deleteMetaItem` / `getMetaItemLayered` siblings: additive to a shipped contract, implementation predating declaration. An undeclared key in a request literal at the member's call shape is now a compile error.

content/docs/permissions/system-context.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ not on any flag.
6464
## How the flag is set
6565

6666
`isSystem` is **server-constructed and never client-supplied**. Inbound HTTP
67-
cannot set it (`packages/rest/src/rest-server.ts:1238`, `:1267`), and neither
67+
cannot set it (`packages/rest/src/rest-server.ts:1239`, `:1268`), and neither
6868
can an action body (`packages/runtime/src/domains/actions.ts:404`). It is
6969
written by internal callers only, as an option on the engine call:
7070

@@ -103,7 +103,7 @@ that silently does not happen.
103103
| 14 | MCP stdio bridge skips the object API-exposure gate | mcp | Get: the bridge reaches objects whose `apiEnabled` / `apiMethods` would refuse an external caller | `stdio-data-bridge.ts:246` |
104104
| 15 | **Read-audit rows are not written** | plugin-audit | Lose: the "a person opened this record" trail. `sudo()` keeps the caller's `userId`, so this flag is the only thing separating a human read from a platform one | `read-audit.ts:556` |
105105
| 16 | Approval snapshot payload redaction skipped | plugin-approvals | Get: the whole snapshot on `find` / `findOne` — the audit/replay channel. Lose: field-visibility redaction over approval payloads | `payload-redaction-middleware.ts:115` |
106-
| 17 | REST anonymous-deny seam satisfied | rest | Get: `enforceAuth` passes with no `userId`. Not reachable from the wire — `isSystem` is never set on an inbound request | `rest-server.ts:1270` |
106+
| 17 | REST anonymous-deny seam satisfied | rest | Get: `enforceAuth` passes with no `userId`. Not reachable from the wire — `isSystem` is never set on an inbound request | `rest-server.ts:1271` |
107107

108108
### 2. Write pipeline and data integrity
109109

@@ -158,7 +158,7 @@ The largest single consumer — **20 of the 109 sites**.
158158
|:--|:---|:---|:---|:---|
159159
| 48 | Object API-exposure gate bypassed (`apiEnabled` / `apiMethods`) | runtime | Get: internal self-writes ignore exposure declarations — these govern **external** exposure, not engine self-writes | `action-execution.ts:136` |
160160
| 49 | Action `requiredPermissions` bypassed | runtime | Get: engine self-invocation runs any action | `action-execution.ts:399` |
161-
| 50 | `manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability | `domains/meta.ts:471`, `:874`, `rest-server.ts:4282`, `:5645`, `:5877`, `:6222`, `:6415` |
161+
| 50 | `manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability | `domains/meta.ts:471`, `:874`, `rest-server.ts:4283`, `:5646`, `:5878`, `:6241`, `:6434` |
162162
| 51 | The shared metadata-write verdict itself returns `allowed` | metadata-core | Get: the one function all of row 50's doors consult answers yes before any capability is examined | `meta-write-capability.ts:134` |
163163
| 52 | Anonymous-deny seam satisfied on the domain dispatchers and the package/federation routes | runtime, rest | Get: passes with no `userId` | `domains/actions.ts:411`, `domains/ai.ts:60`, `domains/automation.ts:989`, `domains/meta.ts:232`, `domains/security.ts:78`, `domains/packages.ts:246`, `external-datasource-routes.ts:302`, `package-routes.ts:97` |
164164
| 53 | MCP principal check satisfied | runtime | Get: MCP surface reachable with no user | `domains/mcp.ts:61` |
@@ -199,7 +199,7 @@ assuming `isSystem` covers it is a documented source of bugs.
199199
| "It preserves a supplied `updated_at` / `updated_by`" | **No.** That is `preserveAudit`, a separate opt-in — and an UPDATE-path exemption only | `field.zod.ts:1451` (#3493 / #6640) |
200200
| "It stamps `created_by`" | **No.** Audit stamping reads `userId` from the context. A user-less system write stamps nothing — that is today's behaviour, not an error | `runtime-identity.ts:280``281` |
201201
| "It bypasses every guard" | **No.** The last-admin guard applies to **every** context, `isSystem` included — the deprovision path that actually locks an org out is the system one | `last-admin-guard.ts:273` |
202-
| "A client can request it" | **No.** Never settable from inbound HTTP or from an action body | `rest-server.ts:1238`, `:1267`; `domains/actions.ts:404` |
202+
| "A client can request it" | **No.** Never settable from inbound HTTP or from an action body | `rest-server.ts:1239`, `:1268`; `domains/actions.ts:404` |
203203

204204
---
205205

0 commit comments

Comments
 (0)