diff --git a/.changeset/rest-server-openapi31-block-removed.md b/.changeset/rest-server-openapi31-block-removed.md new file mode 100644 index 0000000000..fb6c1d8502 --- /dev/null +++ b/.changeset/rest-server-openapi31-block-removed.md @@ -0,0 +1,54 @@ +--- +"@objectstack/spec": major +--- + +refactor(spec)!: remove the `RestServerConfig.openApi31` block — OpenAPI 3.1 webhooks/callbacks config that no runtime ever read (#4579, ADR-0049) + +`RestServerConfig.openApi31` (typed by `OpenApi31ExtensionsSchema`, with +`OpenApiWebhookEventSchema` and `CallbackSchema` under it) was authorable and +inert end to end — the declared ≠ enforced shape ADR-0049 exists to close: + +- The REST server's `normalizeConfig` (`packages/rest/src/rest-server.ts`) + forwards only `api` / `crud` / `metadata` / `batch` / `routes`; `openApi31` + was silently discarded. +- The served `GET /openapi.json` is the pre-generated `@objectstack/spec` + contract, enriched at request time with the live server URL and the + runtime-registered objects — it never consulted the config. +- `gen:openapi` (`scripts/build-openapi.ts`) never read a webhook or callback. + +So a webhook an author declared under `openApi31.webhooks` **never appeared in +any served OpenAPI document** — false compliance, the same class as the +connector-webhook gap (#3197). Zero import-level consumers existed for all +three schemas across objectstack / cloud / objectui (three-repo scan in #4579). + +Migration (FROM → TO): + +- `openApi31` in a `RestServerConfig` value (REST plugin constructor / + `plugin-hono-server` `restConfig`) → **delete the key**. There is no + replacement: nothing ever read it, so removing it changes no served + document. The key is tombstoned, not silently stripped — + `RestServerConfigSchema` is not `.strict()`, so a `retiredKey()` tombstone + makes authoring it a `tsc` error and a parse error carrying this + prescription. +- `import { OpenApi31Extensions(Schema), Callback(Schema), OpenApiWebhookEvent(Schema) } from '@objectstack/spec/api'` + → **no replacement export** (TS2305 after upgrade). For a real outbound + webhook use `Webhook` from `@objectstack/spec/automation`; for connector + webhook events use `WebhookEvent` from `@objectstack/spec/integration`. + (`OpenApiWebhookEvent(Schema)` was the #4572 rename of `./api`'s + `WebhookEvent(Schema)`; this removal absorbs that rename — pre-16 imports of + the bare name land here too.) +- Config-driven OpenAPI 3.1 webhooks/callbacks documentation is a **new + capability**: if it is ever needed it returns via the enforce route of + ADR-0049, through a new ADR — not by re-declaring inert keys. + +The retirement kit: `retiredKey()` tombstone on the non-strict schema (parse + +`tsc` both audible); ADR-0087 D3 semantic migration +`rest-server-openapi31-block-removed` (plugin TS config is never a +`sys_metadata` shape — the stack tree's `api` block declares only its four +scoping/auth knobs — so there is no stored row or stack source for a D2 +conversion to rewrite); baselines (`authorable-surface.json` [RETIRED] line, +`json-schema.manifest.json` def removals, `api-surface.json`) regenerated +deliberately; compiler-API export pin + sabotage-verified tombstone tests. + +No runtime behaviour changes — that impossibility is the reason for the +removal: the served `/openapi.json` is byte-identical before and after. diff --git a/.changeset/webhook-dual-source-api-side.md b/.changeset/webhook-dual-source-api-side.md index e77882ae0e..095fe3f26f 100644 --- a/.changeset/webhook-dual-source-api-side.md +++ b/.changeset/webhook-dual-source-api-side.md @@ -28,14 +28,19 @@ import-statement-level consumer scan: framework, cloud, objectui): `registrationEndpoint` / `enabled`). - **Renamed** `WebhookEventSchema` / `WebhookEvent` in `@objectstack/spec/api` → `OpenApiWebhookEventSchema` / `OpenApiWebhookEvent` (same shape, rename - only; joins the existing `OpenApi*` family). `OpenApi31ExtensionsSchema.webhooks` - now references the renamed schema — its parsed/authored shape is unchanged. + only; joins the existing `OpenApi*` family). + **Superseded in the same major (#4579, ADR-0049):** the renamed pair was + then removed outright with the whole inert `RestServerConfig.openApi31` + block, so the rename never ships as a landing spot — see the + `rest-server-openapi31-block-removed` changeset. - FROM `import { WebhookEvent } from '@objectstack/spec/api'` → - TO `import { OpenApiWebhookEvent } from '@objectstack/spec/api'` (if you - meant the OpenAPI 3.1 webhook descriptor), or - `import { WebhookEvent } from '@objectstack/spec/integration'` (if you - meant the connector event enum — check which shape you actually consume: - object vs string enum). + TO: no `./api` replacement exists (the OpenAPI 3.1 webhook descriptor was + removed in #4579 — nothing ever rendered it into the served + `/openapi.json`). Use + `import { WebhookEvent } from '@objectstack/spec/integration'` if you + meant the connector event enum (check which shape you actually consume: + object vs string enum), or `Webhook` from `@objectstack/spec/automation` + for a real outbound webhook. - `@objectstack/spec/integration`'s `WebhookConfig(Schema)` / `WebhookEvent(Schema)` are **unchanged** and are now the sole owners of the bare names. Imports from `./integration` need no migration. diff --git a/content/docs/references/api/rest-server.mdx b/content/docs/references/api/rest-server.mdx index c9b927964c..e285023664 100644 --- a/content/docs/references/api/rest-server.mdx +++ b/content/docs/references/api/rest-server.mdx @@ -38,8 +38,8 @@ Architecture alignment: ## TypeScript Usage ```typescript -import { BatchEndpointsConfigSchema, CallbackSchema, CrudEndpointPatternSchema, CrudEndpointsConfigSchema, CrudOperation, EndpointRegistrySchema, GeneratedEndpointSchema, MetadataEndpointsConfigSchema, OpenApi31ExtensionsSchema, OpenApiWebhookEventSchema, RestApiConfigSchema, RestServerConfigSchema, RouteGenerationConfigSchema } from '@objectstack/spec/api'; -import type { BatchEndpointsConfig, Callback, CrudEndpointPattern, CrudEndpointsConfig, CrudOperation, EndpointRegistry, GeneratedEndpoint, MetadataEndpointsConfig, OpenApi31Extensions, OpenApiWebhookEvent, RestApiConfig, RestServerConfig, RouteGenerationConfig } from '@objectstack/spec/api'; +import { BatchEndpointsConfigSchema, CrudEndpointPatternSchema, CrudEndpointsConfigSchema, CrudOperation, EndpointRegistrySchema, GeneratedEndpointSchema, MetadataEndpointsConfigSchema, RestApiConfigSchema, RestServerConfigSchema, RouteGenerationConfigSchema } from '@objectstack/spec/api'; +import type { BatchEndpointsConfig, CrudEndpointPattern, CrudEndpointsConfig, CrudOperation, EndpointRegistry, GeneratedEndpoint, MetadataEndpointsConfig, RestApiConfig, RestServerConfig, RouteGenerationConfig } from '@objectstack/spec/api'; // Validate data const result = BatchEndpointsConfigSchema.parse(data); @@ -59,20 +59,6 @@ const result = BatchEndpointsConfigSchema.parse(data); | **defaultAtomic** | `boolean` | ✅ | Default atomic/transaction mode for batch operations | ---- - -## Callback - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **name** | `string` | ✅ | Callback identifier (snake_case) | -| **expression** | `string` | ✅ | Runtime expression (e.g., `{$request.body#/callbackUrl}`) | -| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>` | ✅ | HTTP method for callback request | -| **url** | `string` | ✅ | Callback URL template with runtime expressions | - - --- ## CrudEndpointPattern @@ -159,36 +145,6 @@ const result = BatchEndpointsConfigSchema.parse(data); | **endpoints** | `{ types: boolean; items: boolean; item: boolean; schema: boolean }` | optional | Enable/disable specific endpoints | ---- - -## OpenApi31Extensions - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **webhooks** | `Record; payloadSchema: string; … }>` | optional | OpenAPI 3.1 webhooks (top-level webhook definitions) | -| **callbacks** | `Record; url: string }[]>` | optional | OpenAPI 3.1 callbacks (async response definitions) | -| **jsonSchemaDialect** | `string` | ✅ | JSON Schema dialect for schema definitions | -| **pathItemReferences** | `boolean` | ✅ | Allow $ref in path items (OpenAPI 3.1 feature) | - - ---- - -## OpenApiWebhookEvent - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **name** | `string` | ✅ | Webhook event identifier (snake_case) | -| **description** | `string` | ✅ | Human-readable event description | -| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>` | ✅ | HTTP method for webhook delivery | -| **payloadSchema** | `string` | ✅ | JSON Schema $ref for the webhook payload | -| **headers** | `Record` | optional | Custom headers to include in webhook delivery | -| **security** | `Enum<'hmac_sha256' \| 'basic' \| 'bearer' \| 'api_key'>[]` | ✅ | Supported authentication methods for webhook verification | - - --- ## RestApiConfig @@ -226,7 +182,7 @@ const result = BatchEndpointsConfigSchema.parse(data); | **metadata** | `{ prefix: string; enableCache: boolean; cacheTtl: integer; endpoints?: object }` | optional | Metadata endpoints configuration | | **batch** | `{ maxBatchSize: integer; enableBatchEndpoint: boolean; operations?: object; defaultAtomic: boolean }` | optional | Batch endpoints configuration | | **routes** | `{ includeObjects?: string[]; excludeObjects?: string[]; nameTransform: Enum<'none' \| 'plural' \| 'kebab-case' \| 'camelCase'>; overrides?: Record }> }` | optional | Route generation configuration | -| **openApi31** | `{ webhooks?: Record; payloadSchema: string; … }>; callbacks?: Record; url: string }[]>; jsonSchemaDialect: string; pathItemReferences: boolean }` | optional | OpenAPI 3.1 extensions configuration | +| **openApi31** | `any` | optional | [REMOVED] `RestServerConfig.openApi31` was removed in @objectstack/spec 17 (#4579, ADR-0049) — no runtime ever read it: the REST server forwards only `api`/`crud`/`metadata`/`batch`/`routes`, and the served /openapi.json is the pre-generated contract enriched with the live server URL and the registered objects, so webhook/callback definitions declared here never appeared in it. Delete the key. Config-driven OpenAPI 3.1 webhooks/callbacks documentation is a new capability and must arrive via the enforce route of ADR-0049 (a new ADR), not by re-declaring the key; for a real outbound webhook use `Webhook` from `@objectstack/spec/automation`. | --- diff --git a/docs/protocol-upgrade-guide.md b/docs/protocol-upgrade-guide.md index 0bbe5c889f..fa11352c61 100644 --- a/docs/protocol-upgrade-guide.md +++ b/docs/protocol-upgrade-guide.md @@ -178,6 +178,8 @@ The same enforce-or-remove pass reaches the event vocabulary: `DataEventType` dr The object capability block closes out the same ADR-0049 pass: `enable.trash` and `enable.mru` left the schema in the 16.x line (#3207, the #2377 close-out — every delete has always been a hard delete and MRU tracking was never implemented, so both default-true flags gated nothing), and the `.strict()` capabilities block rejects them with the prescription. This step registers the migration surface that removal was missing: stored 16.x rows replay clean instead of flagging `metadata_spec_invalid`, and `os migrate meta --from 16` rewrites authored sources. Soft delete stays parked at #3146; if built it returns as a live enforced flag rather than by reviving these keys. +The same enforce-or-remove pass retires the `RestServerConfig.openApi31` block (#4579): `OpenApi31ExtensionsSchema` (`webhooks` / `callbacks` / `jsonSchemaDialect` / `pathItemReferences`) with `OpenApiWebhookEventSchema` and `CallbackSchema` under it. Declared-but-unenforced end to end: the REST server's `normalizeConfig` forwards only `api`/`crud`/`metadata`/`batch`/`routes`, the served /openapi.json is the pre-generated contract enriched with the live server URL and registered objects, and `gen:openapi` never read a webhook or callback — so a definition authored under `openApi31.webhooks` never appeared in any served document, and zero import-level consumers existed across objectstack / cloud / objectui. `RestServerConfig` is plugin TS configuration (the REST plugin constructor / `plugin-hono-server` `restConfig`), never a stored metadata shape: the stack tree's own `api` block declares only its four scoping/auth knobs, so no `sys_metadata` row can carry `openApi31` and there is no source for the chain to rewrite — one semantic TODO for config authors rather than a stack conversion, the `validateOnly` shape. The key itself is tombstoned (the schema is not `.strict()`; a plain delete would strip it silently), and a config-driven webhooks/callbacks synthesis, if ever wanted, returns via the enforce route of ADR-0049 through a new ADR. + ### Mechanical (applied for you) | Conversion | Surface | Change | Load window | @@ -266,6 +268,9 @@ The object capability block closes out the same ADR-0049 pass: `enable.trash` an - **`data-engine-batch-retired`** — `contracts.IDataEngine.batch / data.DataEngineBatchRequestSchema` → `IObjectQLEngine.transaction(cb)` for in-process multi-write atomicity; the metadata protocol's `batchData` with `options.atomic: true` for a batch over one object; `POST {basePath}/batch` on the wire - Why not automatic: `batch?` was declared on `IDataEngine` for as long as that contract existed and was never implemented by any engine: `ObjectQL` has no `batch` method and there is no other engine in the tree. It also had no caller — `DataEngineRequest` was imported by exactly one file, the contract declaring the member. Its entire specification was a three-word doc comment ("Batch Operations (Transactional)"), which settles nothing about partial failure, ordering, cross-object references, rollback scope, or what `transaction: false` was supposed to mean — the questions a batch API exists to answer. Contrast its neighbours `getDefaultDriverName?` / `getDriverByName?`, whose optionality is evidenced: each names its implementer and its probing caller. The tell that nobody ever designed against it is in the schema: `DataEngineBatchRequestSchema.requests` nested the request union RECURSIVELY, so a batch could contain batches, with no statement anywhere about what that meant for ordering or rollback. The only test was a type pin — an ad-hoc object literal carrying a `batch` property, asserting the property was defined — which could not fail while the declaration existed and would have passed unchanged for the member's whole life with no engine implementing it. What it claimed is now covered by members that are real, so the removal deletes a false affordance rather than a capability: ADR-0119 D1 made `transaction` reachable through the contract and D4 made `batchData`'s `atomic` honest, while the wire batch has always validated with `CrossObjectBatchRequestSchema` / `BatchUpdateRequestSchema` from `api/batch.zod.ts` — a different schema entirely, untouched here. TS/API surfaces only: an engine is CODE, never stack metadata, so there is no source for the chain to rewrite. Deliberately no schema tombstone either — nothing ever parsed `DataEngineBatchRequestSchema`, so a `retiredKey()` prescription would have no one to reach; its three `authorable-surface.json` baseline lines and its `json-schema.manifest.json` entry are dropped in the same change, deliberately. The enforced channel is tsc. ADR-0049 / ADR-0078, #4618. - Done when: No code calls `engine.batch(...)` and no type references `DataEngineBatchRequest`; in-process multi-write atomicity goes through `IObjectQLEngine.transaction(cb)`, a batch over one object through `batchData` with `options.atomic: true`, and a cross-object batch over the wire through `POST {basePath}/batch`. Because no engine implemented the member, an implementation left behind still compiles and is simply never reached; a CALLER of it no longer type-checks — and there were none. +- **`rest-server-openapi31-block-removed`** — `restServer.openApi31` → (removed — no replacement key exists. Delete the key; for a real outbound webhook use `Webhook` from `@objectstack/spec/automation`. Config-driven OpenAPI 3.1 webhooks/callbacks documentation returns, if ever, via the enforce route of ADR-0049 through a new ADR) + - Why not automatic: The `openApi31` block (`webhooks` / `callbacks` / `jsonSchemaDialect` / `pathItemReferences`, typed by `OpenApi31ExtensionsSchema` with `OpenApiWebhookEventSchema` and `CallbackSchema` under it) promised OpenAPI 3.1 document synthesis nothing delivered: the REST server's `normalizeConfig` forwards only `api`/`crud`/`metadata`/`batch`/`routes`, and the served /openapi.json is the pre-generated @objectstack/spec contract enriched with the live server URL and the registered objects — a webhook declared here never appeared in any served document (ADR-0049; the #3197 connector-webhook shape one layer up). There is no behaviour to preserve and nothing stored to rewrite: `RestServerConfig` is plugin TS configuration (REST plugin constructor / `plugin-hono-server` `restConfig`), never a `sys_metadata` shape — the stack tree's `api` block declares only its four scoping/auth knobs. The three schemas are removed with the key (zero import-level consumers in objectstack / cloud / objectui); the key itself is tombstoned because the schema is not `.strict()` and a plain delete would strip it silently. #4579. + - Done when: No `RestServerConfig` value passed to the REST plugin (or `plugin-hono-server` `restConfig`) carries `openApi31` — a config that includes it now fails the parse with the retirement prescription instead of being silently stripped. No code imports `OpenApi31Extensions(Schema)`, `Callback(Schema)` or `OpenApiWebhookEvent(Schema)` from `@objectstack/spec/api` (TS2305 after upgrade). The served /openapi.json is byte-identical before and after — the block never reached it. --- diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index 2e60e0b407..56fc9af45f 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -2455,8 +2455,6 @@ "CacheInvalidationResponse (type)", "CacheInvalidationResponseSchema (const)", "CacheInvalidationTarget (type)", - "Callback (type)", - "CallbackSchema (const)", "CheckPermissionRequest (type)", "CheckPermissionRequestSchema (const)", "CheckPermissionResponse (type)", @@ -2910,8 +2908,6 @@ "ObjectDefinitionResponseSchema (const)", "ObjectQLReference (type)", "ObjectQLReferenceSchema (const)", - "OpenApi31Extensions (type)", - "OpenApi31ExtensionsSchema (const)", "OpenApiGenerationConfig (type)", "OpenApiGenerationConfigInput (type)", "OpenApiGenerationConfigSchema (const)", @@ -2921,8 +2917,6 @@ "OpenApiServerSchema (const)", "OpenApiSpec (type)", "OpenApiSpecSchema (const)", - "OpenApiWebhookEvent (type)", - "OpenApiWebhookEventSchema (const)", "OperatorMapping (type)", "OperatorMappingSchema (const)", "PackageApiContracts (const)", diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json index 1f5bc9171b..11a3986f87 100644 --- a/packages/spec/authorable-surface.json +++ b/packages/spec/authorable-surface.json @@ -727,10 +727,6 @@ "api/CacheInvalidationResponse:invalidated", "api/CacheInvalidationResponse:success", "api/CacheInvalidationResponse:targets", - "api/Callback:expression", - "api/Callback:method", - "api/Callback:name", - "api/Callback:url", "api/CheckPermissionRequest:action", "api/CheckPermissionRequest:field", "api/CheckPermissionRequest:object", @@ -1594,10 +1590,6 @@ "api/ObjectQLReference:includeFields", "api/ObjectQLReference:includeRelated", "api/ObjectQLReference:objectId", - "api/OpenApi31Extensions:callbacks", - "api/OpenApi31Extensions:jsonSchemaDialect", - "api/OpenApi31Extensions:pathItemReferences", - "api/OpenApi31Extensions:webhooks", "api/OpenApiGenerationConfig:apiVersion", "api/OpenApiGenerationConfig:contact", "api/OpenApiGenerationConfig:description", @@ -1632,12 +1624,6 @@ "api/OpenApiSpec:security", "api/OpenApiSpec:servers", "api/OpenApiSpec:tags", - "api/OpenApiWebhookEvent:description", - "api/OpenApiWebhookEvent:headers", - "api/OpenApiWebhookEvent:method", - "api/OpenApiWebhookEvent:name", - "api/OpenApiWebhookEvent:payloadSchema", - "api/OpenApiWebhookEvent:security", "api/OperatorMapping:odata", "api/OperatorMapping:operator", "api/OperatorMapping:rest", @@ -1832,7 +1818,7 @@ "api/RestServerConfig:batch", "api/RestServerConfig:crud", "api/RestServerConfig:metadata", - "api/RestServerConfig:openApi31", + "api/RestServerConfig:openApi31 [RETIRED]", "api/RestServerConfig:routes", "api/RouteCoverageEntry:category", "api/RouteCoverageEntry:handlerStatus", diff --git a/packages/spec/json-schema.manifest.json b/packages/spec/json-schema.manifest.json index 774c112295..39de0cac2b 100644 --- a/packages/spec/json-schema.manifest.json +++ b/packages/spec/json-schema.manifest.json @@ -139,7 +139,6 @@ "api/CacheInvalidationRequest", "api/CacheInvalidationResponse", "api/CacheInvalidationTarget", - "api/Callback", "api/CheckPermissionRequest", "api/CheckPermissionResponse", "api/CodeGenerationTemplate", @@ -363,12 +362,10 @@ "api/ODataResponse", "api/ObjectDefinitionResponse", "api/ObjectQLReference", - "api/OpenApi31Extensions", "api/OpenApiGenerationConfig", "api/OpenApiSecurityScheme", "api/OpenApiServer", "api/OpenApiSpec", - "api/OpenApiWebhookEvent", "api/OperatorMapping", "api/PackageApiErrorCode", "api/PackageInstallRequest", diff --git a/packages/spec/spec-changes.json b/packages/spec/spec-changes.json index de368e728e..b702f677c3 100644 --- a/packages/spec/spec-changes.json +++ b/packages/spec/spec-changes.json @@ -460,6 +460,13 @@ "migrationId": "data-engine-batch-retired", "toMajor": 17, "rationale": "`batch?` was declared on `IDataEngine` for as long as that contract existed and was never implemented by any engine: `ObjectQL` has no `batch` method and there is no other engine in the tree. It also had no caller — `DataEngineRequest` was imported by exactly one file, the contract declaring the member. Its entire specification was a three-word doc comment (\"Batch Operations (Transactional)\"), which settles nothing about partial failure, ordering, cross-object references, rollback scope, or what `transaction: false` was supposed to mean — the questions a batch API exists to answer. Contrast its neighbours `getDefaultDriverName?` / `getDriverByName?`, whose optionality is evidenced: each names its implementer and its probing caller. The tell that nobody ever designed against it is in the schema: `DataEngineBatchRequestSchema.requests` nested the request union RECURSIVELY, so a batch could contain batches, with no statement anywhere about what that meant for ordering or rollback. The only test was a type pin — an ad-hoc object literal carrying a `batch` property, asserting the property was defined — which could not fail while the declaration existed and would have passed unchanged for the member's whole life with no engine implementing it. What it claimed is now covered by members that are real, so the removal deletes a false affordance rather than a capability: ADR-0119 D1 made `transaction` reachable through the contract and D4 made `batchData`'s `atomic` honest, while the wire batch has always validated with `CrossObjectBatchRequestSchema` / `BatchUpdateRequestSchema` from `api/batch.zod.ts` — a different schema entirely, untouched here. TS/API surfaces only: an engine is CODE, never stack metadata, so there is no source for the chain to rewrite. Deliberately no schema tombstone either — nothing ever parsed `DataEngineBatchRequestSchema`, so a `retiredKey()` prescription would have no one to reach; its three `authorable-surface.json` baseline lines and its `json-schema.manifest.json` entry are dropped in the same change, deliberately. The enforced channel is tsc. ADR-0049 / ADR-0078, #4618." + }, + { + "surface": "restServer.openApi31", + "replacement": "(removed — no replacement key exists. Delete the key; for a real outbound webhook use `Webhook` from `@objectstack/spec/automation`. Config-driven OpenAPI 3.1 webhooks/callbacks documentation returns, if ever, via the enforce route of ADR-0049 through a new ADR)", + "migrationId": "rest-server-openapi31-block-removed", + "toMajor": 17, + "rationale": "The `openApi31` block (`webhooks` / `callbacks` / `jsonSchemaDialect` / `pathItemReferences`, typed by `OpenApi31ExtensionsSchema` with `OpenApiWebhookEventSchema` and `CallbackSchema` under it) promised OpenAPI 3.1 document synthesis nothing delivered: the REST server's `normalizeConfig` forwards only `api`/`crud`/`metadata`/`batch`/`routes`, and the served /openapi.json is the pre-generated @objectstack/spec contract enriched with the live server URL and the registered objects — a webhook declared here never appeared in any served document (ADR-0049; the #3197 connector-webhook shape one layer up). There is no behaviour to preserve and nothing stored to rewrite: `RestServerConfig` is plugin TS configuration (REST plugin constructor / `plugin-hono-server` `restConfig`), never a `sys_metadata` shape — the stack tree's `api` block declares only its four scoping/auth knobs. The three schemas are removed with the key (zero import-level consumers in objectstack / cloud / objectui); the key itself is tombstoned because the schema is not `.strict()` and a plain delete would strip it silently. #4579." } ], "removed": [] @@ -979,6 +986,13 @@ "migrationId": "data-engine-batch-retired", "toMajor": 17, "rationale": "`batch?` was declared on `IDataEngine` for as long as that contract existed and was never implemented by any engine: `ObjectQL` has no `batch` method and there is no other engine in the tree. It also had no caller — `DataEngineRequest` was imported by exactly one file, the contract declaring the member. Its entire specification was a three-word doc comment (\"Batch Operations (Transactional)\"), which settles nothing about partial failure, ordering, cross-object references, rollback scope, or what `transaction: false` was supposed to mean — the questions a batch API exists to answer. Contrast its neighbours `getDefaultDriverName?` / `getDriverByName?`, whose optionality is evidenced: each names its implementer and its probing caller. The tell that nobody ever designed against it is in the schema: `DataEngineBatchRequestSchema.requests` nested the request union RECURSIVELY, so a batch could contain batches, with no statement anywhere about what that meant for ordering or rollback. The only test was a type pin — an ad-hoc object literal carrying a `batch` property, asserting the property was defined — which could not fail while the declaration existed and would have passed unchanged for the member's whole life with no engine implementing it. What it claimed is now covered by members that are real, so the removal deletes a false affordance rather than a capability: ADR-0119 D1 made `transaction` reachable through the contract and D4 made `batchData`'s `atomic` honest, while the wire batch has always validated with `CrossObjectBatchRequestSchema` / `BatchUpdateRequestSchema` from `api/batch.zod.ts` — a different schema entirely, untouched here. TS/API surfaces only: an engine is CODE, never stack metadata, so there is no source for the chain to rewrite. Deliberately no schema tombstone either — nothing ever parsed `DataEngineBatchRequestSchema`, so a `retiredKey()` prescription would have no one to reach; its three `authorable-surface.json` baseline lines and its `json-schema.manifest.json` entry are dropped in the same change, deliberately. The enforced channel is tsc. ADR-0049 / ADR-0078, #4618." + }, + { + "surface": "restServer.openApi31", + "replacement": "(removed — no replacement key exists. Delete the key; for a real outbound webhook use `Webhook` from `@objectstack/spec/automation`. Config-driven OpenAPI 3.1 webhooks/callbacks documentation returns, if ever, via the enforce route of ADR-0049 through a new ADR)", + "migrationId": "rest-server-openapi31-block-removed", + "toMajor": 17, + "rationale": "The `openApi31` block (`webhooks` / `callbacks` / `jsonSchemaDialect` / `pathItemReferences`, typed by `OpenApi31ExtensionsSchema` with `OpenApiWebhookEventSchema` and `CallbackSchema` under it) promised OpenAPI 3.1 document synthesis nothing delivered: the REST server's `normalizeConfig` forwards only `api`/`crud`/`metadata`/`batch`/`routes`, and the served /openapi.json is the pre-generated @objectstack/spec contract enriched with the live server URL and the registered objects — a webhook declared here never appeared in any served document (ADR-0049; the #3197 connector-webhook shape one layer up). There is no behaviour to preserve and nothing stored to rewrite: `RestServerConfig` is plugin TS configuration (REST plugin constructor / `plugin-hono-server` `restConfig`), never a `sys_metadata` shape — the stack tree's `api` block declares only its four scoping/auth knobs. The three schemas are removed with the key (zero import-level consumers in objectstack / cloud / objectui); the key itself is tombstoned because the schema is not `.strict()` and a plain delete would strip it silently. #4579." } ], "removed": [] diff --git a/packages/spec/src/api/rest-server.test.ts b/packages/spec/src/api/rest-server.test.ts index 4c383b5486..b7aa8221c5 100644 --- a/packages/spec/src/api/rest-server.test.ts +++ b/packages/spec/src/api/rest-server.test.ts @@ -12,9 +12,6 @@ import { EndpointRegistrySchema, RestApiConfig, RestServerConfig, - OpenApiWebhookEventSchema, - CallbackSchema, - OpenApi31ExtensionsSchema, type RestApiConfig as RestApiConfigType, type RestServerConfig as RestServerConfigType, } from './rest-server.zod'; @@ -652,156 +649,138 @@ describe('Integration Tests', () => { }); // ========================================== -// OpenAPI 3.1 Webhooks & Callbacks Tests +// OpenAPI 3.1 Webhooks & Callbacks — retired (#4579) // ========================================== +// +// `OpenApiWebhookEventSchema` / `CallbackSchema` / `OpenApi31ExtensionsSchema` +// tests were removed with the schemas (#4579, ADR-0049 enforce-or-remove). +// The retirement itself is pinned below. + +describe('[#4579] `RestServerConfig.openApi31` retirement', () => { + it('REJECTS an authored openApi31 block, with the fix in the message', () => { + // Tombstoned, not deleted: RestServerConfigSchema is not `.strict()`, so a + // plain deletion would silently strip the key — the author's webhook + // declarations would vanish without a word, which is the exact + // declared ≠ enforced failure the removal closes. + expect(() => + RestServerConfigSchema.parse({ + openApi31: { + webhooks: { + test_hook: { + name: 'test_hook', + description: 'Test webhook', + payloadSchema: '#/ref', + security: ['basic'], + }, + }, + pathItemReferences: true, + }, + }), + ).toThrow(/RestServerConfig\.openApi31.*removed.*Delete the key/s); + }); -describe('OpenApiWebhookEventSchema', () => { - it('should accept valid webhook event', () => { - const event = OpenApiWebhookEventSchema.parse({ - name: 'record_created', - description: 'Fired when a record is created', - payloadSchema: '#/components/schemas/RecordCreated', - security: ['hmac_sha256'], + it('still parses every live key cleanly — the tombstone rejects one key, not the config', () => { + const parsed = RestServerConfigSchema.parse({ + api: { version: 'v1', basePath: '/api' }, + crud: { dataPrefix: '/data' }, + metadata: { prefix: '/meta' }, + batch: { maxBatchSize: 200 }, + routes: { excludeObjects: ['system_log'] }, }); - - expect(event.name).toBe('record_created'); - expect(event.method).toBe('POST'); - expect(event.security).toContain('hmac_sha256'); + expect(parsed.api?.version).toBe('v1'); + expect(parsed.crud?.dataPrefix).toBe('/data'); + expect(parsed).not.toHaveProperty('openApi31'); }); +}); - it('should enforce snake_case name', () => { - expect(() => OpenApiWebhookEventSchema.parse({ - name: 'RecordCreated', - description: 'Bad name', - payloadSchema: '#/ref', - security: ['basic'], - })).toThrow(); - }); +// #4642 established that a compile-time conditional-type pin in this package is +// a no-op (tsconfig excludes `**/*.test.ts`; vitest never enables `typecheck`), +// so the load-bearing pin is the compiler-API test below, with anti-vacuity +// guards (a resolution failure would otherwise make every assertion pass +// vacuously); sabotage-verified in the PR (re-adding an export turns it red). +describe('[#4579] the OpenApi31 block schemas are not exported from any entry point', () => { + const REMOVED_NAMES = [ + 'OpenApi31ExtensionsSchema', + 'OpenApi31Extensions', + 'CallbackSchema', + 'Callback', + 'OpenApiWebhookEventSchema', + 'OpenApiWebhookEvent', + ] as const; + + it('resolves the export surface: no public entry names any of the six', async () => { + const ts = (await import('typescript')).default; + const { resolve, dirname } = await import('node:path'); + const { fileURLToPath } = await import('node:url'); + const { readFileSync } = await import('node:fs'); + + const specDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); + // Every public entry point, read from package.json's exports map so a + // future entry cannot silently escape the pin below. + const pkg = JSON.parse(readFileSync(resolve(specDir, 'package.json'), 'utf8')) as { + exports: Record; + }; + const entries: Record = {}; + for (const sub of Object.keys(pkg.exports)) { + if (sub === '.') entries[sub] = resolve(specDir, 'src/index.ts'); + else if (/^\.\/[a-z-]+$/.test(sub)) entries[sub] = resolve(specDir, `src/${sub.slice(2)}/index.ts`); + // './openapi.json' / './package.json' are not TypeScript entry points. + } + // Anti-vacuity: the enumeration must have found the real surface. + expect(Object.keys(entries)).toContain('./api'); + expect(Object.keys(entries).length).toBeGreaterThan(10); + + const program = ts.createProgram(Object.values(entries), { + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.Bundler, + skipLibCheck: true, + noEmit: true, + }); + const checker = program.getTypeChecker(); + const exportsOf = (sub: string) => { + const sf = program.getSourceFile(entries[sub]); + const moduleSym = sf && checker.getSymbolAtLocation(sf); + // Without this guard a resolution failure would make every assertion + // below pass vacuously — the exact way a gate goes dormant (#4642). + expect(moduleSym, `${sub} module symbol must resolve`).toBeTruthy(); + return checker.getExportsOfModule(moduleSym!); + }; - it('should accept event with custom headers', () => { - const event = OpenApiWebhookEventSchema.parse({ - name: 'sync_completed', - description: 'Sync finished', - payloadSchema: '#/ref', - security: ['bearer'], - headers: { 'X-Custom': 'value' }, - }); + // The surface stays non-trivial (the `not.toContain` cannot pass by + // resolving nothing) and the surviving neighbours stand. + const apiNames = exportsOf('./api').map((e) => e.getName()); + expect(apiNames.length, './api must export a non-trivial surface').toBeGreaterThan(50); + expect(apiNames).toContain('RestServerConfigSchema'); + expect(apiNames).toContain('RestApiConfigSchema'); - expect(event.headers?.['X-Custom']).toBe('value'); + for (const sub of Object.keys(entries)) { + const names = exportsOf(sub).map((e) => e.getName()); + for (const removed of REMOVED_NAMES) { + expect(names, `${sub} must not export ${removed} (#4579)`).not.toContain(removed); + } + } }); - it('should accept all security methods', () => { - const methods = ['hmac_sha256', 'basic', 'bearer', 'api_key'] as const; - const event = OpenApiWebhookEventSchema.parse({ - name: 'test_event', - description: 'Test', - payloadSchema: '#/ref', - security: [...methods], - }); - expect(event.security).toHaveLength(4); + it('keeps the runtime namespace consistent with the compiler view', async () => { + const api = await import('./index'); + for (const removed of REMOVED_NAMES) { + expect(removed in api, `./api runtime namespace must not carry ${removed}`).toBe(false); + } + // What survives is the live config surface, not the dead documentation one. + expect('RestServerConfigSchema' in api).toBe(true); }); // v17 dual-source cleanup (#4572): the bare names WebhookEvent(Schema) / - // WebhookConfig(Schema) now belong to @objectstack/spec/integration alone + // WebhookConfig(Schema) belong to @objectstack/spec/integration alone // (connector event enum + connector webhook config). The ./api pair was the // #4411-style trap: same names, different concepts, different forms // (z.object here vs z.enum there). WebhookConfig(Schema) on ./api was dead - // — wired into nothing (not even RestServerConfigSchema) — and was removed - // rather than renamed. Pin: this module no longer declares the bare names. - // The pin is compile-time (typeof import is type-level only — no runtime - // barrel load): if either bare name is re-added here, the conditional type - // flips to `true` and the `false` assignment fails `tsc --noEmit`. - it('does not re-expose the bare WebhookEvent/WebhookConfig names from ./api', () => { - type RestServerModule = typeof import('./rest-server.zod'); - const hasBareEventSchema: 'WebhookEventSchema' extends keyof RestServerModule - ? true - : false = false; - const hasBareConfigSchema: 'WebhookConfigSchema' extends keyof RestServerModule - ? true - : false = false; - expect(hasBareEventSchema).toBe(false); - expect(hasBareConfigSchema).toBe(false); - }); -}); - -describe('CallbackSchema', () => { - it('should accept valid callback', () => { - const cb = CallbackSchema.parse({ - name: 'payment_completed', - expression: '{$request.body#/callbackUrl}', - method: 'POST', - url: '{$request.body#/callbackUrl}', - }); - - expect(cb.name).toBe('payment_completed'); - expect(cb.method).toBe('POST'); - }); - - it('should enforce snake_case name', () => { - expect(() => CallbackSchema.parse({ - name: 'PaymentCompleted', - expression: '{$request.body#/url}', - method: 'POST', - url: 'https://example.com', - })).toThrow(); - }); -}); - -describe('OpenApi31ExtensionsSchema', () => { - it('should accept empty config with defaults', () => { - const ext = OpenApi31ExtensionsSchema.parse({}); - - expect(ext.jsonSchemaDialect).toBe('https://json-schema.org/draft/2020-12/schema'); - expect(ext.pathItemReferences).toBe(false); - }); - - it('should accept webhooks map', () => { - const ext = OpenApi31ExtensionsSchema.parse({ - webhooks: { - record_created: { - name: 'record_created', - description: 'Record created', - payloadSchema: '#/ref', - security: ['hmac_sha256'], - }, - }, - }); - - expect(ext.webhooks?.record_created).toBeDefined(); - }); - - it('should accept callbacks map', () => { - const ext = OpenApi31ExtensionsSchema.parse({ - callbacks: { - onComplete: [ - { - name: 'on_complete', - expression: '{$request.body#/callbackUrl}', - method: 'POST', - url: '{$request.body#/callbackUrl}', - }, - ], - }, - }); - - expect(ext.callbacks?.onComplete).toHaveLength(1); - }); - - it('should accept RestServerConfig with openApi31', () => { - const config = RestServerConfigSchema.parse({ - openApi31: { - webhooks: { - test_hook: { - name: 'test_hook', - description: 'Test webhook', - payloadSchema: '#/ref', - security: ['basic'], - }, - }, - pathItemReferences: true, - }, - }); - - expect(config.openApi31?.pathItemReferences).toBe(true); + // and removed; WebhookEvent(Schema) was first renamed OpenApiWebhookEvent(Schema) + // (#4572) and then removed outright with the openApi31 block (#4579). + // Pin: this module declares neither the bare names nor the renamed ones. + it('does not re-expose the bare WebhookEvent/WebhookConfig names from ./api (#4572)', async () => { + const restServer = await import('./rest-server.zod'); + expect('WebhookEventSchema' in restServer).toBe(false); + expect('WebhookConfigSchema' in restServer).toBe(false); }); }); diff --git a/packages/spec/src/api/rest-server.zod.ts b/packages/spec/src/api/rest-server.zod.ts index 510fbca41a..ac3c882787 100644 --- a/packages/spec/src/api/rest-server.zod.ts +++ b/packages/spec/src/api/rest-server.zod.ts @@ -395,61 +395,27 @@ export type RouteGenerationConfig = z.infer; export type RouteGenerationConfigInput = z.input; // ========================================== -// OpenAPI 3.1 Webhooks & Callbacks +// OpenAPI 3.1 Webhooks & Callbacks — REMOVED (#4579) // ========================================== - -/** - * OpenAPI Webhook Event Schema - * Defines a webhook event as declared in an OpenAPI 3.1 document's top-level - * `webhooks` section — an API-documentation descriptor, NOT a runtime - * subscription. (Renamed from `WebhookEventSchema` in v17 — that bare name - * collided with the connector event enum in `@objectstack/spec/integration`, - * a different concept AND a different form: z.object here vs z.enum there. - * The outbound webhook an author configures is `WebhookSchema` in - * `@objectstack/spec/automation`.) - */ -export const OpenApiWebhookEventSchema = lazySchema(() => z.object({ - name: z.string().regex(/^[a-z_][a-z0-9_]*$/).describe('Webhook event identifier (snake_case)'), - description: z.string().describe('Human-readable event description'), - method: HttpMethod.default('POST').describe('HTTP method for webhook delivery'), - payloadSchema: z.string().describe('JSON Schema $ref for the webhook payload'), - headers: z.record(z.string(), z.string()).optional().describe('Custom headers to include in webhook delivery'), - security: z.array( - z.enum(['hmac_sha256', 'basic', 'bearer', 'api_key']) - ).describe('Supported authentication methods for webhook verification'), -})); - -export type OpenApiWebhookEvent = z.infer; - -/** - * Callback Schema - * OpenAPI 3.1 callback definition for asynchronous API responses - */ -export const CallbackSchema = lazySchema(() => z.object({ - name: z.string().regex(/^[a-z_][a-z0-9_]*$/).describe('Callback identifier (snake_case)'), - expression: z.string().describe('Runtime expression (e.g., {$request.body#/callbackUrl})'), - method: HttpMethod.describe('HTTP method for callback request'), - url: z.string().describe('Callback URL template with runtime expressions'), -})); - -export type Callback = z.infer; - -/** - * OpenAPI 3.1 Extensions Schema - * Extensions specific to OpenAPI 3.1 specification - */ -export const OpenApi31ExtensionsSchema = lazySchema(() => z.object({ - webhooks: z.record(z.string(), OpenApiWebhookEventSchema).optional() - .describe('OpenAPI 3.1 webhooks (top-level webhook definitions)'), - callbacks: z.record(z.string(), z.array(CallbackSchema)).optional() - .describe('OpenAPI 3.1 callbacks (async response definitions)'), - jsonSchemaDialect: z.string().default('https://json-schema.org/draft/2020-12/schema') - .describe('JSON Schema dialect for schema definitions'), - pathItemReferences: z.boolean().default(false) - .describe('Allow $ref in path items (OpenAPI 3.1 feature)'), -})); - -export type OpenApi31Extensions = z.infer; +// +// `OpenApiWebhookEventSchema`, `CallbackSchema` and `OpenApi31ExtensionsSchema` +// — the `RestServerConfig.openApi31` block — were removed in v17 (#4579, +// ADR-0049 enforce-or-remove). The block was declared-but-unenforced end to +// end: `normalizeConfig` (packages/rest/src/rest-server.ts) forwards only +// `api` / `crud` / `metadata` / `batch` / `routes`; the served +// `GET /openapi.json` is the pre-generated @objectstack/spec contract enriched +// at request time with the live server URL + the runtime-registered objects; +// and `gen:openapi` (scripts/build-openapi.ts) never read a webhook or +// callback. So a definition authored under `openApi31.webhooks` never appeared +// in any served document — false compliance, not a capability. Zero +// import-level consumers across objectstack / cloud / objectui (three-repo +// scan, #4579). Config-driven OpenAPI 3.1 webhooks/callbacks documentation is +// a NEW capability: if it is ever needed it returns via the enforce route of +// ADR-0049, through an ADR — not by re-declaring inert keys. The live OpenAPI +// switch is `RestApiConfigSchema.enableOpenApi` above; the outbound webhook an +// author actually configures is `WebhookSchema` in `@objectstack/spec/automation`. +// The tombstone on `RestServerConfigSchema.openApi31` below carries the +// author-facing prescription. // ========================================== // Complete REST Server Configuration @@ -510,9 +476,23 @@ export const RestServerConfigSchema = lazySchema(() => z.object({ routes: RouteGenerationConfigSchema.optional().describe('Route generation configuration'), /** - * OpenAPI 3.1 extensions (webhooks, callbacks) - */ - openApi31: OpenApi31ExtensionsSchema.optional().describe('OpenAPI 3.1 extensions configuration'), + * [REMOVED in #4579] The OpenAPI 3.1 extensions block (`webhooks` / + * `callbacks` / `jsonSchemaDialect` / `pathItemReferences`). Tombstoned + * rather than deleted: this schema is not `.strict()`, so a plain deletion + * would silently strip the key — an author who keeps declaring webhooks here + * would get a clean parse and a served /openapi.json that never mentions + * them, which is the exact declared ≠ enforced failure the removal closes + * (see the section comment above). + */ + openApi31: retiredKey( + '`RestServerConfig.openApi31` was removed in @objectstack/spec 17 (#4579, ADR-0049) — no ' + + 'runtime ever read it: the REST server forwards only `api`/`crud`/`metadata`/`batch`/`routes`, ' + + 'and the served /openapi.json is the pre-generated contract enriched with the live server URL ' + + 'and the registered objects, so webhook/callback definitions declared here never appeared in ' + + 'it. Delete the key. Config-driven OpenAPI 3.1 webhooks/callbacks documentation is a new ' + + 'capability and must arrive via the enforce route of ADR-0049 (a new ADR), not by re-declaring ' + + 'the key; for a real outbound webhook use `Webhook` from `@objectstack/spec/automation`.', + ), })); export type RestServerConfig = z.infer; diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 8f3cb7c7c5..398f457c8a 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -702,7 +702,23 @@ const step17: MigrationStep = { + 'with the prescription. This step registers the migration surface that removal was ' + 'missing: stored 16.x rows replay clean instead of flagging `metadata_spec_invalid`, ' + 'and `os migrate meta --from 16` rewrites authored sources. Soft delete stays parked at ' - + '#3146; if built it returns as a live enforced flag rather than by reviving these keys.', + + '#3146; if built it returns as a live enforced flag rather than by reviving these keys.\n\n' + + 'The same enforce-or-remove pass retires the `RestServerConfig.openApi31` block (#4579): ' + + '`OpenApi31ExtensionsSchema` (`webhooks` / `callbacks` / `jsonSchemaDialect` / ' + + '`pathItemReferences`) with `OpenApiWebhookEventSchema` and `CallbackSchema` under it. ' + + "Declared-but-unenforced end to end: the REST server's `normalizeConfig` forwards only " + + '`api`/`crud`/`metadata`/`batch`/`routes`, the served /openapi.json is the pre-generated ' + + 'contract enriched with the live server URL and registered objects, and `gen:openapi` ' + + 'never read a webhook or callback — so a definition authored under `openApi31.webhooks` ' + + 'never appeared in any served document, and zero import-level consumers existed across ' + + 'objectstack / cloud / objectui. `RestServerConfig` is plugin TS configuration (the REST ' + + 'plugin constructor / `plugin-hono-server` `restConfig`), never a stored metadata shape: ' + + "the stack tree's own `api` block declares only its four scoping/auth knobs, so no " + + '`sys_metadata` row can carry `openApi31` and there is no source for the chain to ' + + 'rewrite — one semantic TODO for config authors rather than a stack conversion, the ' + + '`validateOnly` shape. The key itself is tombstoned (the schema is not `.strict()`; a ' + + 'plain delete would strip it silently), and a config-driven webhooks/callbacks synthesis, ' + + 'if ever wanted, returns via the enforce route of ADR-0049 through a new ADR.', conversionIds: [ 'action-execute-to-target', 'field-conditionalRequired-to-requiredWhen', @@ -1089,6 +1105,38 @@ const step17: MigrationStep = { + 'implemented the member, an implementation left behind still compiles and is simply ' + 'never reached; a CALLER of it no longer type-checks — and there were none.', }, + { + id: 'rest-server-openapi31-block-removed', + surface: 'restServer.openApi31', + replacement: + '(removed — no replacement key exists. Delete the key; for a real outbound webhook use ' + + '`Webhook` from `@objectstack/spec/automation`. Config-driven OpenAPI 3.1 ' + + 'webhooks/callbacks documentation returns, if ever, via the enforce route of ADR-0049 ' + + 'through a new ADR)', + reason: + 'The `openApi31` block (`webhooks` / `callbacks` / `jsonSchemaDialect` / ' + + '`pathItemReferences`, typed by `OpenApi31ExtensionsSchema` with ' + + '`OpenApiWebhookEventSchema` and `CallbackSchema` under it) promised OpenAPI 3.1 ' + + "document synthesis nothing delivered: the REST server's `normalizeConfig` forwards " + + 'only `api`/`crud`/`metadata`/`batch`/`routes`, and the served /openapi.json is the ' + + 'pre-generated @objectstack/spec contract enriched with the live server URL and the ' + + 'registered objects — a webhook declared here never appeared in any served document ' + + '(ADR-0049; the #3197 connector-webhook shape one layer up). There is no behaviour ' + + 'to preserve and nothing stored to rewrite: `RestServerConfig` is plugin TS ' + + 'configuration (REST plugin constructor / `plugin-hono-server` `restConfig`), never ' + + "a `sys_metadata` shape — the stack tree's `api` block declares only its four " + + 'scoping/auth knobs. The three schemas are removed with the key (zero import-level ' + + 'consumers in objectstack / cloud / objectui); the key itself is tombstoned because ' + + 'the schema is not `.strict()` and a plain delete would strip it silently. #4579.', + acceptanceCriteria: + 'No `RestServerConfig` value passed to the REST plugin (or `plugin-hono-server` ' + + '`restConfig`) carries `openApi31` — a config that includes it now fails the parse ' + + 'with the retirement prescription instead of being silently stripped. No code ' + + 'imports `OpenApi31Extensions(Schema)`, `Callback(Schema)` or ' + + '`OpenApiWebhookEvent(Schema)` from `@objectstack/spec/api` (TS2305 after upgrade). ' + + 'The served /openapi.json is byte-identical before and after — the block never ' + + 'reached it.', + }, ], };