Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/25121-spec-openapi-viewer-prose.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 3 additions & 3 deletions content/docs/references/api/rest-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <basePath>/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 |
Expand All @@ -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. |
Expand Down Expand Up @@ -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 <basePath>/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 |
Expand Down
4 changes: 2 additions & 2 deletions packages/spec/liveness/rest_api.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
},
Expand Down
15 changes: 7 additions & 8 deletions packages/spec/scripts/lib/openapi-self-consistency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -153,9 +152,9 @@ export function assertRefsResolve(doc: unknown): void {
` defined components.schemas: [${defined.join(', ') || '<none>'}]\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).`,
);
Expand Down
10 changes: 4 additions & 6 deletions packages/spec/src/api/rest-server.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <basePath>/openapi.json`
* for the machine-readable contract and `GET <basePath>/docs` for a
* zero-dep Scalar-rendered HTML viewer. The spec ships pre-generated
* Enable the OpenAPI 3.1 document endpoint — `GET <basePath>/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 <basePath>/openapi.json`)'),

/**
* Server-wide switch for the structured-search surface — and, like every key
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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: '
Expand Down
Loading