diff --git a/.changeset/28406-publish-drafts-request-schema.md b/.changeset/28406-publish-drafts-request-schema.md new file mode 100644 index 00000000000..d4862d7d405 --- /dev/null +++ b/.changeset/28406-publish-drafts-request-schema.md @@ -0,0 +1,15 @@ +--- +'@objectstack/spec': minor +--- + +feat(spec): the body of `POST /packages/:id/publish-drafts` is declared, so the change note a publish carries is part of the published API contract + +Clause-②: yes (widening) +Metadata-protocol: added + +`@objectstack/spec/api` gains `PublishPackageDraftsRequestSchema` and its type `PublishPackageDraftsRequest`, beside the door's existing `PublishPackageDraftsResponseSchema`. It declares the two body members the door reads: + +- **`message`** (optional string): the author's change note. The package commit records it, and so does every item history row the publish writes (`change_note`, read back as each history event's `message`). Absent or empty, those rows record the sentence naming the package. +- **`backfill`** (optional array of `{ object, field }`, both non-empty strings, nothing else in an entry): fields the publish adds whose literal `defaultValue` is written into existing rows, as the signed-in caller. A malformed, repeated or unhonourable entry refuses the whole request `400 VALIDATION_ERROR` before anything is published. + +The object is not strict: other members are stripped, not refused. `actor` is deliberately not declared, since the door records the authenticated caller; `@objectstack/client`'s `packages.publishDrafts` still sends its deprecated `opts.actor`, and that call keeps working unchanged. Nothing changes on the wire: the door reads the body as it did before this declaration. diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index d66141a6f6e..099a2de7ecc 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -1,7 +1,7 @@ --- title: Protocol schema — API Protocol reference navTitle: Protocol -description: "Protocol schemas of the ObjectStack API Protocol: AiAgentCapabilities and 139 more — each property with its type, default and a TypeScript example." +description: "Protocol schemas of the ObjectStack API Protocol: AiAgentCapabilities and 140 more — each property with its type, default and a TypeScript example." --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -13,8 +13,8 @@ description: "Protocol schemas of the ObjectStack API Protocol: AiAgentCapabilit ## 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, FlowRegistrationSchema, FlowRegistrationRefusalSchema, 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, GetRecordFieldAccessResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, HistoryMetaItemRequestSchema, HistoryMetaItemResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListDraftsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, MetadataAuthoringClientSchema, MetadataAuthoringSurfaceSchema, MetadataConflictErrorSchema, NotificationSchema, NotificationPreferencesSchema, PublishMetaItemRequestSchema, PublishMetaItemResponseSchema, PublishPackageDraftsResponseSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, RollbackMetaItemResponseSchema, RuntimeAuthoringIssueSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SearchAllHitSchema, SearchAllPageHitSchema, 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, FlowRegistration, FlowRegistrationRefusal, 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, GetRecordFieldAccessResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, HistoryMetaItemRequest, HistoryMetaItemResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListDraftsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, MetadataAuthoringClient, MetadataAuthoringSurface, MetadataConflictError, Notification, NotificationPreferences, PublishMetaItemRequest, PublishMetaItemResponse, PublishPackageDraftsResponse, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, RollbackMetaItemResponse, RuntimeAuthoringIssue, SaveMetaItemRequest, SaveMetaItemResponse, SearchAllHit, SearchAllPageHit, 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, FlowRegistrationSchema, FlowRegistrationRefusalSchema, 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, GetRecordFieldAccessResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, HistoryMetaItemRequestSchema, HistoryMetaItemResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListDraftsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, MetadataAuthoringClientSchema, MetadataAuthoringSurfaceSchema, MetadataConflictErrorSchema, NotificationSchema, NotificationPreferencesSchema, PublishMetaItemRequestSchema, PublishMetaItemResponseSchema, PublishPackageDraftsRequestSchema, PublishPackageDraftsResponseSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, RollbackMetaItemResponseSchema, RuntimeAuthoringIssueSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SearchAllHitSchema, SearchAllPageHitSchema, 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, FlowRegistration, FlowRegistrationRefusal, 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, GetRecordFieldAccessResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, HistoryMetaItemRequest, HistoryMetaItemResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListDraftsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, MetadataAuthoringClient, MetadataAuthoringSurface, MetadataConflictError, Notification, NotificationPreferences, PublishMetaItemRequest, PublishMetaItemResponse, PublishPackageDraftsRequest, PublishPackageDraftsResponse, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, RollbackMetaItemResponse, RuntimeAuthoringIssue, SaveMetaItemRequest, SaveMetaItemResponse, SearchAllHit, SearchAllPageHit, 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); @@ -2559,6 +2559,25 @@ The authoring surface of a metadata write. Closed: `copilot` is an AI assistant' | **severity** | `Enum<'error' \| 'warning' \| 'info'>` | ✅ | How the gate treated this finding. `error` means the write was REFUSED (these appear on the 422, never on a 2xx); `warning` / `info` are advisory — the write succeeded and the finding is FYI. | +--- + +## PublishPackageDraftsRequest + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **message** | `string` | optional | The author's change note (ADR-0146 D1). The package commit records it, and so does every item history row this publish writes, as `change_note` (read back as each history event's `message`). Absent or empty, those rows record the sentence naming the package instead. | +| **backfill** | `{ object: string; field: string }[]` | optional | Opt-in: fields this publish adds whose literal `defaultValue` is written into the object's existing rows once the drafts are live, as the signed-in caller. Absent or `[]`, no row is touched. Each entry names an object and a field and nothing else, and names it once. The whole request is refused `400 VALIDATION_ERROR` before anything is published when an entry is malformed, repeated, or names a field that is not new in this publish or declares no literal default; `backfillApplied` in the response reports the rows written per field. | + +### Nested Shape: `PublishPackageDraftsRequest.backfill[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Name of the object whose existing rows are written. | +| **field** | `string` | ✅ | Name of the field this publish adds to that object. | + + --- ## PublishPackageDraftsResponse diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index da2b12fc792..e846ca90112 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,7 +1,7 @@ --- title: Protocol reference — every schema by module navTitle: Protocol Reference -description: Every schema published by @objectstack/spec — 1573 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1574 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/. */} @@ -21,7 +21,7 @@ counts are sums of the rows they head. Regenerate with | Module | Pages | Schemas | Description | | :--- | ---: | ---: | :--- | | [AI Protocol](/docs/references/ai) | 12 | 68 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. | -| [API Protocol](/docs/references/api) | 34 | 452 | REST contracts, endpoints, routing, realtime, batch, discovery. | +| [API Protocol](/docs/references/api) | 34 | 453 | REST contracts, endpoints, routing, realtime, batch, discovery. | | [Automation Protocol](/docs/references/automation) | 15 | 77 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. | | [Data Protocol](/docs/references/data) | 31 | 174 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. | | [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. | @@ -34,7 +34,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) | 37 | 287 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | | [UI Protocol](/docs/references/ui) | 16 | 174 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **204** | **1573** | 14 protocol modules | +| **Total** | **204** | **1574** | 14 protocol modules | --- @@ -63,7 +63,7 @@ Agents, tools, skills, RAG and knowledge sources, model registry, conversations. ## API Protocol -**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **34 pages, 452 schemas** +**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **34 pages, 453 schemas** REST contracts, endpoints, routing, realtime, batch, discovery. @@ -92,7 +92,7 @@ REST contracts, endpoints, routing, realtime, batch, discovery. | [`package-api-assembled.zod.ts`](/docs/references/api/package-api-assembled) | `AssembledInstalledPackage`, `GetInstalledPackageResponse`, `InstalledPackageAtEitherStage`, `ListInstalledPackagesResponse` | | [`package-lifecycle.zod.ts`](/docs/references/api/package-lifecycle) | `DiscardPackageDraftsResponse`, `DuplicatePackageResponse`, `ImportUnmanagedPackageRequest`, `ImportUnmanagedPackageResponse`, `ListPackageCommitsResponse`, `PackageExportManifest`, `PackagePublishResult`, `ReassignOrphanedMetadataResponse`, `RevertPackageCommitResponse`, `RollbackToPackageCommitResponse` | | [`plugin-rest-api.zod.ts`](/docs/references/api/plugin-rest-api) | `ErrorHandlingConfig`, `OpenApiGenerationConfig`, `RequestValidationConfig`, `ResponseEnvelopeConfig`, `RestApiEndpoint`, `RestApiPluginConfig`, `RestApiRouteCategory`, `RestApiRouteRegistration`, `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`, `FlowRegistration`, `FlowRegistrationRefusal`, `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`, `GetRecordFieldAccessResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HistoryMetaItemRequest`, `HistoryMetaItemResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListDraftsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `MetadataAuthoringClient`, `MetadataAuthoringSurface`, `MetadataConflictError`, `Notification`, `NotificationPreferences`, `PublishMetaItemRequest`, `PublishMetaItemResponse`, `PublishPackageDraftsResponse`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `RollbackMetaItemResponse`, `RuntimeAuthoringIssue`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SearchAllHit`, `SearchAllPageHit`, `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`, `FlowRegistration`, `FlowRegistrationRefusal`, `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`, `GetRecordFieldAccessResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HistoryMetaItemRequest`, `HistoryMetaItemResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListDraftsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `MetadataAuthoringClient`, `MetadataAuthoringSurface`, `MetadataConflictError`, `Notification`, `NotificationPreferences`, `PublishMetaItemRequest`, `PublishMetaItemResponse`, `PublishPackageDraftsRequest`, `PublishPackageDraftsResponse`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `RollbackMetaItemResponse`, `RuntimeAuthoringIssue`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SearchAllHit`, `SearchAllPageHit`, `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/api.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md index 0f34efe8cca..305b166416f 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md @@ -19,4 +19,4 @@ hand-patch a number here** — fix the code or the verdict and regenerate. | Dir | Sites | |---|---| -| `api/` | 461 | +| `api/` | 463 | diff --git a/packages/client/src/route-ledger-response-schema.test.ts b/packages/client/src/route-ledger-response-schema.test.ts index 3c440839bf7..aeb17f31c37 100644 --- a/packages/client/src/route-ledger-response-schema.test.ts +++ b/packages/client/src/route-ledger-response-schema.test.ts @@ -53,7 +53,7 @@ import { AUTH_ROUTE_LEDGER } from '../../plugins/plugin-auth/src/auth-route-ledg /** The five independent ledgers, labelled so a failure names the file to open. */ const LEDGERS: ReadonlyArray<{ readonly label: string; - readonly rows: ReadonlyArray<{ route: string; responseSchema?: string }>; + readonly rows: ReadonlyArray<{ route: string; responseSchema?: string; requestSchema?: string }>; }> = [ { label: 'packages/runtime/src/route-ledger.ts', rows: ROUTE_LEDGER }, { label: 'packages/rest/src/rest-route-ledger.ts', rows: REST_ROUTE_LEDGER }, @@ -185,3 +185,34 @@ describe('[#5791] every ledgered `responseSchema` names a real spec schema', () ).toContain('GET /api/v1/discovery'); }); }); + +/** + * The request-side twin (ADR-0146 D1): `requestSchema`, so far on the dispatcher + * ledger only, names the spec export declaring the BODY a route reads. Same + * resolver, same reason — a name is data, and a typo or a retired schema must + * fail here rather than rot. That a filled row has conformance coverage is + * pinned beside the coverage itself (`packages-publish-drafts-request-conformance.test.ts` + * in `packages/runtime`). + */ +describe('every ledgered `requestSchema` names a real spec schema', () => { + const requestRows = () => + LEDGERS.flatMap(({ label, rows }) => + rows + .filter((r): r is typeof r & { requestSchema: string } => r.requestSchema !== undefined) + .map((r) => ({ ledger: label, route: r.route, requestSchema: r.requestSchema })), + ); + + it('resolves each declared name against the live `@objectstack/spec/api` / `/api-assembled` exports', () => { + const broken = requestRows() + .map(({ ledger, route, requestSchema }) => { + const why = resolutionFailure(requestSchema); + return why ? `${ledger} — \`${route}\`: requestSchema '${requestSchema}' ${why}` : undefined; + }) + .filter((m): m is string => m !== undefined); + expect(broken, 'Ledger rows whose `requestSchema` does not resolve to an exported zod schema.').toEqual([]); + }); + + it('anti-vacuity: the publish-drafts row declares its request body', () => { + expect(requestRows().map((r) => r.route)).toContain('POST /packages/:id/publish-drafts'); + }); +}); diff --git a/packages/runtime/src/domains/packages-publish-drafts-request-conformance.test.ts b/packages/runtime/src/domains/packages-publish-drafts-request-conformance.test.ts new file mode 100644 index 00000000000..22aeb03bc15 --- /dev/null +++ b/packages/runtime/src/domains/packages-publish-drafts-request-conformance.test.ts @@ -0,0 +1,203 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * ADR-0146 D1 — request-side conformance for `POST /packages/:id/publish-drafts`: + * what the door does with each member of the body + * `PublishPackageDraftsRequestSchema` (`@objectstack/spec/api`) declares, and + * with the members it does not declare. + * + * This is the coverage the ledger's `requestSchema` field is forbidden to be + * written without (`route-ledger.ts`, the field's JSDoc), and the reason the + * `POST /packages/:id/publish-drafts` row may carry + * `requestSchema: 'PublishPackageDraftsRequestSchema'`. The request schema + * declares the body; it does not claim the door parses the body with it. So + * each member's behaviour at the door is pinned here as measured: + * + * - `message` — a string reaches `publishPackageDrafts` as sent; a non-string + * is dropped (the publish goes ahead with no note), not refused; + * - `backfill` — reaches the protocol as sent, and a body the schema refuses + * is refused by the protocol's shape judgement `400 VALIDATION_ERROR` + * before anything is published (the real protocol, below); + * - `actor` — never reaches `publishPackageDrafts`: the publisher is the + * authenticated caller. That is why the schema strips unknown keys instead + * of refusing them: the SDK's deprecated `opts.actor` keeps working. + * + * The protocol is a double for the forwarding cases (what it receives is the + * assertion) and the real `ObjectStackProtocolImplementation` for the + * refusal cases, whose shape judgement runs before any server state is read. + */ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { PublishPackageDraftsRequestSchema } from '@objectstack/spec/api'; +import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; +import { HttpDispatcher } from '../http-dispatcher.js'; +import { ROUTE_LEDGER } from '../route-ledger.js'; + +const PKG = 'com.workspace'; +const USER = 'u_pkg_admin'; + +/** `/packages` demands `manage_metadata` on every state-changing route. */ +const PKG_ADMIN = () => ({ + request: {}, + executionContext: { + userId: USER, + systemPermissions: ['manage_metadata', 'studio.access', 'setup.access'], + }, +}) as any; + +const HAPPY = { + success: true, outcome: 'published', publishedCount: 1, failedCount: 0, + published: [{ type: 'view', name: 'notes_open', version: 'sha256:aa11' }], failed: [], + commitId: 'cmt_01', +}; + +/** + * A door over `protocol`. `apps` are the apps the ADR-0045 flip loop reads + * back after the publish; none by default, so the only producer call a body + * member can reach is `publishPackageDrafts` itself. + */ +function makeDoor(protocol: Record, apps: Array> = []) { + const getMetaItems = vi.fn().mockImplementation(async () => ({ items: apps.map((a) => ({ ...a })) })); + const saveMetaItem = vi.fn().mockImplementation(async () => ({ ok: true })); + const service = Object.assign(protocol, { getMetaItems, saveMetaItem }); + const kernel: any = { + getService: (name: string) => { + if (name === 'protocol') return Promise.resolve(service); + if (name === 'objectql') { + return Promise.resolve({ + insert: vi.fn(), find: vi.fn(), update: vi.fn(), + registry: { getAllPackages: vi.fn().mockReturnValue([]) }, + }); + } + if (name === 'metadata') return Promise.resolve({ getObject: vi.fn() }); + return null; + }, + context: { getService: () => null, trigger: vi.fn() }, + }; + return { dispatcher: new HttpDispatcher(kernel), saveMetaItem }; +} + +/** Publish `body` through the door over a protocol double; answer the wire response and what the protocol received. */ +async function publishWith(body: unknown, apps?: Array>) { + const publishPackageDrafts = vi.fn().mockImplementation(async () => JSON.parse(JSON.stringify(HAPPY))); + const door = makeDoor({ publishPackageDrafts }, apps); + const result = await door.dispatcher.handlePackages(`/${PKG}/publish-drafts`, 'POST', body, {}, PKG_ADMIN()); + expect(publishPackageDrafts).toHaveBeenCalledTimes(1); + const sent = publishPackageDrafts.mock.calls[0]![0] as Record; + return { response: result.response as any, sent, saveMetaItem: door.saveMetaItem }; +} + +/** The request members of what the protocol received: everything but the path id and the caller's context. */ +function bodyMembersSent(sent: Record) { + const { packageId, context, actor, ...rest } = sent; + expect(packageId).toBe(PKG); + expect(actor).toBe(USER); + expect(context).toMatchObject({ userId: USER }); + return rest; +} + +afterEach(() => vi.restoreAllMocks()); + +describe('publish-drafts request body conforms to PublishPackageDraftsRequestSchema (ADR-0146 D1)', () => { + it('the ledger row names the schema this suite pins', () => { + const row = ROUTE_LEDGER.find((e) => e.route === 'POST /packages/:id/publish-drafts'); + expect(row, 'the publish-drafts row must exist in ROUTE_LEDGER').toBeDefined(); + expect(row!.requestSchema).toBe('PublishPackageDraftsRequestSchema'); + }); + + it('each body the schema accepts reaches the protocol as the schema parses it', async () => { + const bodies: unknown[] = [ + {}, + { message: 'Split notes into open and closed lists' }, + { backfill: [{ object: 'crm_account', field: 'is_key' }] }, + { message: 'Add the key-account flag', backfill: [{ object: 'crm_account', field: 'is_key' }] }, + { message: '', backfill: [] }, + ]; + for (const body of bodies) { + const parsed = PublishPackageDraftsRequestSchema.parse(body); + const { response, sent } = await publishWith(body); + expect(response.status, JSON.stringify(body)).toBe(200); + expect(bodyMembersSent(sent), JSON.stringify(body)).toEqual(parsed); + } + }); + + it('an absent body reads as `{}`: no note and no backfill', async () => { + for (const body of [undefined, null]) { + const { response, sent } = await publishWith(body); + expect(response.status).toBe(200); + expect(bodyMembersSent(sent)).toEqual({}); + } + }); + + it('a body `actor` is stripped by the schema and never reaches the publish: the caller is recorded (the SDK still sends it)', async () => { + const body = { actor: 'someone_else', message: 'note' }; + expect(PublishPackageDraftsRequestSchema.parse(body)).toEqual({ message: 'note' }); + const { response, sent } = await publishWith(body); + // Not refused: an existing `packages.publishDrafts({ actor })` call keeps working. + expect(response.status).toBe(200); + expect(sent.actor).toBe(USER); + expect(bodyMembersSent(sent)).toEqual({ message: 'note' }); + }); + + it('members the protocol method takes but the schema does not declare are not read from the body', async () => { + const { sent } = await publishWith({ aiModel: 'some-model', context: { userId: 'u_other' }, packageId: 'com.other' }); + expect(sent.aiModel).toBeUndefined(); + expect(bodyMembersSent(sent)).toEqual({}); + }); + + it('a note the schema refuses is dropped by the door, not refused: the publish goes ahead with no note', async () => { + // Measured divergence: the schema declares `message` a string and the + // door forwards only a string, but answers 200 for anything else. A + // door that parses the body with the schema flips this pin to a 400. + expect(PublishPackageDraftsRequestSchema.safeParse({ message: 42 }).success).toBe(false); + const { response, sent } = await publishWith({ message: 42 }); + expect(response.status).toBe(200); + expect(bodyMembersSent(sent)).toEqual({}); + }); + + it('today the ADR-0045 app re-save after the publish still forwards a body `actor` (the one undeclared read left)', async () => { + // The publish itself records the caller (above); the visibility flip + // that re-saves each unpublished app afterwards still passes a body + // `actor` to `saveMetaItem`. Removing that read is the sibling-door + // card's (the six package doors' actor fix). When it lands this pin + // goes red: flip it to `USER`/absent and drop the sentence about it + // from the publish-drafts row note in `route-ledger.ts`. + const { saveMetaItem } = await publishWith( + { actor: 'someone_else' }, + [{ name: 'crm', label: 'CRM', _unpublished: true }], + ); + expect(saveMetaItem).toHaveBeenCalledTimes(1); + expect((saveMetaItem.mock.calls[0]![0] as Record).actor).toBe('someone_else'); + }); +}); + +describe('a backfill the schema refuses is refused at the door before anything is published (real protocol)', () => { + /** The real protocol over an engine nothing may touch: the shape judgement runs before any read. */ + function realProtocol() { + const untouchable = new Proxy({}, { + get(_t, key) { throw new Error(`the engine was touched (${String(key)}) before the shape judgement`); }, + }); + return new ObjectStackProtocolImplementation(untouchable as any, undefined, undefined, 'package-author'); + } + + it('answers 400 VALIDATION_ERROR naming the entry, for each malformed shape', async () => { + const bodies: Array<{ body: Record; path: string }> = [ + { body: { backfill: { object: 'crm_account', field: 'is_key' } }, path: 'backfill' }, + { body: { backfill: ['crm_account.is_key'] }, path: 'backfill.0' }, + { body: { backfill: [{ object: 'crm_account' }] }, path: 'backfill.0' }, + { body: { backfill: [{ object: '', field: 'is_key' }] }, path: 'backfill.0' }, + { body: { backfill: [{ object: 'crm_account', field: 'is_key', value: true }] }, path: 'backfill.0' }, + ]; + for (const { body, path } of bodies) { + expect(PublishPackageDraftsRequestSchema.safeParse(body).success, JSON.stringify(body)).toBe(false); + const protocol = realProtocol(); + const publish = vi.spyOn(protocol, 'publishPackageDrafts'); + const { dispatcher } = makeDoor(protocol as unknown as Record); + const result = await dispatcher.handlePackages(`/${PKG}/publish-drafts`, 'POST', body, {}, PKG_ADMIN()); + const response = result.response as any; + expect(response.status, JSON.stringify(body)).toBe(400); + expect(response.body?.error?.code, JSON.stringify(body)).toBe('VALIDATION_ERROR'); + expect(JSON.stringify(response.body), JSON.stringify(body)).toContain(path); + expect(publish).toHaveBeenCalledTimes(1); + } + }); +}); diff --git a/packages/runtime/src/route-ledger.ts b/packages/runtime/src/route-ledger.ts index 2dcc721aece..84574cf8761 100644 --- a/packages/runtime/src/route-ledger.ts +++ b/packages/runtime/src/route-ledger.ts @@ -154,6 +154,28 @@ export interface RouteLedgerEntry { * resolution belongs in the guard that can import the spec, not in the data. */ responseSchema?: string; + /** + * Name of the `@objectstack/spec/api` export declaring the JSON BODY this + * route READS — the request-side twin of {@link responseSchema}, under the + * same rules: a NAME (this module stays import-free), resolved against the + * live spec exports by `packages/client/src/route-ledger-response-schema.test.ts`, + * and ABSENT MEANS "UNDECLARED", never a defect. + * + * What a filled row claims: the named schema's members are exactly the body + * members the handler reads, with the shapes it reads them in. It does NOT + * by itself claim the handler REFUSES a body the schema refuses — whether + * and where a violating body is refused is a per-member fact, pinned by the + * row's conformance suite. (This is NOT the REST catalog's `requestSchema` + * in `@objectstack/spec/api` `plugin-rest-api.zod.ts`, whose ratchet demands + * the route refuse every violating body; the dispatcher's `/packages` + * family is not in that catalog.) + * + * ⛔ DO NOT FILL A ROW THAT HAS NO CONFORMANCE COVERAGE: a suite that drives + * THIS handler, shows each declared member reaching the producer as sent and + * an undeclared member not reaching it, and asserts this row names the + * schema it parses with. + */ + requestSchema?: string; /** * The AUTHORIZATION posture this route has been REVIEWED to have, named by * the `authz-conformance.matrix.ts` row that classifies it (ADR-0056 D10). @@ -499,8 +521,9 @@ export const ROUTE_LEDGER: readonly RouteLedgerEntry[] = [ responseSchema: 'PackagePublishResultSchema', note: 'dispatch() answers through the { success, data } envelope, so the named schema is the `data` — `MetadataManager.publishPackage`\'s declared return, whose exact schema already existed in spec `system/metadata-persistence.zod.ts` and is re-exported into `/api`, never declared there a second time. Conformance: spec `api/package-lifecycle.test.ts`' }, { route: 'POST /packages/:id/publish-drafts', domain: '/packages', disposition: 'sdk', client: 'packages.publishDrafts', + requestSchema: 'PublishPackageDraftsRequestSchema', responseSchema: 'PublishPackageDraftsResponseSchema', - note: 'dispatch() answers through the { success, data } envelope, so the named schema is the `data` — the protocol result AFTER the door\'s own mutations (seedApplied back-fill, ADR-0045 unhiddenApps/unhideError, rebindError). Fillable because packages-publish-drafts-response-conformance.test.ts drives THIS handler and parses the payload it answers; the producer half is pinned in objectql\'s publish-package-drafts-response-conformance.test.ts. `probes` is deliberately opaque in the declaration, upgraded to a modeled schema only when a consumer needs a field of it' }, + note: 'dispatch() answers through the { success, data } envelope, so the named schema is the `data` — the protocol result AFTER the door\'s own mutations (seedApplied back-fill, ADR-0045 unhiddenApps/unhideError, rebindError). Fillable because packages-publish-drafts-response-conformance.test.ts drives THIS handler and parses the payload it answers; the producer half is pinned in objectql\'s publish-package-drafts-response-conformance.test.ts. `probes` is deliberately opaque in the declaration, upgraded to a modeled schema only when a consumer needs a field of it. `requestSchema` is the body the door reads (`message`, `backfill`; a body `actor` is ignored, so the schema strips unknown keys rather than refusing them), filled because packages-publish-drafts-request-conformance.test.ts drives THIS handler and pins what each body member does' }, { route: 'POST /packages/:id/discard-drafts', domain: '/packages', disposition: 'sdk', client: 'packages.discardDrafts', responseSchema: 'DiscardPackageDraftsResponseSchema', note: 'Enveloped — the named schema is the `data`: `discardPackageDrafts`\'s declared return, transcribed describe-only. Conformance: spec `api/package-lifecycle.test.ts`' }, diff --git a/packages/spec/api-surface/api.json b/packages/spec/api-surface/api.json index d5104cd5366..1868fc7bca8 100644 --- a/packages/spec/api-surface/api.json +++ b/packages/spec/api-surface/api.json @@ -806,6 +806,8 @@ "PublishMetaItemRequestSchema (const)", "PublishMetaItemResponse (type)", "PublishMetaItemResponseSchema (const)", + "PublishPackageDraftsRequest (type)", + "PublishPackageDraftsRequestSchema (const)", "PublishPackageDraftsResponse (type)", "PublishPackageDraftsResponseSchema (const)", "QueryAdapterConfig (type)", diff --git a/packages/spec/authorable-surface/api.json b/packages/spec/authorable-surface/api.json index 3249d7097b6..bc78a34f0f7 100644 --- a/packages/spec/authorable-surface/api.json +++ b/packages/spec/authorable-surface/api.json @@ -1370,6 +1370,8 @@ "api/PublishMetaItemResponse:seq", "api/PublishMetaItemResponse:success", "api/PublishMetaItemResponse:version", + "api/PublishPackageDraftsRequest:backfill", + "api/PublishPackageDraftsRequest:message", "api/PublishPackageDraftsResponse:backfillApplied", "api/PublishPackageDraftsResponse:commitId", "api/PublishPackageDraftsResponse:failed", diff --git a/packages/spec/declaration-map/api.json b/packages/spec/declaration-map/api.json index c90c0605468..3f176d796ce 100644 --- a/packages/spec/declaration-map/api.json +++ b/packages/spec/declaration-map/api.json @@ -589,6 +589,8 @@ "PublishMetaItemRequestSchema": "api/PublishMetaItemRequest", "PublishMetaItemResponse": "api/PublishMetaItemResponse", "PublishMetaItemResponseSchema": "api/PublishMetaItemResponse", + "PublishPackageDraftsRequest": "api/PublishPackageDraftsRequest", + "PublishPackageDraftsRequestSchema": "api/PublishPackageDraftsRequest", "PublishPackageDraftsResponse": "api/PublishPackageDraftsResponse", "PublishPackageDraftsResponseSchema": "api/PublishPackageDraftsResponse", "QueryAdapterConfig": "api/QueryAdapterConfig", diff --git a/packages/spec/export-origins/api.json b/packages/spec/export-origins/api.json index d37f42c14b8..2d5dbd42677 100644 --- a/packages/spec/export-origins/api.json +++ b/packages/spec/export-origins/api.json @@ -777,6 +777,8 @@ "PublishMetaItemRequestSchema": "src/api/protocol.zod.ts#PublishMetaItemRequestSchema (const)", "PublishMetaItemResponse": "src/api/protocol.zod.ts#PublishMetaItemResponse (type)", "PublishMetaItemResponseSchema": "src/api/protocol.zod.ts#PublishMetaItemResponseSchema (const)", + "PublishPackageDraftsRequest": "src/api/protocol.zod.ts#PublishPackageDraftsRequest (type)", + "PublishPackageDraftsRequestSchema": "src/api/protocol.zod.ts#PublishPackageDraftsRequestSchema (const)", "PublishPackageDraftsResponse": "src/api/protocol.zod.ts#PublishPackageDraftsResponse (type)", "PublishPackageDraftsResponseSchema": "src/api/protocol.zod.ts#PublishPackageDraftsResponseSchema (const)", "QueryAdapterConfig": "src/api/query-adapter.zod.ts#QueryAdapterConfig (type)", diff --git a/packages/spec/json-schema.manifest/api.json b/packages/spec/json-schema.manifest/api.json index f766597010d..6335b4d7657 100644 --- a/packages/spec/json-schema.manifest/api.json +++ b/packages/spec/json-schema.manifest/api.json @@ -322,6 +322,7 @@ "api/ProvenanceWaiver", "api/PublishMetaItemRequest", "api/PublishMetaItemResponse", + "api/PublishPackageDraftsRequest", "api/PublishPackageDraftsResponse", "api/QueryAdapterConfig", "api/QueryAdapterTarget", diff --git a/packages/spec/src/api/protocol.test.ts b/packages/spec/src/api/protocol.test.ts index 55ace91a446..d51cdc1c963 100644 --- a/packages/spec/src/api/protocol.test.ts +++ b/packages/spec/src/api/protocol.test.ts @@ -1242,7 +1242,60 @@ describe('RuntimeAuthoringIssueSchema (the ONE finding shape)', () => { }); }); -import { PublishPackageDraftsResponseSchema } from './protocol.zod'; +import { PublishPackageDraftsRequestSchema, PublishPackageDraftsResponseSchema } from './protocol.zod'; + +/** + * ADR-0146 D1 — the request half of the batch publish door: the body + * `POST /packages/:id/publish-drafts` reads. What the door does with each body + * is pinned against the real handler in + * `packages/runtime/src/domains/packages-publish-drafts-request-conformance.test.ts`; + * here, what the declaration itself accepts and refuses. + */ +describe('PublishPackageDraftsRequestSchema (declares the batch publish request body)', () => { + const issuesOf = (body: unknown) => { + const r = PublishPackageDraftsRequestSchema.safeParse(body); + expect(r.success, `expected ${JSON.stringify(body)} to be refused`).toBe(false); + return r.success ? [] : r.error.issues.map((i) => ({ code: i.code, path: i.path.join('.') })); + }; + + it('accepts an empty body, a note, a backfill, and both', () => { + expect(PublishPackageDraftsRequestSchema.parse({})).toEqual({}); + expect(PublishPackageDraftsRequestSchema.parse({ message: 'Split notes into two lists' })) + .toEqual({ message: 'Split notes into two lists' }); + const backfill = [{ object: 'crm_account', field: 'is_key' }]; + expect(PublishPackageDraftsRequestSchema.parse({ backfill })).toEqual({ backfill }); + expect(PublishPackageDraftsRequestSchema.parse({ message: '', backfill: [] })).toEqual({ message: '', backfill: [] }); + }); + + it('is not strict: the SDK\'s deprecated `actor` is accepted and stripped, never carried through', () => { + // `packages.publishDrafts` still sends `opts.actor`; the door ignores it and + // records the authenticated caller. A strict object would refuse that call. + const parsed = PublishPackageDraftsRequestSchema.parse({ actor: 'someone_else', message: 'note', aiModel: 'x' }); + expect(parsed).toEqual({ message: 'note' }); + expect('actor' in parsed).toBe(false); + }); + + it('refuses a note that is not a string', () => { + expect(issuesOf({ message: 42 })).toEqual([{ code: 'invalid_type', path: 'message' }]); + expect(issuesOf({ message: null })).toEqual([{ code: 'invalid_type', path: 'message' }]); + }); + + it('refuses a backfill that is not an array of { object, field } pairs of non-empty strings', () => { + expect(issuesOf({ backfill: { object: 'crm_account', field: 'is_key' } })) + .toEqual([{ code: 'invalid_type', path: 'backfill' }]); + expect(issuesOf({ backfill: ['crm_account.is_key'] })).toEqual([{ code: 'invalid_type', path: 'backfill.0' }]); + expect(issuesOf({ backfill: [{ object: 'crm_account' }] })).toEqual([{ code: 'invalid_type', path: 'backfill.0.field' }]); + expect(issuesOf({ backfill: [{ object: '', field: 'is_key' }] })).toEqual([{ code: 'too_small', path: 'backfill.0.object' }]); + // An entry names an object and a field and nothing else, as the protocol's + // shape judgement refuses (`unrecognized_keys` there too). + expect(issuesOf({ backfill: [{ object: 'crm_account', field: 'is_key', value: true }] })) + .toEqual([{ code: 'unrecognized_keys', path: 'backfill.0' }]); + }); + + it('declares exactly the two members the door reads', () => { + expect(Object.keys(PublishPackageDraftsRequestSchema.shape).sort()).toEqual(['backfill', 'message']); + }); +}); /** * #9406 — the same suite one door over again, for the BATCH publish door diff --git a/packages/spec/src/api/protocol.zod.ts b/packages/spec/src/api/protocol.zod.ts index 50c901126a1..87c51f9750f 100644 --- a/packages/spec/src/api/protocol.zod.ts +++ b/packages/spec/src/api/protocol.zod.ts @@ -1459,6 +1459,54 @@ export const PublishMetaItemResponseSchema = lazySchema(() => z.object({ ), })); +/** + * Publish Package Drafts Request — the body of the "publish whole app" door. + * + * Declares the JSON body `POST /api/v1/packages/:id/publish-drafts` reads + * (ADR-0033, ADR-0146 D1), measured at the door + * (`packages/runtime/src/domains/packages.ts`) and not assumed. The package + * is the `:id` path segment, never a body member. The door reads exactly two + * body members and hands them to `publishPackageDrafts`: + * + * - `message` — forwarded only when it is a string; + * - `backfill` — forwarded as sent whenever the key is present; the protocol + * judges its shape before anything is published and refuses the whole + * request `400 VALIDATION_ERROR` with every bad entry in `issues[]`. + * + * Every other member is ignored, so the object is NOT strict: unknown keys + * are stripped, not refused. That keeps an existing caller working — + * `@objectstack/client`'s `packages.publishDrafts` still sends its deprecated + * `opts.actor`, and the door records the request's authenticated identity + * instead (ADR-0146 D1). `actor` is deliberately not declared: the publisher + * is never a name the body supplies. `aiModel` and `context`, which the + * protocol method also takes, are not declared either: the door never reads + * them from the body (the execution context is the caller's own). + * + * The body is optional on the wire; an absent body reads as `{}`, a publish + * with no note and no backfill. + */ +export const PublishPackageDraftsRequestSchema = lazySchema(() => z.object({ + message: z.string().optional().describe( + 'The author\'s change note (ADR-0146 D1). The package commit records it, ' + + 'and so does every item history row this publish writes, as ' + + '`change_note` (read back as each history event\'s `message`). Absent or ' + + 'empty, those rows record the sentence naming the package instead.', + ), + backfill: z.array(z.object({ + object: z.string().min(1).describe('Name of the object whose existing rows are written.'), + field: z.string().min(1).describe('Name of the field this publish adds to that object.'), + }).strict()).optional().describe( + 'Opt-in: fields this publish adds whose literal `defaultValue` ' + + 'is written into the object\'s existing rows once the drafts are live, as ' + + 'the signed-in caller. Absent or `[]`, no row is touched. Each entry ' + + 'names an object and a field and nothing else, and names it once. The ' + + 'whole request is refused `400 VALIDATION_ERROR` before anything is ' + + 'published when an entry is malformed, repeated, or names a field that ' + + 'is not new in this publish or declares no literal default; ' + + '`backfillApplied` in the response reports the rows written per field.', + ), +})); + /** * Publish Package Drafts Response — the "publish whole app" door. * @@ -4007,6 +4055,11 @@ export type MetadataConflictError = z.input; export type MetadataConflictErrorParsed = z.infer; export type PublishMetaItemRequest = z.input; export type PublishMetaItemResponse = z.input; +/** + * The batch publish door's request body — `POST /packages/:id/publish-drafts` + * (ADR-0146 D1). Unknown members are stripped, not refused. + */ +export type PublishPackageDraftsRequest = z.input; /** * The batch publish door's response — `POST /packages/:id/publish-drafts` * (#9406). `probes` is `unknown` by declaration, not by omission: the #9406 diff --git a/packages/spec/src/type-alias-convention.pin.test.ts b/packages/spec/src/type-alias-convention.pin.test.ts index 560ea561e21..90fb21a7b90 100644 --- a/packages/spec/src/type-alias-convention.pin.test.ts +++ b/packages/spec/src/type-alias-convention.pin.test.ts @@ -285,7 +285,7 @@ import type * as M194 from './automation/assignment-rule.zod.js'; import type * as M195 from './security/queue.zod.js'; // --------------------------------------------------------------------------- -// 805 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. +// 806 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 @@ -618,6 +618,7 @@ export type Iso_api_protocol__MetadataAuthoringClientSchema = Assert, z.infer< typeof M28.MetadataAuthoringSurfaceSchema > >>; export type Iso_api_protocol__PublishMetaItemRequestSchema = Assert, z.infer< typeof M28.PublishMetaItemRequestSchema > >>; export type Iso_api_protocol__PublishMetaItemResponseSchema = Assert, z.infer< typeof M28.PublishMetaItemResponseSchema > >>; +export type Iso_api_protocol__PublishPackageDraftsRequestSchema = Assert, z.infer< typeof M28.PublishPackageDraftsRequestSchema > >>; export type Iso_api_protocol__PublishPackageDraftsResponseSchema = Assert, z.infer< typeof M28.PublishPackageDraftsResponseSchema > >>; export type Iso_api_protocol__RealtimeConnectRequestSchema = Assert, z.infer< typeof M28.RealtimeConnectRequestSchema > >>; export type Iso_api_protocol__RealtimeConnectResponseSchema = Assert, z.infer< typeof M28.RealtimeConnectResponseSchema > >>; @@ -1739,7 +1740,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 805 isomorphic pins', () => { + it('still declares all 806 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 @@ -2700,7 +2701,13 @@ describe('ADR-0122 type-alias convention', () => { // `tencentcloud-tc3-hmac-sha256` shape, no default and no transform // (Iso_shared_connectorAuth__ConnectorInstanceSignedTencentCloudTc3AuthSchema). // +1 added. - expect(pins).toHaveLength(805); + // 805 -> 806 is #28406 (ADR-0146 D1): `PublishPackageDraftsRequestSchema`, + // the request half of the batch publish door whose response half is + // pinned beside it — an optional `z.string()` and an optional array of a + // strict object of two `z.string().min(1)`s, no `.default()`, + // `.transform()`, `.catch()` or `.pipe()` + // (Iso_api_protocol__PublishPackageDraftsRequestSchema). +1 added. + expect(pins).toHaveLength(806); // The count is stated in PROSE twice as well — this case's title and the // section header above the pin list — and until commit c6b05c76a nothing read either