From 7bb7255f1ed21d57f33680699023593189b63991 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 15:03:40 +0000 Subject: [PATCH 1/4] 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 Claude-Session: https://claude.ai/code/session_01KX8wnyjStaZcuMyAMNsy3N --- .changeset/rest-meta-history-cast-retired.md | 31 +++ .changeset/spec-history-meta-item-member.md | 13 ++ content/docs/references/api/protocol.mdx | 46 ++++- content/docs/references/index.mdx | 10 +- ...07-unknown-key-strictness-ledger.counts.md | 2 +- packages/rest/src/rest-server.ts | 25 ++- packages/spec/api-surface/api.json | 4 + packages/spec/authorable-surface/api.json | 6 + packages/spec/export-origins/api.json | 4 + packages/spec/json-schema.manifest/api.json | 2 + packages/spec/src/api/protocol.test.ts | 177 ++++++++++++++++++ packages/spec/src/api/protocol.zod.ts | 163 ++++++++++++++++ .../src/type-alias-convention.pin.test.ts | 21 ++- 13 files changed, 490 insertions(+), 14 deletions(-) create mode 100644 .changeset/rest-meta-history-cast-retired.md create mode 100644 .changeset/spec-history-meta-item-member.md diff --git a/.changeset/rest-meta-history-cast-retired.md b/.changeset/rest-meta-history-cast-retired.md new file mode 100644 index 0000000000..f92009cffa --- /dev/null +++ b/.changeset/rest-meta-history-cast-retired.md @@ -0,0 +1,31 @@ +--- +"@objectstack/rest": patch +--- + +refactor(rest): the history door call site is compiled against the declared contract (#12005) + +The `GET /meta/:type/:name/history` door in `packages/rest/src/rest-server.ts` +reached its protocol method through `(p as any)` — once for the +feature-detection guard, once for the call — so the compiler checked nothing +about the request literal it built. The cast was load-bearing on **member +existence** (`historyMetaItem` was undeclared in `packages/spec` entirely — +removing the cast answered `TS2339`), the same half the audit twin's cast +carried before #11678. + +With `MetadataProtocol.historyMetaItem` declared (the spec half of this +landing), the guard is now `if (!p.historyMetaItem)` and the request is a named +const typed as `TransportScopedMetaRequest` — the +reset-door spelling, not the audit door's plain request type, because this door +still spreads the transport-level `environmentId` (long-standing wire shape, +deliberately unchanged; the #9741 ruling keeps it layered on by the wrapper +rather than becoming a protocol key). + +**No behaviour change of any kind, and nothing about the wire moves.** The +outgoing payload is byte-identical (same keys, same conditional spreads, same +`Number.isFinite` drops); the 501 refusal is untouched (its bare-string +envelope remains the #7035-family ratcheted debt it already was — converging it +is a behaviour change this declaration must not smuggle). The guard survives +with identical truthiness semantics: the member is declared **optional** (a +kernel may implement neither door), and the guard is also what narrows it to +callable at the call site. An undeclared key in the literal is now a compile +error instead of a payload member no contract has ever seen. diff --git a/.changeset/spec-history-meta-item-member.md b/.changeset/spec-history-meta-item-member.md new file mode 100644 index 0000000000..8a477549b3 --- /dev/null +++ b/.changeset/spec-history-meta-item-member.md @@ -0,0 +1,13 @@ +--- +"@objectstack/spec": minor +--- + +**`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). + +`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. + +Additive, not breaking: + +- `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. +- `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). +- `MetadataProtocol.historyMetaItem?(request: HistoryMetaItemRequest): Promise` — 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. diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index e509fd154d..8f0c261eff 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -12,8 +12,8 @@ description: Protocol protocol schemas ## TypeScript Usage ```typescript -import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AuditMetaItemRequestSchema, AuditMetaItemResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CloneDataResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DiffMetaItemResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, FindReferencesToMetaResponseSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaDiagnosticsResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemLayeredRequestSchema, GetMetaItemLayeredResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetPublishedMetaItemResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListDraftsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, NotificationSchema, NotificationPreferencesSchema, PublishMetaItemRequestSchema, PublishMetaItemResponseSchema, PublishPackageDraftsResponseSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, RollbackMetaItemResponseSchema, RuntimeAuthoringIssueSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SearchAllHitSchema, SearchAllResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UninstallPackageRequestSchema, UninstallPackageResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, ValidateDataIssueSchema, ValidateDataRequestSchema, ValidateDataResponseSchema } from '@objectstack/spec/api'; -import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AuditMetaItemRequest, AuditMetaItemResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CloneDataResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DiffMetaItemResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, FindReferencesToMetaResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaDiagnosticsResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemLayeredRequest, GetMetaItemLayeredResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetPublishedMetaItemResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListDraftsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, Notification, NotificationPreferences, PublishMetaItemRequest, PublishMetaItemResponse, PublishPackageDraftsResponse, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, RollbackMetaItemResponse, RuntimeAuthoringIssue, SaveMetaItemRequest, SaveMetaItemResponse, SearchAllHit, SearchAllResponse, SetPresenceRequest, SetPresenceResponse, UninstallPackageRequest, UninstallPackageResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, ValidateDataIssue, ValidateDataRequest, ValidateDataResponse } from '@objectstack/spec/api'; +import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AuditMetaItemRequestSchema, AuditMetaItemResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CloneDataResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DiffMetaItemResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, FindReferencesToMetaResponseSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaDiagnosticsResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemLayeredRequestSchema, GetMetaItemLayeredResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetPublishedMetaItemResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, HistoryMetaItemRequestSchema, HistoryMetaItemResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListDraftsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, NotificationSchema, NotificationPreferencesSchema, PublishMetaItemRequestSchema, PublishMetaItemResponseSchema, PublishPackageDraftsResponseSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, RollbackMetaItemResponseSchema, RuntimeAuthoringIssueSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SearchAllHitSchema, SearchAllResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UninstallPackageRequestSchema, UninstallPackageResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, ValidateDataIssueSchema, ValidateDataRequestSchema, ValidateDataResponseSchema } from '@objectstack/spec/api'; +import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AuditMetaItemRequest, AuditMetaItemResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CloneDataResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DiffMetaItemResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, FindReferencesToMetaResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaDiagnosticsResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemLayeredRequest, GetMetaItemLayeredResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetPublishedMetaItemResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, HistoryMetaItemRequest, HistoryMetaItemResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListDraftsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, Notification, NotificationPreferences, PublishMetaItemRequest, PublishMetaItemResponse, PublishPackageDraftsResponse, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, RollbackMetaItemResponse, RuntimeAuthoringIssue, SaveMetaItemRequest, SaveMetaItemResponse, SearchAllHit, SearchAllResponse, SetPresenceRequest, SetPresenceResponse, UninstallPackageRequest, UninstallPackageResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, ValidateDataIssue, ValidateDataRequest, ValidateDataResponse } from '@objectstack/spec/api'; // Validate data const result = AiAgentCapabilitiesSchema.parse(data); @@ -1787,6 +1787,48 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own | **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | +--- + +## HistoryMetaItemRequest + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Metadata type name | +| **name** | `string` | ✅ | Item name | +| **organizationId** | `string` | optional | Organization (tenant) partition the change log is read from. Absent means the env-wide partition (the implementation resolves the overlay repository as `organizationId ?? null`, keyed `org: 'env'`). Declared because the implementation declares and reads it; plain `string`, not nullable, because that is the implementation's parameter type — unlike the audit twin, whose door always sends `ctx?.tenantId ?? null` and whose implementation declares `string \| null`. The REST history door currently sends no organization at all (the #8747-family tenant-scoping question is a separate measurement for that door — declaring the member records the implementation contract, it does not answer that question). | +| **sinceSeq** | `number` | optional | Exclusive lower bound on `seq` for pagination: only events with `seq > sinceSeq` are returned. Absent means "from the beginning". | +| **limit** | `number` | optional | Maximum events to return, oldest first. Forwarded to the repository unclamped and with NO default — absent means the full remaining change log. (Deliberately no declared bounds: unlike the audit twin's [1, 500] clamp, nothing on this path clamps or refuses, so declaring `.min()`/`.max()` here would refuse values the shipped verb accepts.) | + + +--- + +## HistoryMetaItemResponse + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **events** | `{ seq: integer; op: Enum<'create' \| 'update' \| 'delete' \| 'rename' \| 'publish' \| 'revert'>; ref: object; hash: string \| null; … }[]` | ✅ | The durable change-log for the item, oldest first. See the schema-level note for what an empty array means — and what it never means. | + +### Nested Shape: `HistoryMetaItemResponse.events[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **seq** | `integer` | ✅ | Sequence number this write produced in the org log (sys_metadata_history.event_seq) — the token the request's `sinceSeq` filters on, and the same key the write-verb receipts (`SaveMetaItemResponseSchema.seq` and siblings) carry. | +| **op** | `Enum<'create' \| 'update' \| 'delete' \| 'rename' \| 'publish' \| 'revert'>` | ✅ | Which change-log operation the event records (ADR-0008 §2.4). Closed vocabulary, mirrored from the producer's own enum. | +| **ref** | `{ org: string; type: string; name: string; version?: string }` | ✅ | Which item the event is about. | +| **hash** | `string \| null` | ✅ | Content hash of the body this event wrote; `null` when the event wrote none (`op="delete"`). | +| **parentHash** | `string \| null` | ✅ | Hash the written version was derived from; `null` for a first version. | +| **version** | `integer` | optional | Per-(org,type,name) monotonic lineage counter at this event — the token `rollbackMetaItem({ toVersion })` pins. Absent when the row recorded none. | +| **previousName** | `string` | optional | Set on op="rename": the old machine name. | +| **actor** | `string \| null` | ✅ | Who wrote this. `null` = system-initiated (boot sync, migration, scheduled job) — never a sentinel string (#4556): consumers that resolve this against `sys_user` must be able to tell "nobody" from "a user id". | +| **message** | `string` | optional | Optional commit message recorded with the write. | +| **ts** | `string` | ✅ | When the write happened (ISO-8601 string). | +| **source** | `string` | ✅ | Origin label of the write: "fs", "studio", "rest", "ai", "git-import", … | + + --- ## HttpFindQueryParams diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index a0a4c2d5bd..03b24d1d0c 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,6 +1,6 @@ --- title: Protocol Reference -description: Every schema published by @objectstack/spec — 1595 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1597 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -20,7 +20,7 @@ counts are sums of the rows they head. Regenerate with | Module | Pages | Schemas | Description | | :--- | ---: | ---: | :--- | | [AI Protocol](/docs/references/ai) | 11 | 66 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. | -| [API Protocol](/docs/references/api) | 31 | 436 | REST contracts, endpoints, routing, realtime, batch, discovery. | +| [API Protocol](/docs/references/api) | 31 | 438 | REST contracts, endpoints, routing, realtime, batch, discovery. | | [Automation Protocol](/docs/references/automation) | 13 | 68 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. | | [Cloud Protocol](/docs/references/cloud) | 11 | 94 | Environments, packages and versions, marketplace, developer portal, tenancy. | | [Data Protocol](/docs/references/data) | 29 | 166 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. | @@ -33,7 +33,7 @@ counts are sums of the rows they head. Regenerate with | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 36 | 291 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | | [UI Protocol](/docs/references/ui) | 16 | 153 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **200** | **1595** | 14 protocol modules | +| **Total** | **200** | **1597** | 14 protocol modules | --- @@ -61,7 +61,7 @@ Agents, tools, skills, RAG and knowledge sources, model registry, conversations. ## API Protocol -**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **31 pages, 436 schemas** +**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **31 pages, 438 schemas** REST contracts, endpoints, routing, realtime, batch, discovery. @@ -88,7 +88,7 @@ REST contracts, endpoints, routing, realtime, batch, discovery. | [`package-api.zod.ts`](/docs/references/api/package-api) | `GetInstalledPackageRequest`, `GetInstalledPackageResponse`, `ListInstalledPackagesRequest`, `ListInstalledPackagesResponse`, `PackageApiErrorCode`, `PackageInstallRequest`, `PackageInstallResponse`, `PackagePathParams`, `PackageRollbackRequest`, `PackageUpgradeRequest`, `PackageUpgradeResponse`, `ResolveDependenciesRequest`, `ResolveDependenciesResponse`, `UninstallPackageApiRequest`, `UninstallPackageApiResponse`, `UploadArtifactRequest`, `UploadArtifactResponse` | | [`package-lifecycle.zod.ts`](/docs/references/api/package-lifecycle) | `DiscardPackageDraftsResponse`, `DuplicatePackageResponse`, `ListPackageCommitsResponse`, `PackageExportManifest`, `PackagePublishResult`, `ReassignOrphanedMetadataResponse`, `RevertPackageCommitResponse`, `RollbackToPackageCommitResponse` | | [`plugin-rest-api.zod.ts`](/docs/references/api/plugin-rest-api) | `ErrorHandlingConfig`, `HandlerStatus`, `OpenApiGenerationConfig`, `RequestValidationConfig`, `ResponseEnvelopeConfig`, `RestApiEndpoint`, `RestApiPluginConfig`, `RestApiRouteCategory`, `RestApiRouteRegistration`, `RouteCoverageEntry`, `RouteCoverageReport`, `ValidationMode` | -| [`protocol.zod.ts`](/docs/references/api/protocol) | `AiAgentCapabilities`, `AiAgentChatRequest`, `AiAgentSummary`, `AiAgentsResponse`, `AiChatRequest`, `AiChatResponse`, `AiCompleteRequest`, `AiConversation`, `AiMessage`, `AiModelsResponse`, `AiPendingAction`, `AiPendingActionStatus`, `AiStreamChunk`, `ApproveAiPendingActionResponse`, `AuditMetaItemRequest`, `AuditMetaItemResponse`, `AutomationActionsResponse`, `AutomationTriggerRequest`, `AutomationTriggerResponse`, `BatchDataRequest`, `BatchDataResponse`, `CheckPermissionRequest`, `CheckPermissionResponse`, `CloneDataResponse`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DiffMetaItemResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `FindReferencesToMetaResponse`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaDiagnosticsResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemLayeredRequest`, `GetMetaItemLayeredResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetPublishedMetaItemResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListDraftsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `Notification`, `NotificationPreferences`, `PublishMetaItemRequest`, `PublishMetaItemResponse`, `PublishPackageDraftsResponse`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `RollbackMetaItemResponse`, `RuntimeAuthoringIssue`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SearchAllHit`, `SearchAllResponse`, `SetPresenceRequest`, `SetPresenceResponse`, `UninstallPackageRequest`, `UninstallPackageResponse`, `UnregisterDeviceRequest`, `UnregisterDeviceResponse`, `UpdateAiConversationRequest`, `UpdateDataRequest`, `UpdateDataResponse`, `UpdateManyDataRequest`, `UpdateManyDataResponse`, `UpdateNotificationPreferencesRequest`, `UpdateNotificationPreferencesResponse`, `ValidateDataIssue`, `ValidateDataRequest`, `ValidateDataResponse` | +| [`protocol.zod.ts`](/docs/references/api/protocol) | `AiAgentCapabilities`, `AiAgentChatRequest`, `AiAgentSummary`, `AiAgentsResponse`, `AiChatRequest`, `AiChatResponse`, `AiCompleteRequest`, `AiConversation`, `AiMessage`, `AiModelsResponse`, `AiPendingAction`, `AiPendingActionStatus`, `AiStreamChunk`, `ApproveAiPendingActionResponse`, `AuditMetaItemRequest`, `AuditMetaItemResponse`, `AutomationActionsResponse`, `AutomationTriggerRequest`, `AutomationTriggerResponse`, `BatchDataRequest`, `BatchDataResponse`, `CheckPermissionRequest`, `CheckPermissionResponse`, `CloneDataResponse`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DiffMetaItemResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `FindReferencesToMetaResponse`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaDiagnosticsResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemLayeredRequest`, `GetMetaItemLayeredResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetPublishedMetaItemResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HistoryMetaItemRequest`, `HistoryMetaItemResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListDraftsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `Notification`, `NotificationPreferences`, `PublishMetaItemRequest`, `PublishMetaItemResponse`, `PublishPackageDraftsResponse`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `RollbackMetaItemResponse`, `RuntimeAuthoringIssue`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SearchAllHit`, `SearchAllResponse`, `SetPresenceRequest`, `SetPresenceResponse`, `UninstallPackageRequest`, `UninstallPackageResponse`, `UnregisterDeviceRequest`, `UnregisterDeviceResponse`, `UpdateAiConversationRequest`, `UpdateDataRequest`, `UpdateDataResponse`, `UpdateManyDataRequest`, `UpdateManyDataResponse`, `UpdateNotificationPreferencesRequest`, `UpdateNotificationPreferencesResponse`, `ValidateDataIssue`, `ValidateDataRequest`, `ValidateDataResponse` | | [`query-adapter.zod.ts`](/docs/references/api/query-adapter) | `ODataQueryAdapter`, `OperatorMapping`, `QueryAdapterConfig`, `QueryAdapterTarget`, `RestQueryAdapter` | | [`realtime.zod.ts`](/docs/references/api/realtime) | `RealtimeConfig`, `RealtimeEvent`, `RealtimeEventType`, `RealtimePresence`, `Subscription`, `SubscriptionEvent`, `TransportProtocol` | | [`realtime-shared.zod.ts`](/docs/references/api/realtime-shared) | `BasePresence`, `PresenceStatus`, `RealtimeRecordAction` | diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md index 8b06d600ef..6ecb446c23 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md @@ -257,7 +257,7 @@ directory rather than per file. | Dir | Sites | |---|---| | `ai/` | 77 | -| `api/` | 449 | +| `api/` | 453 | | `cloud/` | 83 | | `identity/` | 32 | | `integration/` | 10 | diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index 2f63997669..d748787f30 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -85,6 +85,7 @@ import type { GetMetaItemLayeredRequest, PublishMetaItemRequest, AuditMetaItemRequest, + HistoryMetaItemRequest, DeleteMetaItemRequest, } from '@objectstack/spec/api'; // [#8073] The closed ADR-0112 error vocabulary, so the explain family's single @@ -5995,7 +5996,14 @@ export class RestServer { try { const environmentId = isScoped ? req.params?.environmentId : undefined; const p = await this.resolveProtocol(environmentId, req); - if (!(p as any).historyMetaItem) { + // The cast came off when `MetadataProtocol` declared + // `historyMetaItem` (#12005 — the #11006 pattern, exactly + // as #11678 de-cast the audit twin below). The member is + // declared OPTIONAL, so this truthiness guard is not just + // feature detection: it is what narrows the member to + // callable at the call site. Same guard semantics as + // before, minus the cast. + if (!p.historyMetaItem) { res.status(501).json({ error: 'History query not supported by protocol implementation', }); @@ -6012,13 +6020,24 @@ export class RestServer { const limit = req.query?.limit !== undefined ? Number(req.query.limit) : undefined; - const result = await (p as any).historyMetaItem({ + // Typed through `TransportScopedMetaRequest` like the + // reset door above, NOT as a plain `HistoryMetaItemRequest` + // like the audit door below: this door still spreads the + // transport-level `environmentId` (long-standing wire + // shape, deliberately unchanged — the #9741 ruling keeps + // it out of the protocol schema, and the implementation + // never reads it), so the wrapper is what layers that one + // member on. Every OTHER key is compiled against the spec + // contract — an undeclared member here is now TS2353 + // instead of a payload member no contract has ever seen. + const historyRequest: TransportScopedMetaRequest = { type: req.params.type, name: req.params.name, ...(environmentId ? { environmentId } : {}), ...(sinceSeq !== undefined && Number.isFinite(sinceSeq) ? { sinceSeq } : {}), ...(limit !== undefined && Number.isFinite(limit) ? { limit } : {}), - }); + }; + const result = await p.historyMetaItem(historyRequest); res.json(result); } catch (error: any) { handleRouteError(res, error); diff --git a/packages/spec/api-surface/api.json b/packages/spec/api-surface/api.json index 9c2a662b54..2f1af995ab 100644 --- a/packages/spec/api-surface/api.json +++ b/packages/spec/api-surface/api.json @@ -497,6 +497,10 @@ "GetUiViewResponseSchema (const)", "HandlerStatus (type)", "HandlerStatusSchema (const)", + "HistoryMetaItemRequest (type)", + "HistoryMetaItemRequestSchema (const)", + "HistoryMetaItemResponse (type)", + "HistoryMetaItemResponseSchema (const)", "HttpFindQueryParamsSchema (const)", "HttpMethod (type)", "HttpStatusErrorCodeMap (const)", diff --git a/packages/spec/authorable-surface/api.json b/packages/spec/authorable-surface/api.json index 8b9f23fae4..43d3d2c2d8 100644 --- a/packages/spec/authorable-surface/api.json +++ b/packages/spec/authorable-surface/api.json @@ -851,6 +851,12 @@ "api/GetUiViewResponse:name", "api/GetUiViewResponse:object", "api/GetUiViewResponse:protection", + "api/HistoryMetaItemRequest:limit", + "api/HistoryMetaItemRequest:name", + "api/HistoryMetaItemRequest:organizationId", + "api/HistoryMetaItemRequest:sinceSeq", + "api/HistoryMetaItemRequest:type", + "api/HistoryMetaItemResponse:events", "api/HttpFindQueryParams:count", "api/HttpFindQueryParams:distinct [RETIRED]", "api/HttpFindQueryParams:expand", diff --git a/packages/spec/export-origins/api.json b/packages/spec/export-origins/api.json index 09a299728e..4467fd490a 100644 --- a/packages/spec/export-origins/api.json +++ b/packages/spec/export-origins/api.json @@ -497,6 +497,10 @@ "GetUiViewResponseSchema": "src/api/protocol.zod.ts#GetUiViewResponseSchema (const)", "HandlerStatus": "src/api/plugin-rest-api.zod.ts#HandlerStatus (type)", "HandlerStatusSchema": "src/api/plugin-rest-api.zod.ts#HandlerStatusSchema (const)", + "HistoryMetaItemRequest": "src/api/protocol.zod.ts#HistoryMetaItemRequest (type)", + "HistoryMetaItemRequestSchema": "src/api/protocol.zod.ts#HistoryMetaItemRequestSchema (const)", + "HistoryMetaItemResponse": "src/api/protocol.zod.ts#HistoryMetaItemResponse (type)", + "HistoryMetaItemResponseSchema": "src/api/protocol.zod.ts#HistoryMetaItemResponseSchema (const)", "HttpFindQueryParamsSchema": "src/api/protocol.zod.ts#HttpFindQueryParamsSchema (const)", "HttpMethod": "src/shared/http.zod.ts#HttpMethod (type)", "HttpStatusErrorCodeMap": "src/api/errors.zod.ts#HttpStatusErrorCodeMap (const)", diff --git a/packages/spec/json-schema.manifest/api.json b/packages/spec/json-schema.manifest/api.json index 7cf65f9c8f..fde7da17d5 100644 --- a/packages/spec/json-schema.manifest/api.json +++ b/packages/spec/json-schema.manifest/api.json @@ -208,6 +208,8 @@ "api/GetUiViewRequest", "api/GetUiViewResponse", "api/HandlerStatus", + "api/HistoryMetaItemRequest", + "api/HistoryMetaItemResponse", "api/HttpFindQueryParams", "api/HttpMethod", "api/IdRequest", diff --git a/packages/spec/src/api/protocol.test.ts b/packages/spec/src/api/protocol.test.ts index a8353c5dd2..f2d143c300 100644 --- a/packages/spec/src/api/protocol.test.ts +++ b/packages/spec/src/api/protocol.test.ts @@ -2074,6 +2074,183 @@ describe('MetadataProtocol declares auditMetaItem (#11678)', () => { }); }); +import { HistoryMetaItemRequestSchema, HistoryMetaItemResponseSchema } from './protocol.zod'; +import type { HistoryMetaItemRequest, HistoryMetaItemResponse } from './protocol.zod'; + +describe('HistoryMetaItemRequestSchema mirrors the implementation parameter type (#12005)', () => { + // The history door is the audit door's explicitly named twin + // (`rest-server.ts` says so at the `Number(...)`-shape comment) and was in + // exactly the state the audit door left via #11678/PR #12003: NEITHER side + // declared, the REST call site reaching the verb through `(p as any)` twice + // (guard + call). The measure is the implementation's parameter type in + // `@objectstack/metadata-protocol` — + // `{ type, name, organizationId?: string, sinceSeq?: number, limit?: number }` + // — and the REST door's actual sends; nothing else is declared because + // nothing else is enforced. As in the #11678 block above, accept-pins + // assert the parsed VALUE: this is a non-strict object, so `success` alone + // is exactly the silent-strip state this family of cards closes. + + const base = { type: 'view', name: 'account_list' } as const; + + it('accepts the full request and PRESERVES every member through parse', () => { + const full = { ...base, organizationId: 'org_alpha', sinceSeq: 3, limit: 50 }; + const result = HistoryMetaItemRequestSchema.safeParse(full); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual(full); + } + }); + + it('requires type AND name — the change log is per item', () => { + expect(HistoryMetaItemRequestSchema.safeParse(base).success).toBe(true); + expect(HistoryMetaItemRequestSchema.safeParse({ type: 'view' }).success).toBe(false); + expect(HistoryMetaItemRequestSchema.safeParse({ name: 'account_list' }).success).toBe(false); + }); + + it('organizationId is an optional string — the implementation\'s declared type, not the audit twin\'s nullable', () => { + // The audit twin declares `string | null` because ITS implementation + // does and its door always sends `ctx?.tenantId ?? null`. This + // implementation declares plain `organizationId?: string`, and this door + // sends no organization at all — so the mirror is `.optional()` without + // `.nullable()`. (Whether the door SHOULD send one is the #8747-family + // measurement the card fences to a future issue, deliberately not + // answered by this declaration.) + const withOrg = HistoryMetaItemRequestSchema.safeParse({ ...base, organizationId: 'org_alpha' }); + expect(withOrg.success).toBe(true); + if (withOrg.success) { + expect((withOrg.data as { organizationId?: string }).organizationId).toBe('org_alpha'); + } + expect(HistoryMetaItemRequestSchema.safeParse(base).success).toBe(true); + expect(HistoryMetaItemRequestSchema.safeParse({ ...base, organizationId: 42 }).success).toBe(false); + }); + + it('sinceSeq and limit are optional numbers — values, not bags', () => { + expect(HistoryMetaItemRequestSchema.safeParse(base).success).toBe(true); + expect(HistoryMetaItemRequestSchema.safeParse({ ...base, sinceSeq: '3' }).success).toBe(false); + expect(HistoryMetaItemRequestSchema.safeParse({ ...base, limit: '50' }).success).toBe(false); + // The implementation forwards `limit` to the repository UNCLAMPED (no + // [1, 500] clamp here, unlike the audit twin), so the schema deliberately + // declares no bounds — `.max()` would refuse a value the shipped verb + // accepts. + const unclamped = HistoryMetaItemRequestSchema.safeParse({ ...base, limit: 9999 }); + expect(unclamped.success).toBe(true); + }); + + it('does not declare environmentId — transport-level by the #9741 ruling, stripped and shape-absent', () => { + // Same regression guard as the audit block above, with one twist the + // audit twin no longer has: this door STILL spreads `environmentId` into + // its outgoing payload (dead weight — the implementation never declares + // or reads it). That wire member rides the door's + // `TransportScopedMetaRequest` wrapper in `packages/rest`; it must never + // become a protocol key here. + const result = HistoryMetaItemRequestSchema.safeParse({ ...base, environmentId: 'env_alpha' }); + expect(result.success).toBe(true); + if (result.success) { + expect('environmentId' in (result.data as object)).toBe(false); + } + const shape = (HistoryMetaItemRequestSchema as unknown as { shape: Record }).shape; + expect(Object.keys(shape)).not.toContain('environmentId'); + }); +}); + +describe('HistoryMetaItemResponseSchema declares the change-log body (#12005)', () => { + /** + * A verbatim-shaped capture of a real `historyMetaItem` return: one update + * (all optional members present) followed by the delete tombstone + * (`hash: null`, system actor, the sys-repository's fallback `source`, no + * `version`/`message` recorded — `rowToEvent` omits both when the row + * carries neither). + */ + const realResponse = { + events: [ + { + seq: 12, + op: 'update', + ref: { org: 'org_alpha', type: 'view', name: 'account_list' }, + hash: 'sha256:6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b', + parentHash: 'sha256:d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35', + version: 3, + actor: 'admin@objectos.ai', + message: 'tweak columns', + ts: '2026-08-25T02:11:09.000Z', + source: 'studio', + }, + { + seq: 13, + op: 'delete', + ref: { org: 'org_alpha', type: 'view', name: 'account_list' }, + hash: null, + parentHash: 'sha256:6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b', + actor: null, + ts: '2026-08-25T02:12:41.000Z', + source: 'sys-metadata-repo', + }, + ], + }; + + it('parses the real event shapes and PRESERVES every member', () => { + const result = HistoryMetaItemResponseSchema.safeParse(realResponse); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual(realResponse); + } + }); + + it('the honest-empty answer parses — {events: []} is a declared, legal body', () => { + // `[]` is the honest answer for a clean change log AND for a non-overlay + // metadata type (no history by construction — the implementation answers + // `[]` instead of throwing). The capability gap is NOT in this body: a + // protocol without the verb is refused 501 before the call. + const result = HistoryMetaItemResponseSchema.safeParse({ events: [] }); + expect(result.success).toBe(true); + if (result.success) { + expect((result.data as { events: unknown[] }).events).toEqual([]); + } + }); + + it('keeps the op vocabulary closed, and ref.type deliberately open', () => { + const bad = (patch: Record) => + HistoryMetaItemResponseSchema.safeParse({ events: [{ ...realResponse.events[0], ...patch }] }); + // `op` mirrors the producer's own closed enum (ADR-0008 §2.4)… + expect(bad({ op: 'save' }).success).toBe(false); + // …while `ref.type` is a plain string BY INTENT: plugin runtime-create + // types flow through this door and are registry-absent by design, so the + // static registry enum would refuse real rows (the schema-level note + // carries the #12038 1C reasoning). + const pluginType = bad({ ref: { org: 'org_alpha', type: 'capability', name: 'account_list' } }); + expect(pluginType.success).toBe(true); + }); +}); + +describe('MetadataProtocol declares historyMetaItem (#12005)', () => { + // Type-level pins, same pattern as the #11678 audit block above: before + // this declaration the casts at the REST call site carried MEMBER-EXISTENCE + // weight (TS2339, not TS2353), so the request literal there was typed by + // nothing. These pins are what turns red if the member is dropped again or + // drifts off the history schemas. + + it('declares the member optional, against the history request/response schemas', () => { + expectTypeOf().toEqualTypeOf< + ((request: HistoryMetaItemRequest) => Promise) | undefined + >(); + // An implementation without the verb still type-checks against the + // interface — the CONFORMING-deployment half of the door's 501 refusal. + const absent: Pick = {}; + expect('historyMetaItem' in absent).toBe(false); + }); + + it('refuses an undeclared key at the member call shape', () => { + const good: HistoryMetaItemRequest = { type: 'view', name: 'account_list', sinceSeq: 3, limit: 50 }; + expect(good.type).toBe('view'); + // @ts-expect-error `environmentId` is transport-level (#9741) — not a declared request member (the REST door's spread of it rides the TransportScopedMetaRequest wrapper, never this shape). + const withEnv: HistoryMetaItemRequest = { type: 'view', name: 'account_list', environmentId: 'env_a' }; + expect(withEnv.name).toBe('account_list'); + // @ts-expect-error an undeclared (here: misspelt) key is refused at the call shape. + const misspelt: HistoryMetaItemRequest = { type: 'view', name: 'account_list', sinceSeqs: 3 }; + expect(misspelt.name).toBe('account_list'); + }); +}); + import { DeleteMetaItemRequestSchema } from './protocol.zod'; // `DeleteMetaItemResponse` is imported once, above, beside the response-side // suite that pins its two #13155 keys — the member pin below reads it from diff --git a/packages/spec/src/api/protocol.zod.ts b/packages/spec/src/api/protocol.zod.ts index 163951fdc1..f003f0138a 100644 --- a/packages/spec/src/api/protocol.zod.ts +++ b/packages/spec/src/api/protocol.zod.ts @@ -1375,6 +1375,147 @@ export const AuditMetaItemResponseSchema = lazySchema(() => z.object({ ), })); +/** + * History Metadata Item Request + * + * Request shape for `GET /api/v1/meta/:type/:name/history` (the + * `historyMetaItem` protocol method) — the durable change-log for one item: + * the `sys_metadata_history` events (every overlay put/delete, ADR-0008 §2.4) + * that Studio's History tab renders as a timeline. Mirrors the + * implementation's parameter type in `@objectstack/metadata-protocol` member + * for member — a declared-surface catch-up, not a new capability (the #11006 + * maintainer-ruled pattern, 2026-08-22 option B, carried one door over + * exactly as #11678 carried it to the audit twin): the verb and every member + * here already ship and are enforced. + * + * `environmentId` is deliberately NOT declared — the transport-level + * multi-kernel routing key is OUT of protocol request shapes by the #9741 + * maintainer ruling (2026-08-18): `resolveProtocol(environmentId)` selects + * the target kernel before this method is entered, and the implementation + * never declares or reads it off the request. Unlike the audit twin (whose + * door stopped sending it when #8747 scoped the read), the REST history door + * still spreads it into the outgoing payload; that long-standing wire member + * rides the door's `TransportScopedMetaRequest` wrapper, never this schema. + */ +export const HistoryMetaItemRequestSchema = lazySchema(() => z.object({ + type: z.string().describe('Metadata type name'), + name: z.string().describe('Item name'), + organizationId: z.string().optional().describe( + 'Organization (tenant) partition the change log is read from. Absent ' + + 'means the env-wide partition (the implementation resolves the overlay ' + + 'repository as `organizationId ?? null`, keyed `org: \'env\'`). ' + + 'Declared because the implementation declares and reads it; plain ' + + '`string`, not nullable, because that is the implementation\'s ' + + 'parameter type — unlike the audit twin, whose door always sends ' + + '`ctx?.tenantId ?? null` and whose implementation declares ' + + '`string | null`. The REST history door currently sends no ' + + 'organization at all (the #8747-family tenant-scoping question is a ' + + 'separate measurement for that door — declaring the member records ' + + 'the implementation contract, it does not answer that question).', + ), + sinceSeq: z.number().optional().describe( + 'Exclusive lower bound on `seq` for pagination: only events with ' + + '`seq > sinceSeq` are returned. Absent means "from the beginning".', + ), + limit: z.number().optional().describe( + 'Maximum events to return, oldest first. Forwarded to the repository ' + + 'unclamped and with NO default — absent means the full remaining ' + + 'change log. (Deliberately no declared bounds: unlike the audit ' + + 'twin\'s [1, 500] clamp, nothing on this path clamps or refuses, so ' + + 'declaring `.min()`/`.max()` here would refuse values the shipped ' + + 'verb accepts.)', + ), +})); + +/** + * History Metadata Item Response + * + * The body of `GET /api/v1/meta/:type/:name/history`, mirrored member for + * member from the implementation's return type + * (`ObjectStackProtocolImplementation.historyMetaItem` returns + * `{ events: MetadataEvent[] }`), in `seq` order — oldest first, the + * opposite end of the log from the audit twin's newest-first trail. + * + * The event shape transcribes `MetadataEventSchema` from + * `@objectstack/metadata-core` (ADR-0008 §2.4) — the spec cannot import that + * package (dependency direction), so the shape is transcribed here the same + * way the audit twin transcribed its event rows (#11678). Two deliberate + * widenings against the source schema, both because a declared-surface + * catch-up must not refuse bodies the shipped verb yields: `ref.type` is a + * plain string rather than the static metadata-type registry enum (plugin + * runtime-create types flow through this door and are registry-absent by + * design — freezing the registry into this contract is the same drift the + * #12038 1C ruling refused for the published-body route), and `ref.name` + * carries no spelling regex. + * + * `{ events: [] }` is the honest answer for a genuinely empty change log AND + * for a non-overlay metadata type (neither `allowOrgOverride` nor + * `allowRuntimeCreate`): such types have no history by construction — + * `saveMetaItem` refuses them outright (#5086) — and the implementation + * answers `[]` rather than throwing so callers can treat "no history" + * uniformly. It is NEVER the answer for a protocol that lacks the verb: the + * REST door refuses 501 before the call. + */ +export const HistoryMetaItemResponseSchema = lazySchema(() => z.object({ + events: z.array(z.object({ + seq: z.number().int().nonnegative().describe( + 'Sequence number this write produced in the org log ' + + '(sys_metadata_history.event_seq) — the token the request\'s ' + + '`sinceSeq` filters on, and the same key the write-verb receipts ' + + '(`SaveMetaItemResponseSchema.seq` and siblings) carry.', + ), + op: z.enum(['create', 'update', 'delete', 'rename', 'publish', 'revert']).describe( + 'Which change-log operation the event records (ADR-0008 §2.4). Closed ' + + 'vocabulary, mirrored from the producer\'s own enum.', + ), + ref: z.object({ + org: z.string().describe( + 'Tenant/org partition the row lives in; `env` for the env-wide ' + + 'partition, `system` for built-ins.', + ), + type: z.string().describe( + 'Canonical singular metadata type key. A plain string by intent — ' + + 'plugin runtime-create types flow through this door and are ' + + 'registry-absent by design (see the schema-level note).', + ), + name: z.string().describe('Item machine name.'), + version: z.string().optional().describe( + 'Optional version pin (content hash); omitted for HEAD.', + ), + }).describe('Which item the event is about.'), + hash: z.string().nullable().describe( + 'Content hash of the body this event wrote; `null` when the event ' + + 'wrote none (`op="delete"`).', + ), + parentHash: z.string().nullable().describe( + 'Hash the written version was derived from; `null` for a first ' + + 'version.', + ), + version: z.number().int().positive().optional().describe( + 'Per-(org,type,name) monotonic lineage counter at this event — the ' + + 'token `rollbackMetaItem({ toVersion })` pins. Absent when the row ' + + 'recorded none.', + ), + previousName: z.string().optional().describe('Set on op="rename": the old machine name.'), + actor: z.string().nullable().describe( + 'Who wrote this. `null` = system-initiated (boot sync, migration, ' + + 'scheduled job) — never a sentinel string (#4556): consumers that ' + + 'resolve this against `sys_user` must be able to tell "nobody" from ' + + '"a user id".', + ), + message: z.string().optional().describe('Optional commit message recorded with the write.'), + ts: z.string().describe('When the write happened (ISO-8601 string).'), + source: z.string().describe( + 'Origin label of the write: "fs", "studio", "rest", "ai", ' + + '"git-import", …', + ), + })).describe( + 'The durable change-log for the item, oldest first. See the ' + + 'schema-level note for what an empty array means — and what it never ' + + 'means.', + ), +})); + // ========================================== // Meta history / diagnostics family (#12038) // ========================================== @@ -2813,6 +2954,8 @@ export type DeleteMetaItemRequest = z.input; export type DeleteMetaItemResponse = z.input; export type AuditMetaItemRequest = z.input; export type AuditMetaItemResponse = z.input; +export type HistoryMetaItemRequest = z.input; +export type HistoryMetaItemResponse = z.input; /** Opaque by ruling (#12038 1C) — see {@link GetPublishedMetaItemResponseSchema}. */ export type GetPublishedMetaItemResponse = z.input; /** Post-parse shape of {@link GetPublishedMetaItemResponse} — defaults applied, transforms run (ADR-0122). */ @@ -3121,6 +3264,26 @@ export interface MetadataProtocol { * capability is never reported as an empty trail). */ auditMetaItem?(request: AuditMetaItemRequest): Promise; + /** + * Durable change-log read (`GET /api/v1/meta/:type/:name/history`) — the + * `sys_metadata_history` events for one item, oldest first, that Studio's + * History tab renders as a timeline (the audit member above serves the + * sibling 审计日志 / Audit log tab; ADR-0008 §2.4 is the event contract). + * Declared optional like its `auditMetaItem` / `deleteMetaItem` / + * `getMetaItemLayered` siblings: additive to a shipped contract, with the + * implementation (`@objectstack/metadata-protocol`) predating the + * declaration. Promotes what was an ADR-0076 D9 server-only extension into + * a declared optional member (the #11006 maintainer-ruled pattern, + * 2026-08-22 option B, carried one door over exactly as #11678 carried it + * to the audit twin) — before this, the REST history door reached the verb + * through a runtime cast and its request literal was compiled against + * nothing. A host without the verb is CONFORMING: the REST door + * feature-detects and answers 501 before the call. A host WITH the verb + * answers `{ events: [] }` for a non-overlay type — no history exists by + * construction — which is a declared, honest body, never a capability + * signal. + */ + historyMetaItem?(request: HistoryMetaItemRequest): Promise; getUiView?(request: GetUiViewRequest): Promise; } diff --git a/packages/spec/src/type-alias-convention.pin.test.ts b/packages/spec/src/type-alias-convention.pin.test.ts index 66ea8ab6d1..2dd0e2893d 100644 --- a/packages/spec/src/type-alias-convention.pin.test.ts +++ b/packages/spec/src/type-alias-convention.pin.test.ts @@ -269,7 +269,7 @@ import type * as M170 from './ui/component.zod.js'; import type * as M183 from './api/sortability.zod.js'; // --------------------------------------------------------------------------- -// 833 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. +// 835 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. // // That number is machine-checked, not hand-kept. The runtime companion at the // bottom of this file recomputes the pin count from the source and asserts that @@ -486,6 +486,8 @@ export type Iso137 = Assert, z.infer< typeof M28.DeleteMetaItemResponseSchema > >>; export type Iso857 = Assert, z.infer< typeof M28.AuditMetaItemRequestSchema > >>; export type Iso858 = Assert, z.infer< typeof M28.AuditMetaItemResponseSchema > >>; +export type Iso863 = Assert, z.infer< typeof M28.HistoryMetaItemRequestSchema > >>; +export type Iso864 = Assert, z.infer< typeof M28.HistoryMetaItemResponseSchema > >>; export type Iso139 = Assert, z.infer< typeof M28.GetMetaItemCachedRequestSchema > >>; export type Iso140 = Assert, z.infer< typeof M28.GetUiViewRequestSchema > >>; export type Iso141 = Assert, z.infer< typeof M28.AutomationTriggerRequestSchema > >>; @@ -1679,7 +1681,7 @@ describe('ADR-0122 type-alias convention', () => { // this title and the section header above the pin list — are now asserted // against the recomputed count below, so neither can go stale without a red // test naming it. - it('still declares all 833 isomorphic pins', () => { + it('still declares all 835 isomorphic pins', () => { // The truth of each pin is proved by tsc, not here — an `Assert>` // that stops holding is a compile error with the alias named. What tsc // cannot notice is a pin that was DELETED: removing the assertion removes @@ -2023,9 +2025,22 @@ describe('ADR-0122 type-alias convention', () => { // `MergeConflictSchema` / `MergeResultSchema`) left with the module. -4 // removed; the Iso numbers and the `M86` module number stay vacant (ids // are claims about pins, not positions). + // + // 833 -> 835 is #12005's `HistoryMetaItemRequestSchema` / + // `HistoryMetaItemResponseSchema` — the history door declared on the + // #11006 pattern, exactly as #11678 (PR #12003) declared its audit twin. + // Isomorphism MEASURED, not assumed: the request is two required + // `z.string()`s, an optional `z.string()` and two optional + // `z.number()`s; the response is one `z.array` of a plain object of + // strings (some `.nullable()`, some `.optional()`), `z.number().int()`s, + // one closed `z.enum` and one nested plain object of strings — no + // `.default()`, `.transform()`, `.catch()` or `.pipe()` anywhere in + // either tree, so the two shapes coincide and ADR-0122 gives each a pin + // rather than an `XParsed`. Ids `Iso863`/`Iso864`, the next free ones — + // ids are claims about pins, not positions. const self = readFileSync(fileURLToPath(import.meta.url), 'utf8'); const pins = self.match(/^export type Iso\d+ = Assert Date: Sun, 30 Aug 2026 15:08:31 +0000 Subject: [PATCH 2/4] 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 Claude-Session: https://claude.ai/code/session_01KX8wnyjStaZcuMyAMNsy3N --- content/docs/references/api/protocol.mdx | 4 ++-- packages/spec/src/api/protocol.zod.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index 8f0c261eff..eff9ac8f8f 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -1797,7 +1797,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own | :--- | :--- | :--- | :--- | | **type** | `string` | ✅ | Metadata type name | | **name** | `string` | ✅ | Item name | -| **organizationId** | `string` | optional | Organization (tenant) partition the change log is read from. Absent means the env-wide partition (the implementation resolves the overlay repository as `organizationId ?? null`, keyed `org: 'env'`). Declared because the implementation declares and reads it; plain `string`, not nullable, because that is the implementation's parameter type — unlike the audit twin, whose door always sends `ctx?.tenantId ?? null` and whose implementation declares `string \| null`. The REST history door currently sends no organization at all (the #8747-family tenant-scoping question is a separate measurement for that door — declaring the member records the implementation contract, it does not answer that question). | +| **organizationId** | `string` | optional | Organization (tenant) partition the change log is read from. Absent means the env-wide partition (the implementation resolves the overlay repository as `organizationId ?? null`, keyed `org: 'env'`). Declared because the implementation declares and reads it; plain `string`, not nullable, because that is the implementation's parameter type — unlike the audit twin, whose door always sends `ctx?.tenantId ?? null` and whose implementation declares `string \| null`. The REST history door currently sends no organization at all (whether it should is a tenant-scoping question measured separately for that door — declaring the member records the implementation contract, it does not answer that question). | | **sinceSeq** | `number` | optional | Exclusive lower bound on `seq` for pagination: only events with `seq > sinceSeq` are returned. Absent means "from the beginning". | | **limit** | `number` | optional | Maximum events to return, oldest first. Forwarded to the repository unclamped and with NO default — absent means the full remaining change log. (Deliberately no declared bounds: unlike the audit twin's [1, 500] clamp, nothing on this path clamps or refuses, so declaring `.min()`/`.max()` here would refuse values the shipped verb accepts.) | @@ -1823,7 +1823,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own | **parentHash** | `string \| null` | ✅ | Hash the written version was derived from; `null` for a first version. | | **version** | `integer` | optional | Per-(org,type,name) monotonic lineage counter at this event — the token `rollbackMetaItem({ toVersion })` pins. Absent when the row recorded none. | | **previousName** | `string` | optional | Set on op="rename": the old machine name. | -| **actor** | `string \| null` | ✅ | Who wrote this. `null` = system-initiated (boot sync, migration, scheduled job) — never a sentinel string (#4556): consumers that resolve this against `sys_user` must be able to tell "nobody" from "a user id". | +| **actor** | `string \| null` | ✅ | Who wrote this. `null` = system-initiated (boot sync, migration, scheduled job) — never a sentinel string: consumers that resolve this against `sys_user` must be able to tell "nobody" from "a user id". | | **message** | `string` | optional | Optional commit message recorded with the write. | | **ts** | `string` | ✅ | When the write happened (ISO-8601 string). | | **source** | `string` | ✅ | Origin label of the write: "fs", "studio", "rest", "ai", "git-import", … | diff --git a/packages/spec/src/api/protocol.zod.ts b/packages/spec/src/api/protocol.zod.ts index f003f0138a..df69e0725e 100644 --- a/packages/spec/src/api/protocol.zod.ts +++ b/packages/spec/src/api/protocol.zod.ts @@ -1409,8 +1409,8 @@ export const HistoryMetaItemRequestSchema = lazySchema(() => z.object({ + 'parameter type — unlike the audit twin, whose door always sends ' + '`ctx?.tenantId ?? null` and whose implementation declares ' + '`string | null`. The REST history door currently sends no ' - + 'organization at all (the #8747-family tenant-scoping question is a ' - + 'separate measurement for that door — declaring the member records ' + + 'organization at all (whether it should is a tenant-scoping question ' + + 'measured separately for that door — declaring the member records ' + 'the implementation contract, it does not answer that question).', ), sinceSeq: z.number().optional().describe( @@ -1499,7 +1499,7 @@ export const HistoryMetaItemResponseSchema = lazySchema(() => z.object({ previousName: z.string().optional().describe('Set on op="rename": the old machine name.'), actor: z.string().nullable().describe( 'Who wrote this. `null` = system-initiated (boot sync, migration, ' - + 'scheduled job) — never a sentinel string (#4556): consumers that ' + + 'scheduled job) — never a sentinel string: consumers that ' + 'resolve this against `sys_user` must be able to tell "nobody" from ' + '"a user id".', ), From aad3d736fe77ccf32437a7bdfc82c57658390303 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 16:31:25 +0000 Subject: [PATCH 3/4] 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 Claude-Session: https://claude.ai/code/session_01KX8wnyjStaZcuMyAMNsy3N --- content/docs/permissions/system-context.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/content/docs/permissions/system-context.mdx b/content/docs/permissions/system-context.mdx index 92bf0390d5..dc5873f4f6 100644 --- a/content/docs/permissions/system-context.mdx +++ b/content/docs/permissions/system-context.mdx @@ -64,7 +64,7 @@ not on any flag. ## How the flag is set `isSystem` is **server-constructed and never client-supplied**. Inbound HTTP -cannot set it (`packages/rest/src/rest-server.ts:1235`, `:1264`), and neither +cannot set it (`packages/rest/src/rest-server.ts:1236`, `:1265`), and neither can an action body (`packages/runtime/src/domains/actions.ts:404`). It is written by internal callers only, as an option on the engine call: @@ -103,7 +103,7 @@ that silently does not happen. | 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` | | 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` | | 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` | -| 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:1267` | +| 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:1268` | ### 2. Write pipeline and data integrity @@ -158,7 +158,7 @@ The largest single consumer — **20 of the 109 sites**. |:--|:---|:---|:---|:---| | 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` | | 49 | Action `requiredPermissions` bypassed | runtime | Get: engine self-invocation runs any action | `action-execution.ts:399` | -| 50 | `manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability | `domains/meta.ts:471`, `:874`, `rest-server.ts:4271`, `:5634`, `:5866`, `:6211`, `:6404` | +| 50 | `manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability | `domains/meta.ts:471`, `:874`, `rest-server.ts:4272`, `:5635`, `:5867`, `:6230`, `:6423` | | 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` | | 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:92` | | 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. | "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) | | "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` | | "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` | -| "A client can request it" | **No.** Never settable from inbound HTTP or from an action body | `rest-server.ts:1235`, `:1264`; `domains/actions.ts:404` | +| "A client can request it" | **No.** Never settable from inbound HTTP or from an action body | `rest-server.ts:1236`, `:1265`; `domains/actions.ts:404` | --- From c8a5d8b48ccdebd1db52b7e91f174c0e1ddbdf3c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 00:41:04 +0000 Subject: [PATCH 4/4] docs(spec): regenerate protocol.mdx from the merged tree Discharges the regeneration the merge commit deferred (os-regen). Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01KX8wnyjStaZcuMyAMNsy3N --- content/docs/references/api/protocol.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index eff9ac8f8f..357e83e356 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -1887,10 +1887,10 @@ Install package request | **capabilities** | `never` | optional | [REMOVED] `manifest.capabilities` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no discovery path ever consulted the block: nothing read `implements`, `provides`, `requires`, `extensionPoints` or `extensions`, so the declared "interoperability and automatic discovery" never happened. Delete the key. Real dependency resolution runs off top-level `manifest.dependencies`, which stays. Capability-based discovery must be designed with an enforcing reader first, not revived here. | | **extensions** | `never` | optional | [REMOVED] `manifest.extensions` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — an untyped map with zero readers: whatever was parked here was stored and never consulted. Delete the key. Extend the platform through the enforced channels instead: `contributes.kinds` registers metadata kinds, `navigationContributions` injects navigation into other packages' apps, and code-level extension happens in the plugin itself (`init`/`start`). | | **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +8 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages | -| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. | +| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — and the plugin trust tier (`manifest.runtime`) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. Use the permission declarations, which are enforced. | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | | **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) | -| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier (ADR-0025 §3.6) | +| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (unverified publisher requesting `node` → HTTP 422 + manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares | | **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) | | **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) |