diff --git a/.changeset/rest-meta-audit-reset-casts-retired.md b/.changeset/rest-meta-audit-reset-casts-retired.md new file mode 100644 index 0000000000..77831ac848 --- /dev/null +++ b/.changeset/rest-meta-audit-reset-casts-retired.md @@ -0,0 +1,34 @@ +--- +"@objectstack/rest": patch +--- + +refactor(rest): the audit and reset door call sites are compiled against the declared contract (#11678, #11679) + +The `GET /meta/:type/:name/audit` and `DELETE /meta/:type/:name` doors in +`packages/rest/src/rest-server.ts` reached their protocol methods through +`(p as any)` — once for each feature-detection guard, once for each call — so +the compiler checked nothing about the request literals they built. The two +casts were load-bearing in opposite ways, both measured: the audit door's on +**member existence** (`auditMetaItem` was undeclared in `packages/spec` +entirely — removing the cast answered `TS2339`), the reset door's on **request +shape** (`deleteMetaItem` was declared, but its request schema carried 2 of the +8 members the door sends — removing the cast answered `TS2353` on six keys). + +With `MetadataProtocol.auditMetaItem` declared and +`DeleteMetaItemRequestSchema` caught up (the spec half of this landing), the +guards are now `typeof p.auditMetaItem !== 'function'` / `if (!p.deleteMetaItem)` +and each request is a named const typed against the spec contract — the reset +door through `TransportScopedMetaRequest` (it still +spreads the transport-level `environmentId`, which stays layered on by the +#9741 envelope rather than becoming a protocol key), the audit door as a plain +`AuditMetaItemRequest` (it stopped sending `environmentId` when #8747 scoped +the read, so there is no transport member left to layer on). + +**No behaviour change of any kind, and nothing about the wire moves.** The +outgoing payloads are byte-identical (same keys, same conditional spreads); the +edits hoist each literal into a const and drop type-level casts. The 501 +feature-detection guards survive on purpose: both members are declared +**optional** (a kernel may implement neither door), and each guard is also what +narrows its member to callable at the call site. An undeclared key in either +literal is now a compile error instead of a payload member no contract has ever +seen. diff --git a/.changeset/spec-audit-meta-item-member.md b/.changeset/spec-audit-meta-item-member.md new file mode 100644 index 0000000000..68d7059b62 --- /dev/null +++ b/.changeset/spec-audit-meta-item-member.md @@ -0,0 +1,13 @@ +--- +"@objectstack/spec": minor +--- + +**`MetadataProtocol` declares the optional `auditMetaItem` member, and the audit door's request/response schemas join the spec** (#11678 — the #11006 maintainer-ruled pattern, 2026-08-22 option B, carried one door over). + +`GET /api/v1/meta/:type/:name/audit` — the ADR-0010 §3.6 compliance trail behind Studio's 审计日志 / Audit log tab — was a step behind the half-declared publish door #11006 adjudicated: **neither** side was declared (`auditMetaItem` 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: + +- `AuditMetaItemRequestSchema` / `AuditMetaItemRequest` — `{ type, name, organizationId?: string | null, limit? }`, mirroring the implementation's parameter type in `@objectstack/metadata-protocol` member for member. `organizationId` is nullable because the REST door always sends it, possibly `null` (#8747's fail-closed tenant scoping: `null`/absent = env-wide rows only, never every tenant's). `limit` declares no bounds because the implementation clamps to [1, 500] rather than refusing. `environmentId` stays out by the #9741 ruling (transport-level routing key) — and on this door it is not even on the wire any more (#8747 removed it; the implementation never read it). +- `AuditMetaItemResponseSchema` / `AuditMetaItemResponse` — the `{ events: [...] }` body, newest first, with the closed `operation` (save/publish/rollback/delete/reset) and `outcome` (allowed/denied/forced) vocabularies and the ADR-0010 §3.3 `lockState`. The #9426 miss-vs-fault honesty is recorded in the declared types: `{ events: [] }` is the honest answer for a clean trail, a find-less host engine, or an unprovisioned audit table — never for a missing capability (501 before the call) and never for a failed read (propagated, not invented into an empty trail). +- `MetadataProtocol.auditMetaItem?(request: AuditMetaItemRequest): Promise` — optional like its `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/.changeset/spec-delete-meta-item-request-members.md b/.changeset/spec-delete-meta-item-request-members.md new file mode 100644 index 0000000000..52ccf83083 --- /dev/null +++ b/.changeset/spec-delete-meta-item-request-members.md @@ -0,0 +1,17 @@ +--- +"@objectstack/spec": minor +--- + +**`DeleteMetaItemRequestSchema` declares the contract members the REST reset door sends** (#11679 — the #11006 maintainer-ruled pattern on the request-shape half). + +`MetadataProtocol.deleteMetaItem` was declared all along, but its request schema declared 2 of the 8 members `DELETE /api/v1/meta/:type/:name` sends — so the door's call site had to stay behind an `(p as any)` cast (removing it surfaced `TS2353` on six keys, the opposite half of the publish door's `TS2339`), and the one member most worth having a contract — `organizationId`, which selects WHICH overlay row a reset destroys (ADR-0005 org partition; an org-less delete reaches the environment-wide row) — was on the wire with no declaration behind it. + +Additive, not breaking — the five contract-level members join the schema, mirroring the implementation's parameter type in `@objectstack/metadata-protocol`: + +- `organizationId?` — tenant scope for the reset (#8805); load-bearing, decides which row the delete destroys. +- `parentVersion?` — the ADR-0008 optimistic-concurrency pin (REST: the `If-Match` header); absent = last-write-wins. +- `actor?` — identity recorded on the history tombstone row (one producer, #7749); absent = recorded actor-less, never "system" (#4556). +- `state?` — `'active' | 'draft'`; `draft` discards the pending draft overlay only. +- `dropStorage?` — destructive opt-in (default false): also drop the object's physical table (`object` + `active` only; never `sys_`). + +Two wire members stay out, by ruling rather than omission: `environmentId` (transport-level routing key per #9741, layered on by `packages/rest`'s `TransportScopedMetaRequest`) — and there are no internal coordination keys on this door (`_skipSeedApply` is publish-batch-only). diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index 70a5ee6fe3..536a3112aa 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, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemLayeredRequestSchema, GetMetaItemLayeredResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, NotificationSchema, NotificationPreferencesSchema, PublishMetaItemRequestSchema, PublishMetaItemResponseSchema, PublishPackageDraftsResponseSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, RuntimeAuthoringIssueSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, 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, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemLayeredRequest, GetMetaItemLayeredResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, Notification, NotificationPreferences, PublishMetaItemRequest, PublishMetaItemResponse, PublishPackageDraftsResponse, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, RuntimeAuthoringIssue, SaveMetaItemRequest, SaveMetaItemResponse, 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, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemLayeredRequestSchema, GetMetaItemLayeredResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, NotificationSchema, NotificationPreferencesSchema, PublishMetaItemRequestSchema, PublishMetaItemResponseSchema, PublishPackageDraftsResponseSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, RuntimeAuthoringIssueSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, 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, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemLayeredRequest, GetMetaItemLayeredResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, Notification, NotificationPreferences, PublishMetaItemRequest, PublishMetaItemResponse, PublishPackageDraftsResponse, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, RuntimeAuthoringIssue, SaveMetaItemRequest, SaveMetaItemResponse, 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); @@ -222,6 +222,31 @@ const result = AiAgentCapabilitiesSchema.parse(data); | **error** | `string` | optional | Failure reason, when failed | +--- + +## AuditMetaItemRequest + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Metadata type name | +| **name** | `string` | ✅ | Item name | +| **organizationId** | `string \| null` | optional | Organization (tenant) scope for the read (#8747). With an organization, the trail includes that org's rows AND the env-wide (`organization_id IS NULL`) rows — the env-wide limb is load-bearing, because env-level writes are stamped org-less. `null` and absent are equivalent and both mean the env-wide rows only — the fail-closed direction: an unresolved organization reads env-wide rows, never every tenant's. | +| **limit** | `number` | optional | Maximum events to return, newest first. The implementation clamps to [1, 500] and defaults to 100 — out-of-range values are clamped, never refused. | + + +--- + +## AuditMetaItemResponse + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **events** | `{ id: any; occurredAt: string; actor: string; source: string \| null; … }[]` | ✅ | Recent protection-audit events for the item, newest first. See the schema-level note for what an empty array means — and what it never means. | + + --- ## AutomationActionsResponse @@ -447,6 +472,11 @@ const result = AiAgentCapabilitiesSchema.parse(data); | :--- | :--- | :--- | :--- | | **type** | `string` | ✅ | Metadata type name | | **name** | `string` | ✅ | Item name | +| **organizationId** | `string` | optional | Organization (tenant) scope for the reset (#8805). Load-bearing, not advisory: it selects the ADR-0005 overlay partition, so it decides WHICH row the reset destroys — an org-scoped delete removes that tenant's own overlay, while an org-less delete reaches the environment-wide row and would blank the item for every tenant. Absent = environment-wide. | +| **parentVersion** | `string` | optional | ADR-0008 optimistic-concurrency pin: the version token the caller believes is current (on the REST door, the `If-Match` request header). Present, a concurrent edit is reported as a 409 conflict instead of silently reset; absent = last-write-wins against the current row (Studio's "Reset" button is unpinned). | +| **actor** | `string` | optional | Identity recorded on the delete's history tombstone row. On the REST door this is the request's authenticated identity (one producer, #7749) — never a caller-supplied header. Absent, the event is recorded actor-less (null), deliberately not attributed to "system" (#4556). | +| **state** | `Enum<'active' \| 'draft'>` | optional | Which lifecycle row to discard: `draft` discards the pending draft overlay only (the still-active overlay, if any, keeps serving); `active` or absent resets the live row. Absent defaults to `active`. | +| **dropStorage** | `boolean` | optional | Destructive opt-in, default false: also drop the object's physical table after the metadata row is removed (`object` type + `active` state only; never `sys_` tables). Used by the "discard a previewed object" flow so a publish-to-preview leaves no orphan table. | --- diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 33588a5cf6..c6caaf155c 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 — 1583 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1585 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) | 29 | 418 | REST contracts, endpoints, routing, realtime, batch, discovery. | +| [API Protocol](/docs/references/api) | 29 | 420 | 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 | 288 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | | [UI Protocol](/docs/references/ui) | 16 | 152 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **199** | **1583** | 14 protocol modules | +| **Total** | **199** | **1585** | 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` · **29 pages, 418 schemas** +**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **29 pages, 420 schemas** REST contracts, endpoints, routing, realtime, batch, discovery. @@ -86,7 +86,7 @@ REST contracts, endpoints, routing, realtime, batch, discovery. | [`odata.zod.ts`](/docs/references/api/odata) | `ODataConfig`, `ODataError`, `ODataFilterFunction`, `ODataMetadata`, `ODataQuery`, `ODataResponse` | | [`package-api.zod.ts`](/docs/references/api/package-api) | `GetInstalledPackageRequest`, `GetInstalledPackageResponse`, `ListInstalledPackagesRequest`, `ListInstalledPackagesResponse`, `PackageApiErrorCode`, `PackageInstallRequest`, `PackageInstallResponse`, `PackagePathParams`, `PackageRollbackRequest`, `PackageRollbackResponse`, `PackageUpgradeRequest`, `PackageUpgradeResponse`, `ResolveDependenciesRequest`, `ResolveDependenciesResponse`, `UninstallPackageApiRequest`, `UninstallPackageApiResponse`, `UploadArtifactRequest`, `UploadArtifactResponse` | | [`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`, `AutomationActionsResponse`, `AutomationTriggerRequest`, `AutomationTriggerResponse`, `BatchDataRequest`, `BatchDataResponse`, `CheckPermissionRequest`, `CheckPermissionResponse`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemLayeredRequest`, `GetMetaItemLayeredResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `Notification`, `NotificationPreferences`, `PublishMetaItemRequest`, `PublishMetaItemResponse`, `PublishPackageDraftsResponse`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `RuntimeAuthoringIssue`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `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`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemLayeredRequest`, `GetMetaItemLayeredResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `Notification`, `NotificationPreferences`, `PublishMetaItemRequest`, `PublishMetaItemResponse`, `PublishPackageDraftsResponse`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `RuntimeAuthoringIssue`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `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 9d5448626f..e2de913d9b 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/` | 410 | +| `api/` | 413 | | `cloud/` | 83 | | `identity/` | 32 | | `integration/` | 10 | diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index 74e0b3975b..62fd96cb29 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -78,6 +78,8 @@ import type { GetMetaItemCachedRequest, GetMetaItemLayeredRequest, PublishMetaItemRequest, + AuditMetaItemRequest, + DeleteMetaItemRequest, } from '@objectstack/spec/api'; // [#8073] The closed ADR-0112 error vocabulary, so the explain family's single // refusal emitter types its `code` parameter as the vocabulary rather than as @@ -5805,7 +5807,7 @@ export class RestServer { return; } const p = await this.resolveProtocol(environmentId, req); - if (!(p as any).deleteMetaItem) { + if (!p.deleteMetaItem) { // [#7035] ADR-0112 envelope. This site was the worst of // the three shapes: a BARE STRING `error`, with no code // at all — so neither `err.error.code` nor `err.code` @@ -5869,7 +5871,23 @@ export class RestServer { // org-scope comment for the measurement. canonicalMetaUrlType(req.params.type), ctx?.tenantId, ); - const result = await (p as any).deleteMetaItem({ + // [#11679] The `(p as any)` cast this call carried came off + // when `DeleteMetaItemRequestSchema` caught up with the + // eight members this door sends. Unlike the publish door's + // cast (member existence, TS2339), this one was load-bearing + // on REQUEST SHAPE: the member was declared all along, but + // the schema declared only `{ type, name }`, so removing the + // cast surfaced TS2353 on six keys. The literal is now + // compiled against the spec contract through the #9741 + // `TransportScopedMetaRequest` wrapper — `environmentId` is + // the transport-level routing key that wrapper layers on, + // ⛔ never a protocol key; every other key here is checked + // against the declared request, so an undeclared member is a + // compile error instead of a payload member no contract has + // ever seen. The 501 guard above stays: the member is + // declared OPTIONAL, and the guard is what narrows it to + // callable here. + const deleteRequest: TransportScopedMetaRequest = { type: req.params.type, name: req.params.name, organizationId, @@ -5878,7 +5896,8 @@ export class RestServer { ...(actor ? { actor } : {}), ...(stateParam ? { state: stateParam } : {}), ...(dropStorage ? { dropStorage: true } : {}), - }); + }; + const result = await p.deleteMetaItem(deleteRequest); res.json(result); } catch (error: any) { handleRouteError(res, error); @@ -5952,7 +5971,7 @@ export class RestServer { try { const environmentId = isScoped ? req.params?.environmentId : undefined; const p = await this.resolveProtocol(environmentId, req); - if (typeof (p as any).auditMetaItem !== 'function') { + if (typeof p.auditMetaItem !== 'function') { // [#9426 / ADR-0110 D3] A MISS and a FAULT are different // facts, and this branch is the second one: the resolved // protocol cannot read an audit trail AT ALL, so the @@ -5977,16 +5996,17 @@ export class RestServer { // // Refusing HERE rather than asserting at assembly is // deliberate, and is the reasoning PR #9425 landed one - // route over. `auditMetaItem` is not a member of - // `RestProtocol` (= `DataProtocol & MetadataProtocol`) and - // is not declared in `packages/spec` at all — it is an - // ADR-0076 D9 server-only extension, which is why it is - // reached through a runtime cast. A host that implements - // the DECLARED contract exactly is therefore a CONFORMING - // deployment that lands here with no type error, and a - // boot-time assertion would promote an undeclared optional - // extension into a required one — a `packages/spec` - // contract decision, not a route one. + // route over. `auditMetaItem` is a declared OPTIONAL + // member of `MetadataProtocol` (the #11006-pattern + // catch-up that retired this door's `(p as any)` casts; + // it was an undeclared ADR-0076 D9 server-only + // extension before that). A host without the verb is + // therefore a CONFORMING deployment that lands here + // with no type error, and a boot-time assertion would + // promote a declared-optional member into a required + // one — a `packages/spec` contract decision, not a + // route one. The guard is also what narrows the member + // to callable below. // // Envelope per #7035: the ADR-0112 NESTED // `{ error: { code, message } }` the sibling `/meta` 501 @@ -6038,12 +6058,24 @@ export class RestServer { // states below — not from the request payload. It is still // read on the two lines that need it. const ctx = await this.resolveExecCtx(environmentId, req).catch(() => undefined); - const result = await (p as any).auditMetaItem({ + // The `(p as any)` casts this door carried came off when + // `MetadataProtocol` declared `auditMetaItem` (the #11006 + // pattern, same as the publish door below): the literal is + // now compiled against the spec contract, so an undeclared + // key here is a compile error (TS2353) instead of a payload + // member no contract has ever seen. Plain + // `AuditMetaItemRequest` rather than the + // `TransportScopedMetaRequest` wrapper on purpose: this + // door stopped sending `environmentId` when #8747 scoped + // the read (see the note above), so there is no + // transport-level member left to layer on. + const auditRequest: AuditMetaItemRequest = { type: req.params.type, name: req.params.name, organizationId: ctx?.tenantId ?? null, ...(limit !== undefined && Number.isFinite(limit) ? { limit } : {}), - }); + }; + const result = await p.auditMetaItem(auditRequest); 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 2ccf7a2507..be081e11ed 100644 --- a/packages/spec/api-surface/api.json +++ b/packages/spec/api-surface/api.json @@ -72,6 +72,10 @@ "AppDefinitionResponseSchema (const)", "ApproveAiPendingActionResponse (type)", "ApproveAiPendingActionResponseSchema (const)", + "AuditMetaItemRequest (type)", + "AuditMetaItemRequestSchema (const)", + "AuditMetaItemResponse (type)", + "AuditMetaItemResponseSchema (const)", "AuthEndpoint (type)", "AuthEndpointAlias (type)", "AuthEndpointAliases (const)", diff --git a/packages/spec/authorable-surface/api.json b/packages/spec/authorable-surface/api.json index 5ac6720b77..1b714acd69 100644 --- a/packages/spec/authorable-surface/api.json +++ b/packages/spec/authorable-surface/api.json @@ -190,6 +190,11 @@ "api/ApproveAiPendingActionResponse:error", "api/ApproveAiPendingActionResponse:result", "api/ApproveAiPendingActionResponse:status", + "api/AuditMetaItemRequest:limit", + "api/AuditMetaItemRequest:name", + "api/AuditMetaItemRequest:organizationId", + "api/AuditMetaItemRequest:type", + "api/AuditMetaItemResponse:events", "api/AuthEndpoint:forgetPassword", "api/AuthEndpoint:getSession", "api/AuthEndpoint:resetPassword", @@ -468,7 +473,12 @@ "api/DeleteManyDataResponse:total", "api/DeleteManyRequest:ids", "api/DeleteManyRequest:options", + "api/DeleteMetaItemRequest:actor", + "api/DeleteMetaItemRequest:dropStorage", "api/DeleteMetaItemRequest:name", + "api/DeleteMetaItemRequest:organizationId", + "api/DeleteMetaItemRequest:parentVersion", + "api/DeleteMetaItemRequest:state", "api/DeleteMetaItemRequest:type", "api/DeleteMetaItemResponse:message", "api/DeleteMetaItemResponse:reset", diff --git a/packages/spec/export-origins/api.json b/packages/spec/export-origins/api.json index b52c2573dd..205d153201 100644 --- a/packages/spec/export-origins/api.json +++ b/packages/spec/export-origins/api.json @@ -72,6 +72,10 @@ "AppDefinitionResponseSchema": "src/api/metadata.zod.ts#AppDefinitionResponseSchema (const)", "ApproveAiPendingActionResponse": "src/api/protocol.zod.ts#ApproveAiPendingActionResponse (type)", "ApproveAiPendingActionResponseSchema": "src/api/protocol.zod.ts#ApproveAiPendingActionResponseSchema (const)", + "AuditMetaItemRequest": "src/api/protocol.zod.ts#AuditMetaItemRequest (type)", + "AuditMetaItemRequestSchema": "src/api/protocol.zod.ts#AuditMetaItemRequestSchema (const)", + "AuditMetaItemResponse": "src/api/protocol.zod.ts#AuditMetaItemResponse (type)", + "AuditMetaItemResponseSchema": "src/api/protocol.zod.ts#AuditMetaItemResponseSchema (const)", "AuthEndpoint": "src/api/auth-endpoints.zod.ts#AuthEndpoint (type)", "AuthEndpointAlias": "src/api/auth-endpoints.zod.ts#AuthEndpointAlias (type)", "AuthEndpointAliases": "src/api/auth-endpoints.zod.ts#AuthEndpointAliases (const)", diff --git a/packages/spec/json-schema.manifest/api.json b/packages/spec/json-schema.manifest/api.json index 9ad0427bd1..b9de5711c7 100644 --- a/packages/spec/json-schema.manifest/api.json +++ b/packages/spec/json-schema.manifest/api.json @@ -33,6 +33,8 @@ "api/ApiTestingUiType", "api/AppDefinitionResponse", "api/ApproveAiPendingActionResponse", + "api/AuditMetaItemRequest", + "api/AuditMetaItemResponse", "api/AuthEndpoint", "api/AuthFeaturesConfig", "api/AuthProvider", diff --git a/packages/spec/src/api/protocol.test.ts b/packages/spec/src/api/protocol.test.ts index 6f7a8e3be6..31efc47d73 100644 --- a/packages/spec/src/api/protocol.test.ts +++ b/packages/spec/src/api/protocol.test.ts @@ -1782,3 +1782,265 @@ describe('MetadataProtocol declares publishMetaItem (#11006)', () => { expect(misspelt.name).toBe('account_list'); }); }); + +import { AuditMetaItemRequestSchema, AuditMetaItemResponseSchema } from './protocol.zod'; +import type { AuditMetaItemRequest, AuditMetaItemResponse } from './protocol.zod'; + +describe('AuditMetaItemRequestSchema mirrors the implementation parameter type (#11678)', () => { + // The audit door was a step BEHIND the half-declared publish door #11006 + // adjudicated: NEITHER side was declared, and the REST call site reached 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 | null, limit?: number }` — and the + // REST door's actual sends; nothing else is declared because nothing else is + // enforced. As in the #9726/#9741/#11006 blocks 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', limit: 50 }; + const result = AuditMetaItemRequestSchema.safeParse(full); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual(full); + } + }); + + it('requires type AND name — the trail is per item', () => { + expect(AuditMetaItemRequestSchema.safeParse(base).success).toBe(true); + expect(AuditMetaItemRequestSchema.safeParse({ type: 'view' }).success).toBe(false); + expect(AuditMetaItemRequestSchema.safeParse({ name: 'account_list' }).success).toBe(false); + }); + + it('organizationId accepts null AND preserves it — the REST door always sends it, possibly null', () => { + // The door sends `ctx?.tenantId ?? null` unconditionally (#8747's + // fail-closed scoping), so `null` must be a declared spelling for the + // literal to compile at all. Semantically `null` and absent are the same + // env-wide read; the pin is that the parse neither refuses nor strips it. + const withNull = AuditMetaItemRequestSchema.safeParse({ ...base, organizationId: null }); + expect(withNull.success).toBe(true); + if (withNull.success) { + expect('organizationId' in (withNull.data as object)).toBe(true); + expect((withNull.data as { organizationId?: string | null }).organizationId).toBeNull(); + } + expect(AuditMetaItemRequestSchema.safeParse({ ...base, organizationId: 42 }).success).toBe(false); + }); + + it('limit is an optional number — values, not bags', () => { + expect(AuditMetaItemRequestSchema.safeParse(base).success).toBe(true); + expect(AuditMetaItemRequestSchema.safeParse({ ...base, limit: '50' }).success).toBe(false); + // The implementation CLAMPS to [1, 500] rather than refusing, so the + // schema deliberately declares no bounds — declaring `.max(500)` here + // would refuse a value the shipped verb accepts (clamped), which is the + // accept/reject drift a declared-surface catch-up must not introduce. + const clampedNotRefused = AuditMetaItemRequestSchema.safeParse({ ...base, limit: 9999 }); + expect(clampedNotRefused.success).toBe(true); + }); + + it('does not declare environmentId — transport-level by the #9741 ruling, stripped and shape-absent', () => { + // Same regression guard as the meta-read and publish blocks above. On THIS + // door the exclusion is even stronger than the ruling: #8747 removed + // `environmentId` from the door's payload entirely (the implementation + // never read it), so declaring it would resurrect a dead wire member. + const result = AuditMetaItemRequestSchema.safeParse({ ...base, environmentId: 'env_alpha' }); + expect(result.success).toBe(true); + if (result.success) { + expect('environmentId' in (result.data as object)).toBe(false); + } + const shape = (AuditMetaItemRequestSchema as unknown as { shape: Record }).shape; + expect(Object.keys(shape)).not.toContain('environmentId'); + }); +}); + +describe('AuditMetaItemResponseSchema declares the compliance-trail body (#11678)', () => { + /** A verbatim-shaped capture of a real `auditMetaItem` return (one denied save). */ + const realResponse = { + events: [ + { + id: 'evt_01', + occurredAt: '2026-08-25T02:11:09.000Z', + actor: 'admin@objectos.ai', + source: 'protocol.saveMetaItem', + operation: 'save', + outcome: 'denied', + // adr0112-ok: D6b — persisted audit column, its own lowercase vocabulary + code: 'item_locked', + lockState: 'full', + lockOverridden: false, + requestId: null, + note: null, + }, + ], + }; + + it('parses the real event shape and PRESERVES every member', () => { + const result = AuditMetaItemResponseSchema.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 (#9426)', () => { + // `[]` is the honest MISS shape (clean trail / no `find` on the host + // engine / unprovisioned table). The capability gap is NOT in this body: + // a protocol without the verb is refused 501 before the call. + const result = AuditMetaItemResponseSchema.safeParse({ events: [] }); + expect(result.success).toBe(true); + if (result.success) { + expect((result.data as { events: unknown[] }).events).toEqual([]); + } + }); + + it('keeps the operation and outcome vocabularies closed', () => { + const bad = (patch: Record) => + AuditMetaItemResponseSchema.safeParse({ events: [{ ...realResponse.events[0], ...patch }] }); + expect(bad({ operation: 'diff' }).success).toBe(false); + expect(bad({ outcome: 'skipped' }).success).toBe(false); + }); +}); + +describe('MetadataProtocol declares auditMetaItem (#11678)', () => { + // Type-level pins (compiled by the spec test typecheck, the + // translation-typegen.test.ts pattern — same as the #9740 and #11006 blocks + // 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 audit schemas. + + it('declares the member optional, against the audit request/response schemas', () => { + // Optional like its `deleteMetaItem` / `getMetaItemLayered` siblings: + // additive to a shipped contract, implementation predating declaration. + expectTypeOf().toEqualTypeOf< + ((request: AuditMetaItemRequest) => Promise) | undefined + >(); + // An implementation without the verb still type-checks against the + // interface — the CONFORMING-deployment half of #9426's 501 refusal. + const absent: Pick = {}; + expect('auditMetaItem' in absent).toBe(false); + }); + + it('refuses an undeclared key at the member call shape', () => { + const good: AuditMetaItemRequest = { type: 'view', name: 'account_list', organizationId: null }; + expect(good.type).toBe('view'); + // @ts-expect-error `environmentId` is transport-level (#9741) — not a declared request member (and #8747 removed it from this door's wire payload entirely). + const withEnv: AuditMetaItemRequest = { 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: AuditMetaItemRequest = { type: 'view', name: 'account_list', limits: 50 }; + expect(misspelt.name).toBe('account_list'); + }); +}); + +import { DeleteMetaItemRequestSchema } from './protocol.zod'; +import type { DeleteMetaItemRequest, DeleteMetaItemResponse } from './protocol.zod'; + +describe('DeleteMetaItemRequestSchema declares the contract members the reset door sends (#11679)', () => { + // The sharper sibling of the audit door: the MEMBER was declared all along + // (so a scan for undeclared members walked past it), while the request + // schema declared 2 of the 8 members `DELETE /meta/:type/:name` sends — + // which is why the call-site cast could not come off (TS2353 on six keys, + // the opposite half of the publish door's TS2339). The measure is the + // implementation's parameter type in `@objectstack/metadata-protocol` — + // `{ type, name, organizationId?, parentVersion?, actor?, state?, + // dropStorage? }` — and the REST door's actual sends. As in the sibling + // blocks 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', + parentVersion: 'sha256:abc123', + actor: 'admin@objectos.ai', + state: 'draft', + dropStorage: true, + }; + const result = DeleteMetaItemRequestSchema.safeParse(full); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual(full); + } + }); + + it('requires type AND name — the reset addresses one item', () => { + expect(DeleteMetaItemRequestSchema.safeParse(base).success).toBe(true); + expect(DeleteMetaItemRequestSchema.safeParse({ type: 'view' }).success).toBe(false); + expect(DeleteMetaItemRequestSchema.safeParse({ name: 'account_list' }).success).toBe(false); + }); + + it('the three optional strings stay optional and reject non-strings — values, not bags', () => { + for (const key of ['organizationId', 'parentVersion', 'actor'] as const) { + const absent = DeleteMetaItemRequestSchema.safeParse(base); + expect(absent.success).toBe(true); + if (absent.success) { + expect(key in (absent.data as object)).toBe(false); + } + expect(DeleteMetaItemRequestSchema.safeParse({ ...base, [key]: 42 }).success).toBe(false); + expect(DeleteMetaItemRequestSchema.safeParse({ ...base, [key]: { v: 'x' } }).success).toBe(false); + } + }); + + it('keeps the state vocabulary closed and dropStorage boolean', () => { + expect(DeleteMetaItemRequestSchema.safeParse({ ...base, state: 'active' }).success).toBe(true); + expect(DeleteMetaItemRequestSchema.safeParse({ ...base, state: 'pending' }).success).toBe(false); + expect(DeleteMetaItemRequestSchema.safeParse({ ...base, dropStorage: false }).success).toBe(true); + // The REST door only ever SENDS `dropStorage: true` (conditional spread), + // but the contract member is a boolean, mirroring the implementation. + expect(DeleteMetaItemRequestSchema.safeParse({ ...base, dropStorage: 'true' }).success).toBe(false); + }); + + it('does not declare environmentId — transport-level by the #9741 ruling, stripped and shape-absent', () => { + // Same regression guard as the meta-read, publish and audit blocks above: + // the reset door DOES spread `environmentId` into its outgoing payload, + // and that member rides `packages/rest`'s `TransportScopedMetaRequest` + // envelope — never this schema. If someone declares it, this test names + // the ruling they are overturning (2026-08-18 on #9741). + const result = DeleteMetaItemRequestSchema.safeParse({ ...base, environmentId: 'env_alpha' }); + expect(result.success).toBe(true); + if (result.success) { + expect('environmentId' in (result.data as object)).toBe(false); + } + const shape = (DeleteMetaItemRequestSchema as unknown as { shape: Record }).shape; + expect(Object.keys(shape)).not.toContain('environmentId'); + }); +}); + +describe('MetadataProtocol.deleteMetaItem types against the caught-up request schema (#11679)', () => { + // The member declaration itself predates this card; these pins are the + // request-shape half — what turns red if the schema drops back to + // `{ type, name }` (the member would still exist; the door literal would + // stop compiling) or if a key drifts off the implementation's vocabulary. + + it('declares the member optional, against the delete request/response schemas', () => { + expectTypeOf().toEqualTypeOf< + ((request: DeleteMetaItemRequest) => Promise) | undefined + >(); + const absent: Pick = {}; + expect('deleteMetaItem' in absent).toBe(false); + }); + + it('refuses an undeclared key at the member call shape — the TS2353 half this card names', () => { + const good: DeleteMetaItemRequest = { + type: 'view', + name: 'account_list', + organizationId: 'org_alpha', + parentVersion: 'sha256:abc123', + actor: 'admin', + state: 'draft', + dropStorage: true, + }; + expect(good.type).toBe('view'); + // @ts-expect-error `environmentId` is transport-level (#9741) — not a declared request member; the REST door layers it on via TransportScopedMetaRequest. + const withEnv: DeleteMetaItemRequest = { 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: DeleteMetaItemRequest = { type: 'view', name: 'account_list', dropstorage: true }; + expect(misspelt.name).toBe('account_list'); + }); +}); diff --git a/packages/spec/src/api/protocol.zod.ts b/packages/spec/src/api/protocol.zod.ts index 795dc871fc..9ce3b2c4a6 100644 --- a/packages/spec/src/api/protocol.zod.ts +++ b/packages/spec/src/api/protocol.zod.ts @@ -1116,11 +1116,61 @@ export const PublishPackageDraftsResponseSchema = lazySchema(() => z.object({ /** * Delete Metadata Item Request - * Removes a customization overlay row from sys_metadata (ADR-0005). + * Removes a customization overlay row from sys_metadata (ADR-0005) — the + * "reset to artifact factory default" semantic behind + * `DELETE /api/v1/meta/:type/:name`. + * + * Declared member for member against the implementation's parameter type in + * `@objectstack/metadata-protocol` and the REST reset door's actual sends — a + * declared-surface catch-up, not a new capability (#11679, the #11006 + * maintainer-ruled pattern): every member here already ships and is enforced. + * The member existed on `MetadataProtocol` all along; the request schema + * declared 2 of the 8 members the reset door sends, so the door's call site + * had to stay behind an `as any` cast (removing it surfaced `TS2353` on the + * six undeclared keys — the opposite half of the publish door's `TS2339`). + * + * One wire member is deliberately NOT declared: `environmentId`, the + * transport-level multi-kernel routing key, OUT of protocol request shapes by + * the #9741 maintainer ruling (2026-08-18) — `resolveProtocol(environmentId)` + * selects the target kernel before this method is entered, the implementation + * never reads it off the request, and `packages/rest` layers it on via its + * `TransportScopedMetaRequest` wrapper. */ export const DeleteMetaItemRequestSchema = lazySchema(() => z.object({ type: z.string().describe('Metadata type name'), name: z.string().describe('Item name'), + organizationId: z.string().optional().describe( + 'Organization (tenant) scope for the reset (#8805). Load-bearing, not ' + + 'advisory: it selects the ADR-0005 overlay partition, so it decides ' + + 'WHICH row the reset destroys — an org-scoped delete removes that ' + + 'tenant\'s own overlay, while an org-less delete reaches the ' + + 'environment-wide row and would blank the item for every tenant. ' + + 'Absent = environment-wide.', + ), + parentVersion: z.string().optional().describe( + 'ADR-0008 optimistic-concurrency pin: the version token the caller ' + + 'believes is current (on the REST door, the `If-Match` request header). ' + + 'Present, a concurrent edit is reported as a 409 conflict instead of ' + + 'silently reset; absent = last-write-wins against the current row ' + + '(Studio\'s "Reset" button is unpinned).', + ), + actor: z.string().optional().describe( + 'Identity recorded on the delete\'s history tombstone row. On the REST ' + + 'door this is the request\'s authenticated identity (one producer, ' + + '#7749) — never a caller-supplied header. Absent, the event is recorded ' + + 'actor-less (null), deliberately not attributed to "system" (#4556).', + ), + state: z.enum(['active', 'draft']).optional().describe( + 'Which lifecycle row to discard: `draft` discards the pending draft ' + + 'overlay only (the still-active overlay, if any, keeps serving); ' + + '`active` or absent resets the live row. Absent defaults to `active`.', + ), + dropStorage: z.boolean().optional().describe( + 'Destructive opt-in, default false: also drop the object\'s physical ' + + 'table after the metadata row is removed (`object` type + `active` ' + + 'state only; never `sys_` tables). Used by the "discard a previewed ' + + 'object" flow so a publish-to-preview leaves no orphan table.', + ), })); /** @@ -1134,6 +1184,106 @@ export const DeleteMetaItemResponseSchema = lazySchema(() => z.object({ message: z.string().optional(), })); +/** + * Audit Metadata Item Request + * + * Request shape for `GET /api/v1/meta/:type/:name/audit` (the `auditMetaItem` + * protocol method) — the ADR-0010 §3.6 compliance trail: recent + * `sys_metadata_audit` rows (save/publish/rollback/delete/reset attempts, both + * allowed and denied) that Studio's 审计日志 / Audit log tab renders. 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): 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 + * reads it off the request (the REST audit door stopped sending it when #8747 + * scoped the read). + */ +export const AuditMetaItemRequestSchema = lazySchema(() => z.object({ + type: z.string().describe('Metadata type name'), + name: z.string().describe('Item name'), + organizationId: z.string().nullable().optional().describe( + 'Organization (tenant) scope for the read (#8747). With an organization, ' + + 'the trail includes that org\'s rows AND the env-wide ' + + '(`organization_id IS NULL`) rows — the env-wide limb is load-bearing, ' + + 'because env-level writes are stamped org-less. `null` and absent are ' + + 'equivalent and both mean the env-wide rows only — the fail-closed ' + + 'direction: an unresolved organization reads env-wide rows, never every ' + + 'tenant\'s.', + ), + limit: z.number().optional().describe( + 'Maximum events to return, newest first. The implementation clamps to ' + + '[1, 500] and defaults to 100 — out-of-range values are clamped, never ' + + 'refused.', + ), +})); + +/** + * Audit Metadata Item Response + * + * The body of `GET /api/v1/meta/:type/:name/audit`, mirrored member for member + * from the implementation's return type (`ObjectStackProtocolImplementation + * .auditMetaItem`), newest event first. + * + * **What an empty `events` means, and what it deliberately does NOT mean** + * (#9426 / ADR-0110 D3 — a MISS and a FAULT are different facts): `[]` is the + * honest answer for a genuinely clean trail, for a host engine that exposes no + * `find` (a metadata-only store), and for an environment whose audit table has + * not been provisioned. It is NEVER the answer for a protocol that lacks the + * verb — the REST door refuses 501 before the call — nor for a failed read: a + * non-benign read failure (connection drop, timeout, permission denial) + * propagates as an error rather than being invented into an empty trail + * (#9638). + */ +export const AuditMetaItemResponseSchema = lazySchema(() => z.object({ + events: z.array(z.object({ + id: z.unknown().describe('Row id of the audit event. Opaque to callers.'), + occurredAt: z.string().describe('When the attempt happened (ISO-8601 string).'), + actor: z.string().describe( + 'Who attempted the operation. `system` when the row recorded no actor.', + ), + source: z.string().nullable().describe( + 'Which code path recorded the event (e.g. `protocol.deleteMetaItem`). ' + + '`null` when the row recorded none.', + ), + operation: z.enum(['save', 'publish', 'rollback', 'delete', 'reset']).describe( + 'Which metadata-protection door was attempted.', + ), + outcome: z.enum(['allowed', 'denied', 'forced']).describe( + 'Whether the attempt went through, was refused, or overrode a lock ' + + '(ADR-0010 §3.6).', + ), + code: z.string().describe( + 'Machine-readable verdict code for the outcome (e.g. `item_locked`). ' + + 'Empty string when the row recorded none.', + ), + lockState: MetadataLockSchema.nullable().describe( + 'The lock verdict in force at the time of the attempt (ADR-0010 §3.3). ' + + '`null` when no lock applied.', + ), + lockOverridden: z.boolean().describe( + 'True when the attempt went through by overriding a lock (`outcome: ' + + '"forced"` rows).', + ), + requestId: z.string().nullable().describe( + 'Correlation id of the originating request. `null` when the row ' + + 'recorded none.', + ), + note: z.string().nullable().describe( + 'Free-text note recorded with the event. `null` when the row recorded ' + + 'none.', + ), + })).describe( + 'Recent protection-audit events for the item, newest first. See the ' + + 'schema-level note for what an empty array means — and what it never ' + + 'means.', + ), +})); + /** * Get Metadata Item with Cache Request * Get a specific metadata item with HTTP cache validation support @@ -2288,6 +2438,8 @@ export type PublishMetaItemResponse = z.input; export type DeleteMetaItemRequest = z.input; export type DeleteMetaItemResponse = z.input; +export type AuditMetaItemRequest = z.input; +export type AuditMetaItemResponse = z.input; export type GetMetaItemCachedRequest = z.input; export type GetMetaItemCachedResponse = z.input; /** Post-parse shape of {@link GetMetaItemCachedResponse} — defaults applied, transforms run (ADR-0122). */ @@ -2552,6 +2704,22 @@ export interface MetadataProtocol { * implementation predating the declaration. */ getMetaItemLayered?(request: GetMetaItemLayeredRequest): Promise; + /** + * ADR-0010 §3.6 compliance trail read (`GET /api/v1/meta/:type/:name/audit`) + * — recent `sys_metadata_audit` rows for one item, newest first, so Studio's + * 审计日志 / Audit log tab can show who tried what and whether a lock + * blocked it. Declared optional like its `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) — before this, the REST audit + * 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 (#9426 — a missing + * capability is never reported as an empty trail). + */ + auditMetaItem?(request: AuditMetaItemRequest): 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 a4564cc7f4..2c63e1fc0d 100644 --- a/packages/spec/src/type-alias-convention.pin.test.ts +++ b/packages/spec/src/type-alias-convention.pin.test.ts @@ -267,7 +267,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 @@ -482,6 +482,8 @@ export type Iso852 = Assert, z.infer< typeof M28.RuntimeAuthoringIssueSchema > >>; export type Iso137 = Assert, z.infer< typeof M28.DeleteMetaItemRequestSchema > >>; export type Iso138 = 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 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 > >>; @@ -1668,7 +1670,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 @@ -1957,9 +1959,20 @@ describe('ADR-0122 type-alias convention', () => { // `BreakpointColumnMapSchema`, `BreakpointOrderMapSchema` — Iso696/697/ // 824/825): the four schemas no longer exist, so there is nothing left to // exempt. -4 retired, +0 of my own; the Iso numbers stay vacant. + // + // 833 -> 835 is #11678's `AuditMetaItemRequestSchema` / + // `AuditMetaItemResponseSchema` — the audit door declared on the #11006 + // pattern (PR #12003). Isomorphism MEASURED, not assumed: the request is + // two required `z.string()`s, a `z.string().nullable().optional()` and an + // optional `z.number()`; the response is one `z.array` of a plain object + // of strings, booleans, closed `z.enum`s, `.nullable()` strings and a + // `z.unknown()` — 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 `Iso857`/ + // `Iso858`, 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