Document v2 API with OpenAPI schema
This commit is contained in:
@@ -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
|
||||||
|
<path>
|
||||||
|
<x-timestamp>
|
||||||
|
<x-nonce>
|
||||||
|
<raw request body>
|
||||||
|
```
|
||||||
|
|
||||||
|
where `<path>` 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
|
||||||
Reference in New Issue
Block a user