From b2fe6edc3f0825316a4a1f873b6cb4b9b6ccff77 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 02:16:02 +0000 Subject: [PATCH 1/2] docs(spec): name only the OpenAPI document where the retired docs viewer was named Claude-Session: https://claude.ai/code/session_01B2EeooonSXrANpkpgUSfFZ Co-authored-by: Claude --- packages/spec/liveness/rest_api.json | 4 ++-- .../spec/scripts/lib/openapi-self-consistency.ts | 15 +++++++-------- packages/spec/src/api/rest-server.zod.ts | 10 ++++------ .../18.rest-api-config-dead-keys-retired.ts | 4 ++-- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/packages/spec/liveness/rest_api.json b/packages/spec/liveness/rest_api.json index 0dd439637c0..167bf7aa491 100644 --- a/packages/spec/liveness/rest_api.json +++ b/packages/spec/liveness/rest_api.json @@ -68,9 +68,9 @@ }, "enableOpenApi": { "status": "live", - "verifiedAt": "2026-09-21", + "verifiedAt": "2026-10-22", "evidenceScope": "in-repo", - "evidence": "packages/rest/src/rest-server.ts#registerRoutes (`if (this.config.api.enableOpenApi)` gates registerOpenApiEndpoints, which mounts both `/openapi.json` and the `/docs` viewer)", + "evidence": "packages/rest/src/rest-server.ts#registerRoutes (`if (this.config.api.enableOpenApi)` gates registerOpenApiEndpoints, which mounts `/openapi.json`, the OpenAPI document)", "producer": "packages/rest/src/rest-server.ts#normalizeConfig (parses `config.api` through `parseDeclaredApiConfig` and lists the key into `this.config.api`, the object every consumer below reads; the parsed schema's own `.default()`s supply the value when the host omits the key)", "note": "One consumer, and it is the mount. Worth reading beside `documentation.*` below: this flag decides WHETHER the OpenAPI document is served at all, and it is the only key on this block that the served document's existence depends on — nothing on this block decides what the document SAYS. REACHABILITY (#15543, standing reading): embedder-only. A RestServerConfig is the ARGUMENT a host passes when it constructs the server, and the one door is programmatic — createRestApiPlugin({ api }) (packages/rest/src/rest-api-plugin.ts). No shipped boot path opens it with a config of its own: packages/cli/src/commands/serve.ts forwards exactly two keys out of the stack's own top-level `api:` block (enableProjectScoping, projectResolution) and plugin-dev calls createRestApiPlugin() with no config at all, so a CLI-started deployment always gets the schema default for every key here. `live` answers who READS the key; reachability answers who can SET it, and the two never substitute for each other." }, diff --git a/packages/spec/scripts/lib/openapi-self-consistency.ts b/packages/spec/scripts/lib/openapi-self-consistency.ts index d251d4cf5b8..4d0265ee11b 100644 --- a/packages/spec/scripts/lib/openapi-self-consistency.ts +++ b/packages/spec/scripts/lib/openapi-self-consistency.ts @@ -30,11 +30,10 @@ * * 1. **Every local `$ref` resolves.** Any `$ref` beginning with `#/` is a JSON * Pointer into this same document; if it does not resolve, the document is - * broken for every consumer that parses it (Scalar's viewer at - * `GET /api/v1/docs`, and any client generator pointed at - * `GET /api/v1/openapi.json`). Resolution is by pointer rather than by a - * `#/components/schemas/` prefix match so that a future `#/$defs/…` ref - * is covered without touching this file. + * broken for every consumer that parses it (any client generator or + * API tool pointed at `GET /api/v1/openapi.json`). Resolution is by + * pointer rather than by a `#/components/schemas/` prefix match so that a + * future `#/$defs/…` ref is covered without touching this file. * 2. **No schema is silently degraded.** See `assertNoDegradedSchemas`. * * ## Rule 1 after #5744 — vacuous today, retained for `$defs` @@ -153,9 +152,9 @@ export function assertRefsResolve(doc: unknown): void { ` defined components.schemas: [${defined.join(', ') || ''}]\n` + `\n` + ` A $ref that resolves to nothing breaks every consumer of the published\n` + - ` document (the Scalar viewer at GET /api/v1/docs renders an empty schema\n` + - ` panel; client generators fail at parse time). If components.schemas is\n` + - ` empty, the collector in build-openapi.ts skipped its inputs — note that\n` + + ` document at GET /api/v1/openapi.json (client generators fail at parse\n` + + ` time). If components.schemas is empty, the collector in\n` + + ` build-openapi.ts skipped its inputs — note that\n` + ` lazySchema() returns a Proxy whose typeof is 'function', not 'object'\n` + ` (#5168).`, ); diff --git a/packages/spec/src/api/rest-server.zod.ts b/packages/spec/src/api/rest-server.zod.ts index e7c0ba878e1..8efe368c655 100644 --- a/packages/spec/src/api/rest-server.zod.ts +++ b/packages/spec/src/api/rest-server.zod.ts @@ -134,14 +134,13 @@ export const RestApiConfigSchema = lazySchema(() => z.object({ enableDiscovery: z.boolean().default(true).describe('Enable API discovery endpoint'), /** - * Enable OpenAPI 3.1 spec endpoints — `GET /openapi.json` - * for the machine-readable contract and `GET /docs` for a - * zero-dep Scalar-rendered HTML viewer. The spec ships pre-generated + * Enable the OpenAPI 3.1 document endpoint — `GET /openapi.json`, + * the machine-readable contract. The spec ships pre-generated * from @objectstack/spec and is enriched at request time with the * actual server URL + the runtime-registered objects (so `{object}` * placeholders expand into one path per concrete table). */ - enableOpenApi: z.boolean().default(true).describe('Enable OpenAPI 3.1 spec & docs viewer endpoints'), + enableOpenApi: z.boolean().default(true).describe('Enable the OpenAPI 3.1 document endpoint (`GET /openapi.json`)'), /** * Server-wide switch for the structured-search surface — and, like every key @@ -232,8 +231,7 @@ export const RestApiConfigSchema = lazySchema(() => z.object({ '`api.documentation.enabled` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — ' + 'nothing ever read it: whether the server publishes its OpenAPI document is decided by the sibling ' + '`api.enableOpenApi` at the mount, so `enabled: false` turned nothing off. Delete the key; ' - + '`api.enableOpenApi: false` is the switch that leaves the `/openapi.json` document and its `/docs` ' - + 'viewer unmounted.', + + '`api.enableOpenApi: false` is the switch that leaves the `/openapi.json` document unmounted.', ), // [#20294] `.optional()`, no longer `.default('ObjectStack API')`: that // default was materialized into every present block and never served diff --git a/packages/spec/src/migrations/entries/semantic/18.rest-api-config-dead-keys-retired.ts b/packages/spec/src/migrations/entries/semantic/18.rest-api-config-dead-keys-retired.ts index 121c5eb7363..2dd33d6398a 100644 --- a/packages/spec/src/migrations/entries/semantic/18.rest-api-config-dead-keys-retired.ts +++ b/packages/spec/src/migrations/entries/semantic/18.rest-api-config-dead-keys-retired.ts @@ -17,7 +17,7 @@ export const entry: SemanticMigration = { surface: 'restServer.api.responseFormat / restServer.api.documentation.enabled', replacement: '(removed — delete each key; neither had an effect to preserve. Whether the server publishes its ' - + 'OpenAPI document and the docs viewer is `api.enableOpenApi`, the switch the mount already reads. ' + + 'OpenAPI document is `api.enableOpenApi`, the switch the mount already reads. ' + 'Response shapes are fixed — each route answers in the response schema `@objectstack/spec/api` ' + 'declares for it — and are not a server-wide option, so there is no replacement for `responseFormat`.)', reason: @@ -43,7 +43,7 @@ export const entry: SemanticMigration = { + 'the REST plugin\'s `start`) with the retirement prescription, naming the key and ' + '`RestApiConfigSchema`, instead of being accepted and ignored; `tsc` refuses the key at the ' + 'authoring site (`never`). A host that meant "serve no OpenAPI document" sets `api.enableOpenApi: ' - + 'false` and sees `GET /openapi.json` and `GET /docs` unmounted. Every client that parses REST ' + + 'false` and sees `GET /openapi.json` unmounted. Every client that parses REST ' + 'responses reads each route\'s declared response shape. Every LIVE key of the `api` block — ' + 'including `documentation`\'s other members — parses byte-identically to before, and the mounted ' + 'REST surface is unchanged: ' From 32bfe10cda2f5fdec1be6233b6f45e013ab256f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 02:21:48 +0000 Subject: [PATCH 2/2] docs(spec): regenerate the rest-server reference and add the patch changeset Claude-Session: https://claude.ai/code/session_01B2EeooonSXrANpkpgUSfFZ Co-authored-by: Claude --- .changeset/25121-spec-openapi-viewer-prose.md | 16 ++++++++++++++++ content/docs/references/api/rest-server.mdx | 6 +++--- 2 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 .changeset/25121-spec-openapi-viewer-prose.md diff --git a/.changeset/25121-spec-openapi-viewer-prose.md b/.changeset/25121-spec-openapi-viewer-prose.md new file mode 100644 index 00000000000..7f14b83bdfd --- /dev/null +++ b/.changeset/25121-spec-openapi-viewer-prose.md @@ -0,0 +1,16 @@ +--- +'@objectstack/spec': patch +--- + +docs(spec): `api.enableOpenApi` and the `api.documentation.enabled` refusal name only the OpenAPI document; the retired docs viewer page is no longer described + +Clause-②: no +Metadata-protocol: none + +The REST server no longer serves an HTML viewer page at `GET {basePath}/docs`; it serves the machine-readable OpenAPI document at `GET {basePath}/openapi.json`, gated by `api.enableOpenApi`. The spec text now says only that: + +- **`RestApiConfigSchema.enableOpenApi`.** The description read "Enable OpenAPI 3.1 spec & docs viewer endpoints", and the TSDoc named a Scalar-rendered viewer at `GET {basePath}/docs`. Both now name the one endpoint the flag mounts, `GET {basePath}/openapi.json`. +- **The `api.documentation.enabled` refusal.** Its prescription said `api.enableOpenApi: false` leaves "the `/openapi.json` document and its `/docs` viewer unmounted". It now says the switch leaves the `/openapi.json` document unmounted. The key is refused exactly as before, at the same path, with the same opening sentence; only the closing words changed. +- **The `rest-api-config-dead-keys-retired` upgrade entry** (shown by `os migrate meta` and the upgrade guide) drops the viewer from its replacement and acceptance text the same way. + +No schema accepts or refuses anything new, no default changes, and no export moves. diff --git a/content/docs/references/api/rest-server.mdx b/content/docs/references/api/rest-server.mdx index eabedc3dc25..1fd84cef370 100644 --- a/content/docs/references/api/rest-server.mdx +++ b/content/docs/references/api/rest-server.mdx @@ -245,7 +245,7 @@ const result = BatchEndpointsConfigSchema.parse(data); | **enableUi** | `boolean` | optional (default: `true`) | Enable UI API endpoints (Views, Menus, Layouts) | | **enableBatch** | `boolean` | optional (default: `true`) | Enable batch operation endpoints | | **enableDiscovery** | `boolean` | optional (default: `true`) | Enable API discovery endpoint | -| **enableOpenApi** | `boolean` | optional (default: `true`) | Enable OpenAPI 3.1 spec & docs viewer endpoints | +| **enableOpenApi** | `boolean` | optional (default: `true`) | Enable the OpenAPI 3.1 document endpoint (`GET /openapi.json`) | | **enableSearch** | `boolean` | optional (default: `true`) | Enable structured search endpoints (server-wide search opt-out; embedder-only, not settable from `os serve`) | | **enableProjectScoping** | `boolean` | optional (default: `false`) | Enable project-scoped routing for data/meta/AI APIs | | **projectResolution** | `Enum<'required' \| 'optional' \| 'auto'>` | optional (default: `"auto"`) | Project ID resolution strategy | @@ -257,7 +257,7 @@ const result = BatchEndpointsConfigSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **enabled** | `never` | optional | [REMOVED] `api.documentation.enabled` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever read it: whether the server publishes its OpenAPI document is decided by the sibling `api.enableOpenApi` at the mount, so `enabled: false` turned nothing off. Delete the key; `api.enableOpenApi: false` is the switch that leaves the `/openapi.json` document and its `/docs` viewer unmounted. | +| **enabled** | `never` | optional | [REMOVED] `api.documentation.enabled` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever read it: whether the server publishes its OpenAPI document is decided by the sibling `api.enableOpenApi` at the mount, so `enabled: false` turned nothing off. Delete the key; `api.enableOpenApi: false` is the switch that leaves the `/openapi.json` document unmounted. | | **title** | `string` | optional | Title of the served OpenAPI document (`info.title`); unset keeps the bundled title | | **description** | `string` | optional | Description of the served OpenAPI document (`info.description`); unset keeps the bundled description. Your app's own release number belongs here | | **version** | `never` | optional | [REMOVED] `api.documentation.version` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever read it, and the served OpenAPI document's `info.version` has one source: the protocol version, i.e. the version of the `@objectstack/spec` package that generated the document, which no deployment configuration overrides. Delete the key. To publish your app's own release number, write it into `api.documentation.description`, which the served `info.description` carries. | @@ -293,7 +293,7 @@ const result = BatchEndpointsConfigSchema.parse(data); | **enableUi** | `boolean` | optional (default: `true`) | Enable UI API endpoints (Views, Menus, Layouts) | | **enableBatch** | `boolean` | optional (default: `true`) | Enable batch operation endpoints | | **enableDiscovery** | `boolean` | optional (default: `true`) | Enable API discovery endpoint | -| **enableOpenApi** | `boolean` | optional (default: `true`) | Enable OpenAPI 3.1 spec & docs viewer endpoints | +| **enableOpenApi** | `boolean` | optional (default: `true`) | Enable the OpenAPI 3.1 document endpoint (`GET /openapi.json`) | | **enableSearch** | `boolean` | optional (default: `true`) | Enable structured search endpoints (server-wide search opt-out; embedder-only, not settable from `os serve`) | | **enableProjectScoping** | `boolean` | optional (default: `false`) | Enable project-scoped routing for data/meta/AI APIs | | **projectResolution** | `Enum<'required' \| 'optional' \| 'auto'>` | optional (default: `"auto"`) | Project ID resolution strategy |