diff --git a/openvidu-dashboard/openspec/changes/openapi-reference/.openspec.yaml b/openvidu-dashboard/openspec/changes/openapi-reference/.openspec.yaml deleted file mode 100644 index 97e364202..000000000 --- a/openvidu-dashboard/openspec/changes/openapi-reference/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-10-27 diff --git a/openvidu-dashboard/openspec/changes/openapi-reference/design.md b/openvidu-dashboard/openspec/changes/openapi-reference/design.md deleted file mode 100644 index 8f4f57049..000000000 --- a/openvidu-dashboard/openspec/changes/openapi-reference/design.md +++ /dev/null @@ -1,124 +0,0 @@ -# Design - -## Context - -- `RouteRegistry` holds one `RouteDefinition` per route, and `openApiDocument(registry, version)` - (`src/api/openapi.ts`) turns the listed routes into a zod-openapi 6 document. -- How the generator fills error responses today: - - It adds `400` to every operation, `401` to every non-public one, and `403` "PRO feature on CE" to every PRO route. - No document lists those routes in an edition that answers that 403: locks are left out in CE, and PRO never - answers it. - - It adds the `ApiError` schema only to the errors it adds itself. Errors a route declares (login `401`/`429`, - `ready` `503`, analytics `403`, detail `404`) have no body. -- Parameter descriptions come from `.meta()` on the Zod schemas: - - A `.meta()` placed before `.default()` stays inside the parameter's schema and never reaches the parameter object. - This is why `from` and `to` show no description. - - `csvParam()` parses a string, so its parameters render as plain strings. -- `DashboardOptions` carries the edition and version. The public base path is bound to `TOKENS.BasePath` by a CE - startup hook, before the docs router is built. - -## Goals / Non-Goals - -**Goals:** -- A document that a client generator and a reader can use as is. -- Pieces that let PRO document its routes the same way, without CE knowing them. -- Tests that keep the invariants for every route, not only for today's. - -**Non-Goals:** -- Changing requests, responses, status codes or authentication. -- Serving the document in production. - -## Decisions - -### Explicit `operationId`, checked by the registry - -- `RouteDefinition` gains a required `operationId`, camelCase, verb first: - - `list` for paged lists; - - `get` for single documents; - - `get` for sub-resources and summaries; - - `login`, `refreshSession`, `logout`, `getCurrentAdmin` for auth; - - `getHealth`, `getReadiness` for the probes. -- `register()` and `replace()` throw on an `operationId` another route already uses. A lock has none and is never - listed. - -Rejected: deriving IDs from method and path. Names such as `getApiV1RoomsSidSeries` read badly, and they would change -when a path changes. - -### Declared tags - -- `registry.tag(name, description)` declares a tag. Each route module declares its tags before registering its - routes. -- `register()` throws on an undeclared tag. -- The document lists the declared tags, in declaration order, that at least one listed operation uses. -- Tag names are Title Case nouns: `Probes`, `Auth`, `Info`, `Overview`, `Rooms`, `Participants`, `Agents`, `Egress`, - `Ingress`, `Analytics`, `Status`. -- PRO declares its own tags (such as `Tracks`) the same way. - -### Error responses built by the generator - -- `400` (`Invalid request`) is added only when the route has params, a query or a body. -- `401` is added for `bearer` and `optional-bearer` routes. -- The automatic `403` for PRO routes is removed. -- Every response with status ≥ 400 gets `application/json` with `ApiError` when the route gives no schema. -- `RouteResponse` gains `edition?: Edition`. A response with an edition is left out of other editions' documents. The - CE analytics routes mark their `PRO_FEATURE` `403` as `ce`. The registry exposes its edition for this. - -### Parameters - -- **Shared pieces in `query.ts`:** - - Shared parameters carry their descriptions after `.default()`, so zod-openapi lifts them to the parameter: `from`, - `to`, `tz`, `limit`, `order`, `cursor` and `q`. - - `to` gets its own example (`now`). -- **`csvParam(values)`:** adds `param: { style: 'form', explode: false }` and - `override: { type: 'array', items: { type: 'string', enum: values } }`. The route adds the description. The other - comma-separated parameters do the same: `features`, the egress `status` with its `all`, and the analytics `metrics` - and `dimensions`. -- **`If-None-Match`:** the generator adds this header parameter to every operation whose responses include `304`. -- **Integration test:** it asserts that every parameter of the booted document has a description. A new route without - one fails CI instead of silently degrading the document. - -Alternatives rejected for the list parameters: -- Changing their schemas to `z.array()` with `explode`: Express parses `a,b` as one string, so validation would change. -- A document-wide `override` hook keyed on the transform: too implicit. - -### Refresh cookie and `Set-Cookie` - -- `RouteDefinition` gains an optional `refreshCookie: 'required' | 'optional'`. It only affects the document; - authentication is unchanged, and the auth routes stay `public`. -- The generator adds the `refreshCookie` security scheme (`apiKey`, `in: cookie`, name `REFRESH_COOKIE`). It sets - `security: [{ refreshCookie: [] }]` for refresh, and `[{}, { refreshCookie: [] }]` for logout, which also answers - without a cookie. -- The auth responses that set or clear the cookie declare a `Set-Cookie` header with its attributes: login `200`, - refresh `200` and `401`, logout `204`. - -### Metadata - -- `openApiDocument(registry, { version, basePath, license? })`: - - `servers: [{ url: basePath without its trailing slash, or '/' }]`; - - `info.description`: one Markdown text of the conventions, edition-neutral; - - `info.license`: the given one, or `{ name: 'Apache 2.0', identifier: 'Apache-2.0' }`. -- `DashboardOptions` gains an optional `apiLicense`, which an extending edition passes. CE passes nothing. - -### Schema with required keys never defined - -`byInputType: z.record(inputType, count)` renders `required` without `properties`. It becomes -`z.object({ rtmp: count, whip: count, url: count })`, which validates the same values. - -### Unknown-route message - -`apiNotFound` is mounted on `/api`, so `req.path` lacks the prefix. The message uses the path of `req.originalUrl` -(without the query string), which is the path the client sent: `${req.baseUrl}${req.path}` would turn `/api` into -`/api/`. - -## Risks / Trade-offs - -- **[Breaking for PRO at compile time]** `operationId` becomes required, and undeclared tags throw. → PRO adds them in - the same move of `ce.ref`, with its own document test. -- **[`override` drifting from the parser]** The array rendering duplicates the allowed values. → Both come from the same - `values` argument of `csvParam()`. The other list parameters pass their own enum constant to both the parser and the - override. - -## Migration Plan - -This changes the document only; deploy as usual. Clients that generated code from the old document get stable method -names from now on. diff --git a/openvidu-dashboard/openspec/changes/openapi-reference/proposal.md b/openvidu-dashboard/openspec/changes/openapi-reference/proposal.md deleted file mode 100644 index da26fbfef..000000000 --- a/openvidu-dashboard/openspec/changes/openapi-reference/proposal.md +++ /dev/null @@ -1,75 +0,0 @@ -# Proposal - -## Why - -The OpenAPI document (`GET /api/v1/docs/openapi.json`) lists every route, but it does not describe the API well enough -to use it without reading the code: -- No operation has an `operationId`. -- Most parameters have no description. -- Comma-separated lists are typed as bare strings. -- Several error responses are missing or have no body schema. -- Some responses are declared that the edition never answers. -- There is no `servers`, `license` or tag list. - -The unknown-route `404` also names a path without its `/api` prefix. - -## What Changes - -- **Document metadata:** - - `info` gains a description of the API conventions (authentication, time ranges, pagination, errors, conditional - requests, request IDs, versioning) and the license. - - `servers` gains the public base path. - - A `tags` list, with one description per tag, in a stable order. -- **Operations:** - - Every operation has a unique `operationId` and tags from that list. - - Every parameter has a description. - - Comma-separated list parameters are documented as arrays of their allowed values (`style: form`, `explode: false`). - - Routes answered conditionally declare the `If-None-Match` request header next to their `ETag` and `304`. - - The auth routes declare the refresh cookie they read (as a cookie security scheme) and the `Set-Cookie` header they - send. -- **Error responses:** - - Every error response an operation can answer is declared with the `ApiError` schema. - - `400` is declared only for operations that take input. - - A response that only one edition answers appears only in that edition's document. This covers the automatic - `PRO_FEATURE` `403`, which no listed route answers, and the `403` of PRO metrics on the CE analytics routes. -- **Schemas:** the ingress summary's `byInputType` is a plain object with its three keys. It was a record that - required keys it never defined. -- **404 message:** the unknown-route `404` message names the full request path, `No API route for GET - /api/v1/does-not-exist`. - -Scope excludes the following: -- No route, parameter or response body changes. -- The PRO routes are documented by the PRO workspace on top of the generic pieces this change adds, so CE needs no - PRO-specific code, and the seams stay generic: an optional license for the document and the per-route fields. - -Edition: CE, and the generic pieces PRO builds on. Repository: `OpenVidu/openvidu` (dashboard) only. - -## Capabilities - -### New Capabilities - -None. - -### Modified Capabilities - -- `api-conventions`: - - The OpenAPI document gains requirements on its metadata, operations, parameters and error responses. - - The unknown-route scenario fixes its message. - -## Impact - -- Backend: - - `src/api/openapi.ts` (generator). - - `src/api/route-registry.ts` (`operationId`, tag declarations, per-response edition, refresh cookie field). - - `src/api/query.ts` (shared parameter descriptions and the array form of `csvParam`). - - Every `src/*/*-routes.ts` (operation IDs, tags, parameter descriptions, error responses). - - `src/ingress/ingress-responses.ts`. - - `src/http/errors.ts`. - - `src/lifecycle/startup.ts` (base path, and an optional license from `DashboardOptions`). -- Tests: unit tests of the generator and the registry, and an integration test that checks the invariants above on the - document of the booted CE dashboard. -- PRO: its own routes must supply `operationId`, declared tags and parameter descriptions. That is a compile-time - requirement once it moves `ce.ref`. -- CHANGELOG (`CHANGELOG.md`, 3.10.0, Added › Data), a new line: "API reference outside production: an OpenAPI 3.1 - document at `/api/v1/docs/openapi.json` and a browsable page at `/api/v1/docs`, with operation IDs, described - parameters and every error response." diff --git a/openvidu-dashboard/openspec/changes/openapi-reference/specs/api-conventions/spec.md b/openvidu-dashboard/openspec/changes/openapi-reference/specs/api-conventions/spec.md deleted file mode 100644 index 3810e8f39..000000000 --- a/openvidu-dashboard/openspec/changes/openapi-reference/specs/api-conventions/spec.md +++ /dev/null @@ -1,110 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Base path and resource naming - -Edition: CE. Every dashboard API route SHALL live under `/api/v1`, use lower-case plural nouns for resources -(`rooms`, `participants`, `agents`, `tracks`, `egresses`, `ingresses`, `analytics`, `overview`, `status`, `auth`, -`info`), nest -sub-resources at most one level under the parent identifier, and answer every unknown `/api` path with a JSON -`404` instead of the SPA fallback. - -#### Scenario: Sub-resource path - -- **GIVEN** a room with SID `RM_abc` -- **WHEN** the client requests its participants -- **THEN** the path is `GET /api/v1/rooms/RM_abc/participants` - -#### Scenario: Unknown API path - -- **WHEN** the client requests `GET /api/v1/does-not-exist` or `GET /api/anything` -- **THEN** the response is `404` with body `{ error: { code: 'NOT_FOUND', message } }` and - `Content-Type: application/json`, never the SPA `index.html`, and the message names the full path, such as - `No API route for GET /api/v1/does-not-exist` - -## ADDED Requirements - -### Requirement: OpenAPI document metadata - -Edition: CE. The OpenAPI document SHALL carry the following: -- an `info.description` of the API conventions; -- `info.license`: Apache 2.0 unless the edition provides another; -- one `servers` entry with the dashboard's public base path; -- a `tags` list with a description per tag, holding only the tags of the listed operations. - -#### Scenario: Behind a base path - -- **GIVEN** the server runs with `DASHBOARD_BASE_PATH=/dashboard/` and `NODE_ENV=development` -- **WHEN** the client reads the document -- **THEN** `servers[0].url` is `/dashboard`, and the paths start with `/api/v1` - -#### Scenario: Conventions described - -- **WHEN** the client reads `info.description` -- **THEN** it explains the following: - - the bearer access token and the refresh cookie; - - the `from`, `to` and `tz` time expressions; - - cursor pagination with `limit` and `total`; - - the `ApiError` envelope; - - `ETag` and `If-None-Match`; - - the `X-Request-Id` header; - - the versioning policy. - -#### Scenario: Tags - -- **WHEN** the client reads `tags` -- **THEN** every operation's tags are listed there with a description, and no listed tag lacks an operation - -### Requirement: OpenAPI operations - -Edition: CE. Every operation of the OpenAPI document SHALL have the following: -- a unique `operationId`, a summary and tags; -- a description for every parameter, with comma-separated list parameters as arrays of their allowed values - (`style: form`, `explode: false`); -- an `If-None-Match` header parameter when it can answer `304`; -- on the auth routes, the refresh cookie as a cookie security scheme, and the `Set-Cookie` header of the responses that - set or clear it. - -#### Scenario: Operation identifiers - -- **WHEN** the client reads every operation of the document -- **THEN** each has an `operationId` that no other operation shares, such as `listRooms` for `GET /api/v1/rooms` - -#### Scenario: List parameter - -- **WHEN** the client reads the `kind` parameter of `GET /api/v1/participants` -- **THEN** it has a description, `style: form`, `explode: false`, and an array schema whose items enumerate the - participant kinds - -#### Scenario: Conditional request - -- **WHEN** the client reads `GET /api/v1/overview` -- **THEN** it declares the `If-None-Match` header parameter, the `ETag` header of its `200`, and its `304` - -#### Scenario: Refresh cookie - -- **WHEN** the client reads `POST /api/v1/auth/refresh` -- **THEN** its security names the refresh cookie scheme (`in: cookie`, `name: ov_dashboard_refresh`), and its `200` and - `401` declare `Set-Cookie` - -### Requirement: OpenAPI error responses - -Edition: CE. Every error response an operation can answer SHALL be declared with the `ApiError` schema. This means -`400` only for operations that take parameters or a body, `401` for operations that need or accept a token, and the -route's own errors such as `404`, `429` and `503`. A response that only one edition answers SHALL appear only in that -edition's document. - -#### Scenario: Operation without input - -- **WHEN** the client reads `GET /api/v1/health` -- **THEN** it declares no `400` - -#### Scenario: Error body - -- **WHEN** the client reads the `429` of `POST /api/v1/auth/login` -- **THEN** its content is `application/json` with the `ApiError` schema - -#### Scenario: Edition-specific error - -- **WHEN** the client reads `GET /api/v1/analytics/series` in the CE document -- **THEN** it declares the `403` of a PRO metric, dimension or node scope; and no CE operation declares a `403` - `PRO_FEATURE` for being a PRO route diff --git a/openvidu-dashboard/openspec/changes/openapi-reference/tasks.md b/openvidu-dashboard/openspec/changes/openapi-reference/tasks.md deleted file mode 100644 index d8ef3054e..000000000 --- a/openvidu-dashboard/openspec/changes/openapi-reference/tasks.md +++ /dev/null @@ -1,68 +0,0 @@ -# Tasks - -## 1. Backend: registry and generator - -- [x] 1.1 In `src/api/route-registry.ts`, add: - - a required `operationId` and an optional `refreshCookie: 'required' | 'optional'` to `RouteDefinition`; - - `edition?: Edition` to `RouteResponse`; - - `tag(name, description)` and the `edition` getter to `RouteRegistry`. - `register()` and `replace()` reject a repeated `operationId` and an undeclared tag. Verify with - `route-registry.test.ts` cases for both rejections. -- [x] 1.2 In `src/api/openapi.ts`, change `openApiDocument(registry, { version, basePath, license? })` to emit: - - `servers`, `info.description`, `info.license` and `tags` (declared and used, in declaration order); - - `operationId`; - - `400` only with input, `401` for `bearer` and `optional-bearer`, no automatic `403`; - - `ApiError` on every error without a schema; - - responses filtered by `edition`; - - the `If-None-Match` parameter on operations with `304`; - - the `refreshCookie` security scheme; - and drop the dead locked-route description. Verify with `route-registry.test.ts` cases for each rule. -- [x] 1.3 In `src/lifecycle/startup.ts` and `src/index.ts`, add the optional `DashboardOptions.apiLicense` and pass it, - with the bound base path, to the document. Verify with the typecheck and the `servers` case of 1.2. - -## 2. Backend: parameters and routes - -- [x] 2.1 In `src/api/query.ts`, describe `from`, `to`, `tz`, `limit`, `order`, `cursor` and `q` at the parameter - level (after `.default()`), with `to` getting its own example. Make `csvParam()` render as an array of its values - with `style: form`, `explode: false`. Verify with a generator unit case on a ranged list query. -- [x] 2.2 In every `src/*/*-routes.ts` and `src/api/probes.ts`: - - declare the module's tags; - - give each route its `operationId`; - - describe every route-specific parameter; - - render the other comma-separated parameters as arrays (`features`, egress `status`, analytics `metrics` and - `dimensions`); - - mark the analytics `403` as `edition: 'ce'`; - - add `refreshCookie` and the `Set-Cookie` headers to the auth routes. - Verify with the integration test of 3.1. -- [x] 2.3 In `src/ingress/ingress-responses.ts`, make `byInputType` a `z.object` with `rtmp`, `whip` and `url`. Verify - with the ingress response unit tests and the mapper checks. -- [x] 2.4 In `src/http/errors.ts`, make `apiNotFound` name the path of `req.originalUrl`. Verify with the app unit test - of the unknown API path, which asserts the message. - -## 3. Tests and docs - -- [x] 3.1 Add the integration test `test/integration/openapi-document.test.ts`. It boots the CE dashboard in - development and reads `/api/v1/docs/openapi.json`, then asserts: - - unique `operationId`s; - - every tag is declared with a description; - - every parameter has a description; - - every response ≥ 400 has `ApiError`; - - no `400` on operations without input; - - no `403` `PRO_FEATURE` outside the analytics routes; - - `If-None-Match` wherever there is a `304`; - - the refresh cookie scheme on refresh and logout; - - `servers` and `license`. - Verify that `./dashboard.sh test-integration` passes. -- [x] 3.2 Lint both a development document and the generated one with Redocly CLI (`recommended`, scratch install, - not a dependency). Expect no errors, and no warnings other than intentional ones recorded in this file. Verify by - running it by hand once. - Result: valid. The only warnings are `operation-4xx-response` on `getHealth`, `getReadiness` and `logout`, which - answer no 4xx by design. -- [x] 3.3 Update `dashboard-ce/backend/AGENTS.md` (route declaration: `operationId`, tags through `registry.tag`, - parameter descriptions, response `edition`, `refreshCookie`), and add the `CHANGELOG.md` line of the proposal. - Verify that `./dashboard.sh check` passes. - -## Workflow follow-up - -- Move `ce.ref` in openvidu-pro and document the PRO routes there (PRO change). -- Archive when the owner asks. diff --git a/openvidu-dashboard/openspec/specs/api-conventions/spec.md b/openvidu-dashboard/openspec/specs/api-conventions/spec.md index acc6d7f5d..5d369c754 100644 --- a/openvidu-dashboard/openspec/specs/api-conventions/spec.md +++ b/openvidu-dashboard/openspec/specs/api-conventions/spec.md @@ -25,7 +25,8 @@ sub-resources at most one level under the parent identifier, and answer every un - **WHEN** the client requests `GET /api/v1/does-not-exist` or `GET /api/anything` - **THEN** the response is `404` with body `{ error: { code: 'NOT_FOUND', message } }` and - `Content-Type: application/json`, never the SPA `index.html` + `Content-Type: application/json`, never the SPA `index.html`, and the message names the full path, such as + `No API route for GET /api/v1/does-not-exist` ### Requirement: JSON field conventions @@ -583,3 +584,89 @@ omit `stale` otherwise, also when it has no `lastStatsAt`. - **GIVEN** a closed room whose `lastStatsAt` is 2 hours old - **WHEN** it is returned - **THEN** it has no `stale` property + +### Requirement: OpenAPI document metadata + +Edition: CE. The OpenAPI document SHALL carry the following: +- an `info.description` of the API conventions; +- `info.license`: Apache 2.0 unless the edition provides another; +- one `servers` entry with the dashboard's public base path; +- a `tags` list with a description per tag, holding only the tags of the listed operations. + +#### Scenario: Behind a base path + +- **GIVEN** the server runs with `DASHBOARD_BASE_PATH=/dashboard/` and `NODE_ENV=development` +- **WHEN** the client reads the document +- **THEN** `servers[0].url` is `/dashboard`, and the paths start with `/api/v1` + +#### Scenario: Conventions described + +- **WHEN** the client reads `info.description` +- **THEN** it explains the following: + - the bearer access token and the refresh cookie; + - the `from`, `to` and `tz` time expressions; + - cursor pagination with `limit` and `total`; + - the `ApiError` envelope; + - `ETag` and `If-None-Match`; + - the `X-Request-Id` header; + - the versioning policy. + +#### Scenario: Tags + +- **WHEN** the client reads `tags` +- **THEN** every operation's tags are listed there with a description, and no listed tag lacks an operation + +### Requirement: OpenAPI operations + +Edition: CE. Every operation of the OpenAPI document SHALL have the following: +- a unique `operationId`, a summary and tags; +- a description for every parameter, with comma-separated list parameters as arrays of their allowed values + (`style: form`, `explode: false`); +- an `If-None-Match` header parameter when it can answer `304`; +- on the auth routes, the refresh cookie as a cookie security scheme, and the `Set-Cookie` header of the responses that + set or clear it. + +#### Scenario: Operation identifiers + +- **WHEN** the client reads every operation of the document +- **THEN** each has an `operationId` that no other operation shares, such as `listRooms` for `GET /api/v1/rooms` + +#### Scenario: List parameter + +- **WHEN** the client reads the `kind` parameter of `GET /api/v1/participants` +- **THEN** it has a description, `style: form`, `explode: false`, and an array schema whose items enumerate the + participant kinds + +#### Scenario: Conditional request + +- **WHEN** the client reads `GET /api/v1/overview` +- **THEN** it declares the `If-None-Match` header parameter, the `ETag` header of its `200`, and its `304` + +#### Scenario: Refresh cookie + +- **WHEN** the client reads `POST /api/v1/auth/refresh` +- **THEN** its security names the refresh cookie scheme (`in: cookie`, `name: ov_dashboard_refresh`), and its `200` and + `401` declare `Set-Cookie` + +### Requirement: OpenAPI error responses + +Edition: CE. Every error response an operation can answer SHALL be declared with the `ApiError` schema. This means +`400` only for operations that take parameters or a body, `401` for operations that need or accept a token, and the +route's own errors such as `404`, `429` and `503`. A response that only one edition answers SHALL appear only in that +edition's document. + +#### Scenario: Operation without input + +- **WHEN** the client reads `GET /api/v1/health` +- **THEN** it declares no `400` + +#### Scenario: Error body + +- **WHEN** the client reads the `429` of `POST /api/v1/auth/login` +- **THEN** its content is `application/json` with the `ApiError` schema + +#### Scenario: Edition-specific error + +- **WHEN** the client reads `GET /api/v1/analytics/series` in the CE document +- **THEN** it declares the `403` of a PRO metric, dimension or node scope; and no CE operation declares a `403` + `PRO_FEATURE` for being a PRO route