From 98d9b095d900eef4779b765991833ab84dad77fb Mon Sep 17 00:00:00 2001 From: NeilClack Date: Thu, 10 Sep 2026 16:19:53 -0500 Subject: [PATCH] Document v2 API with OpenAPI schema --- docs/openapi.yaml | 467 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 467 insertions(+) create mode 100644 docs/openapi.yaml diff --git a/docs/openapi.yaml b/docs/openapi.yaml new file mode 100644 index 0000000..dc88351 --- /dev/null +++ b/docs/openapi.yaml @@ -0,0 +1,467 @@ +openapi: 3.0.3 +info: + title: Radiant Wave Portal API (v2) + version: 2.0.0 + description: | + Read API used by the Radiant Wave kiosk plus the automation onboarding + webhook. + + ## Authentication + The v2 API is **not** API-key gated. The former bearer-token middleware in + `src/hooks.server.ts` is present but disabled, so v2 routes are served + without a global API key. + + Two endpoints carry their own credential semantics: + + - `GET /api/v2/proclamations` optionally accepts the paired + `x-license-key` / `x-email` headers to scope results to an entitled user. + Supplying neither returns the full catalogue; supplying only one, or a + pair that does not match, is rejected. + - `POST /api/v2/automations/create/account` is protected by an HMAC-SHA256 + request signature (see the operation description). +servers: + - url: https://portal.radiantwavetech.com + description: Production + - url: http://localhost:5173 + description: Local development +tags: + - name: Catalogue + description: Reference data for categories, groups, frequencies and palettes. + - name: Proclamations + description: Proclamation content, optionally scoped by license entitlement. + - name: License + description: License key validation. + - name: Automations + description: Signed machine-to-machine webhooks. +paths: + /api/v2/categories: + get: + tags: [Catalogue] + summary: List proclamation categories + operationId: listCategories + responses: + '200': + description: All proclamation categories. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Category' + /api/v2/proclamation-categories: + get: + tags: [Catalogue] + summary: List proclamation categories (dedicated alias) + description: Alias of `/api/v2/categories`; returns the identical payload. + operationId: listProclamationCategories + responses: + '200': + description: All proclamation categories. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Category' + /api/v2/groups: + get: + tags: [Catalogue] + summary: List proclamation groups + description: Groups are ordered by `sortOrder` ascending. + operationId: listGroups + responses: + '200': + description: All proclamation groups. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Group' + /api/v2/proclamation-groups: + get: + tags: [Catalogue] + summary: List proclamation groups (dedicated alias) + description: Alias of `/api/v2/groups`; returns the identical payload. + operationId: listProclamationGroups + responses: + '200': + description: All proclamation groups. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Group' + /api/v2/frequencies: + get: + tags: [Catalogue] + summary: List canonical solfeggio frequencies + description: | + Returns the nine canonical frequencies (ids 1-9) the kiosk expects. + These are static values, not the portal database autoincrement ids. + operationId: listFrequencies + responses: + '200': + description: The nine canonical frequencies. + content: + application/json: + schema: + type: array + minItems: 9 + maxItems: 9 + items: + $ref: '#/components/schemas/Frequency' + /api/v2/color-palettes: + get: + tags: [Catalogue] + summary: List color palettes per canonical frequency + description: | + Returns one 7-color palette for each canonical frequency id (1-9). + Palette rows stored in the database take precedence; ids without a row + fall back to the built-in canonical palette. + operationId: listColorPalettes + responses: + '200': + description: Nine palettes, one per canonical frequency id. + content: + application/json: + schema: + type: array + minItems: 9 + maxItems: 9 + items: + $ref: '#/components/schemas/ColorPalette' + /api/v2/proclamations: + get: + tags: [Proclamations] + summary: List proclamations + description: | + Without entitlement headers, returns the full proclamation catalogue. + + With both `x-license-key` and `x-email`, returns only the proclamations + assigned to the matching user. Both headers must be present together and + the key must belong to the user. + + Proclamations whose frequency has no canonical id (for example 936 Hz) + are omitted from the response. + operationId: listProclamations + parameters: + - in: header + name: x-license-key + schema: + type: string + required: false + description: License key. Must be supplied together with `x-email`. + - in: header + name: x-email + schema: + type: string + format: email + required: false + description: Account email. Must be supplied together with `x-license-key`. + responses: + '200': + description: Proclamation records with canonical frequency ids and embedded palettes. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Proclamation' + '400': + description: | + Missing entitlement or the supplied key/email pair is invalid. + Message is `not-authorized`, or + `this key doesn't appear to belong to you` when the key does not + belong to the specified user. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + /api/v2/license: + post: + tags: [License] + summary: Validate a license key + description: | + Validates that the license key exists, is active, and belongs to the + account matching `email_address`. A successful validation records + `lastValidatedAt` on the key. + operationId: validateLicense + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LicenseRequest' + responses: + '200': + description: The validated key and its owner email. + content: + application/json: + schema: + $ref: '#/components/schemas/LicenseResponse' + '400': + description: | + Validation failure or malformed body. Possible messages: + `content-type must be application/json`, `invalid JSON body`, + `request body is required`, `license_key is required`, + `email_address is required`, `license key not found`, + `license key is not active`, `user does not exist`, + `key does not belong to specified user`. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + '413': + description: The request body exceeded the 64 KiB limit. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + '429': + description: Rate limit exceeded (10 requests per minute per IP). + headers: + Retry-After: + schema: + type: integer + description: Seconds to wait before retrying. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + /api/v2/automations/create/account: + post: + tags: [Automations] + summary: Create an account and license key (signed webhook) + description: | + Machine-to-machine onboarding webhook. The request must be signed with + HMAC-SHA256 using `API_V2_ONBOARDING_SECRET`. + + The canonical string that is signed is the newline-joined concatenation + of: + + ``` + POST + + + + + ``` + + where `` is `/api/v2/automations/create/account`. The signature is + the lowercase hex digest. + + On success, creates the user if they do not already exist, generates a + license key, emails it, and returns the user id. Invalid or missing + signatures return `401 { "error": "invalid signature" }` (the same + response is used for every signature failure, including replay and + timestamp skew). + operationId: createAutomationAccount + parameters: + - in: header + name: x-timestamp + required: true + schema: + type: string + description: Unix epoch seconds. Must be within 300 seconds of the server clock. + - in: header + name: x-nonce + required: true + schema: + type: string + description: Unique value per request. Replays within the skew window are rejected. + - in: header + name: x-signature + required: true + schema: + type: string + pattern: '^[0-9a-f]{64}$' + description: Lowercase hex HMAC-SHA256 of the canonical request string. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OnboardingRequest' + responses: + '200': + description: Account and license key created. + content: + application/json: + schema: + $ref: '#/components/schemas/OnboardingResponse' + '400': + description: | + Malformed or invalid request. Possible messages: + `request body is required`, `invalid JSON body`, or + `email is required`. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + '401': + description: Signature missing, malformed, expired, replayed, or incorrect. + content: + application/json: + schema: + type: object + required: [error] + properties: + error: + type: string + example: invalid signature + '413': + description: The request body exceeded the 64 KiB limit. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + '429': + description: | + Rate limit exceeded. Limits are 5 requests per minute per IP and + 3 requests per 24 hours per email address. + headers: + Retry-After: + schema: + type: integer + description: Seconds to wait before retrying. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + '500': + description: The account could not be created. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' +components: + schemas: + Category: + type: object + required: [id, name, sortOrder] + properties: + id: + type: integer + name: + type: string + sortOrder: + type: integer + Group: + type: object + required: [id, name, sortOrder] + properties: + id: + type: integer + name: + type: string + sortOrder: + type: integer + Frequency: + type: object + required: [id, name, value] + properties: + id: + type: integer + minimum: 1 + maximum: 9 + description: Canonical id used by the kiosk. + name: + type: string + value: + type: integer + description: Frequency in Hz. + ColorPalette: + type: object + required: [solfeggioFrequencyId, colors] + properties: + solfeggioFrequencyId: + type: integer + minimum: 1 + maximum: 9 + colors: + type: array + description: Seven hex colors, ordered light to dark. + minItems: 1 + items: + type: string + pattern: '^#[0-9A-Fa-f]{6}$' + Proclamation: + type: object + required: [id, name, content, categoryId, groupId, solfeggioFrequencyId] + properties: + id: + type: string + format: uuid + name: + type: string + content: + type: string + description: HTML content. + categoryId: + type: integer + groupId: + type: integer + solfeggioFrequencyId: + type: integer + minimum: 1 + maximum: 9 + colorPalette: + type: array + description: Present only when a palette exists for the canonical frequency id. + minItems: 1 + items: + type: string + pattern: '^#[0-9A-Fa-f]{6}$' + LicenseRequest: + type: object + required: [license_key, email_address] + properties: + license_key: + type: string + pattern: '^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$' + example: ABCD-EFGH-IJKL-MNOP + email_address: + type: string + format: email + maxLength: 254 + LicenseResponse: + type: object + required: [key, email_address] + properties: + key: + type: string + example: ABCD-EFGH-IJKL-MNOP + email_address: + type: string + format: email + OnboardingRequest: + type: object + required: [email] + properties: + email: + type: string + format: email + maxLength: 254 + name: + type: string + maxLength: 120 + description: Optional display name. Defaults to the email local-part. + OnboardingResponse: + type: object + required: [success, message, user_id] + properties: + success: + type: boolean + message: + type: string + example: Account and license key created + user_id: + type: string + ErrorMessage: + type: object + required: [message] + properties: + message: + type: string