{ "openapi": "3.0.0", "info": { "title": "Runic Gateway API", "version": "1.0.0", "description": "REST API for the Runic Gateway website, wiki and admin panel.\n\nThis document is core. Installed modules add their own paths, tags and schemas to it at request time from the fragment each one ships, so `/api/docs.json` on a running instance describes more than `npm run swagger` generates here (docs/website/MODULE_API.md §6.1a).\n\n### Authentication\n- **Web / admin panel** uses an httpOnly session cookie (`rg_token`) issued by `POST /api/v1/auth/login` (plus `/login/totp` when 2FA is enabled).\n- **Native / mobile clients** use bearer access tokens from `POST /api/v1/auth/mobile/login`, refreshed via `/auth/mobile/refresh`.\n\nEndpoints under `/api/v1/admin/**` require a valid session; some are further restricted to the `admin` role (editors are limited to content)." }, "servers": [ { "url": "/", "description": "Same-origin (current host)" }, { "url": "http://localhost:3000", "description": "Local development" } ], "tags": [ { "name": "Health", "description": "Liveness probe" }, { "name": "Auth", "description": "Web session login/logout (cookie + TOTP)" }, { "name": "Auth · Me", "description": "The signed-in account: profile, notification streams and devices" }, { "name": "Auth · Mobile", "description": "Native bearer-token login, refresh and logout" }, { "name": "Auth · SSO", "description": "OAuth2 / OIDC provider discovery and redirect flow" }, { "name": "Public", "description": "Unauthenticated site content (settings, posts, wiki, contact)" }, { "name": "Admin · Account", "description": "Self-service account security (2FA, linked identities)" }, { "name": "Player", "description": "Self-service player accounts (register, credentials, 2FA, linked identities)" }, { "name": "Player · Appeals", "description": "Player-submitted moderation appeals" }, { "name": "Settings", "description": "Site-wide settings any authenticated account may read (nav overrides)" }, { "name": "Admin · Dashboard", "description": "Dashboard summary and site mode" }, { "name": "Admin · Posts", "description": "News / five-on-friday / newsletter / screenshots + uploads" }, { "name": "Admin · Pages", "description": "Editable static site pages" }, { "name": "Admin · Wiki", "description": "Wiki pages, categories, tags and revisions" }, { "name": "Admin · Settings", "description": "Site settings (admin only)" }, { "name": "Admin · Email", "description": "Outbound email configuration and delivery test (admin only)" }, { "name": "Admin · Invites", "description": "Registration invites — issue, list and revoke" }, { "name": "Admin · Moderation", "description": "Player reports, appeals and moderator actions" }, { "name": "Admin · Activity", "description": "Admin activity log" }, { "name": "Admin · Bot Activity", "description": "Bot-scoring/ban state and emergency unban (admin only)" }, { "name": "Admin · Discord Bot", "description": "Discord bot token/config and live status (admin only)" }, { "name": "Admin · Auth Providers", "description": "SSO provider configuration (admin only)" }, { "name": "Admin · Users", "description": "User management (admin only)" } ], "paths": { "/api/csp-report": { "post": { "tags": [ "Health" ], "summary": "Content-Security-Policy violation report sink", "description": "Receives CSP violation reports from browsers (both the `report-uri` `application/csp-report` format and the Reporting API `application/reports+json` format). Unauthenticated by necessity — browsers send reports with no session. Reports are logged, never stored or echoed. Always answers 204.", "responses": { "204": { "description": "Report accepted (or ignored). No content." }, "429": { "description": "Too many reports from this address." } }, "security": [] } }, "/api/health": { "get": { "tags": [ "Health" ], "summary": "Liveness probe", "description": "", "responses": { "200": { "description": "Service is up", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "example": "ok" } } } } } } } } }, "/api/v1/admin/account": { "get": { "tags": [ "Admin · Account" ], "summary": "Get the current account (self)", "description": "", "responses": { "200": { "description": "The account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountStatus" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/account/identities": { "get": { "tags": [ "Admin · Account" ], "summary": "List linked SSO identities (self)", "description": "", "responses": { "200": { "description": "Linked identities", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/LinkedIdentity" } } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/account/identities/{provider}": { "delete": { "tags": [ "Admin · Account" ], "summary": "Unlink an SSO identity (self)", "description": "", "parameters": [ { "name": "provider", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Provider id." } ], "responses": { "200": { "description": "Unlinked", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UnlinkedFlag" } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No linked account for that provider", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/account/totp/disable": { "post": { "tags": [ "Admin · Account" ], "summary": "Disable 2FA by confirming a code", "description": "", "responses": { "200": { "description": "2FA disabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TotpState" } } } }, "400": { "description": "Not enabled, or invalid code", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TotpCodeRequest" } } } } } }, "/api/v1/admin/account/totp/enable": { "post": { "tags": [ "Admin · Account" ], "summary": "Enable 2FA by confirming a code", "description": "", "responses": { "200": { "description": "2FA enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TotpState" } } } }, "400": { "description": "Setup not started, or invalid code", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Two-factor already enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TotpCodeRequest" } } } } } }, "/api/v1/admin/account/totp/setup": { "post": { "tags": [ "Admin · Account" ], "summary": "Begin 2FA enrollment (returns secret + QR)", "description": "", "responses": { "200": { "description": "otpauth URL and QR data to scan", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TotpSetup" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Two-factor already enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/activity": { "get": { "tags": [ "Admin · Activity" ], "summary": "List recent admin activity", "description": "", "parameters": [ { "name": "offset", "in": "query", "schema": { "type": "string" } }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Max rows to return." } ], "responses": { "200": { "description": "Activity entries", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/auth/providers": { "get": { "tags": [ "Admin · Auth Providers" ], "summary": "List configured SSO providers (admin only)", "description": "", "responses": { "200": { "description": "Providers (secrets stripped)", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ProviderConfig" } } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "tags": [ "Admin · Auth Providers" ], "summary": "Create a custom SSO provider (admin only)", "description": "Built-in providers (google, discord) are configured via PUT, not created here.", "responses": { "201": { "description": "Created provider", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProviderConfig" } } } }, "400": { "description": "Validation error, or a built-in/invalid kind", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Provider id already exists", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProviderCreateRequest" } } } } } }, "/api/v1/admin/auth/providers/{id}": { "put": { "tags": [ "Admin · Auth Providers" ], "summary": "Update an SSO provider (admin only)", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Provider id." } ], "responses": { "200": { "description": "Updated provider", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProviderConfig" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Provider not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProviderCreateRequest" } } } } }, "delete": { "tags": [ "Admin · Auth Providers" ], "summary": "Delete a custom SSO provider (admin only)", "description": "Built-in providers cannot be deleted — disable them instead.", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Provider id." } ], "responses": { "200": { "description": "Deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeletedFlag" } } } }, "400": { "description": "Built-in provider cannot be deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Provider not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/bot-activity": { "get": { "tags": [ "Admin · Bot Activity" ], "summary": "Bot-scoring / ban state and recent events (admin only)", "description": "", "responses": { "200": { "description": "Banned IPs, scores and recent events", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/bot-activity/unban": { "post": { "tags": [ "Admin · Bot Activity" ], "summary": "Emergency unban an IP (admin only)", "description": "", "responses": { "200": { "description": "Unbanned (echoes the ip and whether an entry was cleared)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UnbanResult" } } } }, "400": { "description": "Invalid IP", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UnbanRequest" } } } } } }, "/api/v1/admin/dashboard": { "get": { "tags": [ "Admin · Dashboard" ], "summary": "Dashboard summary counts", "description": "", "responses": { "200": { "description": "Summary: site mode, last change, post/user counts and recent activity", "content": { "application/json": { "schema": { "type": "object", "properties": { "site_mode": { "type": "string", "example": "live" }, "last_change": { "type": "object", "properties": { "at": { "type": "string", "nullable": true }, "by": { "type": "string", "nullable": true } } }, "counts": { "type": "object", "properties": { "posts": { "type": "object", "additionalProperties": true }, "users": { "type": "integer" } } }, "recent_activity": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/discord-bot/config": { "get": { "tags": [ "Admin · Discord Bot" ], "summary": "Get Discord bot config + live status (admin only)", "description": "", "responses": { "200": { "description": "Masked config + live status", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "tags": [ "Admin · Discord Bot" ], "summary": "Save Discord bot config (admin only)", "description": "token is write-only — omit/blank it to keep the existing one unchanged.", "responses": { "200": { "description": "Updated config + live status", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Validation error, invalid token, or missing token while enabling", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "guildId": { "type": "string" }, "token": { "type": "string" }, "enabled": { "type": "boolean" } } } } } } } }, "/api/v1/admin/email/config": { "get": { "tags": [ "Admin · Email" ], "summary": "Get email delivery config + status (admin only)", "description": "", "responses": { "200": { "description": "Config (refresh token stripped) + status", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "tags": [ "Admin · Email" ], "summary": "Update email delivery config (admin only)", "description": "Set the From display name and enabled toggle. Enabling requires a connected Gmail account.", "responses": { "200": { "description": "Updated config", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Cannot enable before connecting a mailbox", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "senderName": { "type": "string" }, "enabled": { "type": "boolean" } } } } } } } }, "/api/v1/admin/email/connect/callback": { "get": { "tags": [ "Admin · Email" ], "summary": "OAuth2 callback — stores the refresh token, redirects to Settings", "description": "", "parameters": [ { "name": "code", "in": "query", "schema": { "type": "string" } }, { "name": "state", "in": "query", "schema": { "type": "string" } }, { "name": "error", "in": "query", "schema": { "type": "string" } } ], "responses": { "302": { "description": "Redirect back to /admin/settings" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/email/connect/start": { "get": { "tags": [ "Admin · Email" ], "summary": "Begin the Gmail OAuth2 connect flow (admin only)", "description": "Returns { url } to redirect the browser to Google. Reuses the google SSO OAuth client.", "responses": { "200": { "description": "Authorization URL", "content": { "application/json": { "schema": { "type": "object", "properties": { "url": { "type": "string" } } } } } }, "400": { "description": "Google OAuth client not configured", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/email/disconnect": { "post": { "tags": [ "Admin · Email" ], "summary": "Disconnect Gmail and disable email (admin only)", "description": "", "responses": { "200": { "description": "Disconnected config", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/email/test": { "post": { "tags": [ "Admin · Email" ], "summary": "Send a test email (admin only)", "description": "", "responses": { "200": { "description": "Sent", "content": { "application/json": { "schema": { "type": "object", "properties": { "sent": { "type": "boolean" }, "to": { "type": "string" } } } } } }, "400": { "description": "Bad Request" }, "502": { "description": "Send failed / not configured", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "to": { "type": "string", "format": "email" } } } } } } } }, "/api/v1/admin/invites": { "post": { "tags": [ "Admin · Invites" ], "summary": "Create and email an account invite at a chosen access level", "description": "", "responses": { "201": { "description": "Invite created", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "email", "role" ], "properties": { "email": { "type": "string" }, "role": { "type": "string" } } } } } } }, "get": { "tags": [ "Admin · Invites" ], "summary": "List recent invites (no tokens)", "description": "", "parameters": [ { "name": "limit", "in": "query", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Invites, newest first", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/invites/{id}": { "delete": { "tags": [ "Admin · Invites" ], "summary": "Revoke a pending invite", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Invite id." } ], "responses": { "200": { "description": "Revoked", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "404": { "description": "No pending invite to revoke", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/moderation/appeals": { "get": { "tags": [ "Admin · Moderation" ], "summary": "List moderation appeals (default: pending + under_review)", "description": "Filter with ?status= or ?status=all. Paginated with ?limit&offset.", "responses": { "200": { "description": "Appeals queue", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/AppealQueueItem" } } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/moderation/appeals/{id}": { "get": { "tags": [ "Admin · Moderation" ], "summary": "Get a single moderation appeal", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Appeal id." } ], "responses": { "200": { "description": "The appeal", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AppealQueueItem" } } } }, "400": { "description": "Bad Request" }, "404": { "description": "Appeal not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/moderation/appeals/{id}/claim": { "post": { "tags": [ "Admin · Moderation" ], "summary": "Claim a pending appeal (→ under_review)", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Appeal id." } ], "responses": { "200": { "description": "The claimed appeal", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AppealQueueItem" } } } }, "400": { "description": "Bad Request" }, "404": { "description": "Appeal not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Appeal is not open for claiming", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/moderation/appeals/{id}/resolve": { "post": { "tags": [ "Admin · Moderation" ], "summary": "Resolve an appeal (approved | denied); approval may auto-reverse the Discord action", "description": "Approving a ban/mute appeal best-effort asks the bot to reverse the Discord action (unban / clear timeout). The bot being down never fails the resolution — reversal_status is recorded as failed. The response echoes the updated appeal plus a `reversal` object.", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Appeal id." } ], "responses": { "200": { "description": "The resolved appeal (with reversal outcome)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AppealResolveResult" } } } }, "400": { "description": "Validation error (status must be approved or denied)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "404": { "description": "Appeal not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Appeal is already resolved", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResolveAppealRequest" } } } } } }, "/api/v1/admin/moderation/filter-hits": { "get": { "tags": [ "Admin · Moderation" ], "summary": "Recent automated content-filter hits", "description": "", "responses": { "200": { "description": "OK" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/moderation/members": { "get": { "tags": [ "Admin · Moderation" ], "summary": "Recent member join/leave events (optionally filtered by type)", "description": "", "parameters": [ { "name": "type", "in": "query", "schema": { "type": "string" } } ], "responses": { "200": { "description": "OK" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/moderation/recent": { "get": { "tags": [ "Admin · Moderation" ], "summary": "Recent moderation actions, optionally filtered by type", "description": "", "responses": { "200": { "description": "OK" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/moderation/search": { "get": { "tags": [ "Admin · Moderation" ], "summary": "Look up moderated users by Discord id or username snapshot", "description": "", "parameters": [ { "name": "q", "in": "query", "schema": { "type": "string" } } ], "responses": { "200": { "description": "OK" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/moderation/spam-hits": { "get": { "tags": [ "Admin · Moderation" ], "summary": "Recent automated spam-detection hits", "description": "", "responses": { "200": { "description": "OK" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/moderation/stats/summary": { "get": { "tags": [ "Admin · Moderation" ], "summary": "Moderation action counts for 24h/7d/30d (admin or moderator)", "description": "", "responses": { "200": { "description": "OK" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/moderation/user/{discordId}": { "get": { "tags": [ "Admin · Moderation" ], "summary": "Per-user moderation summary (counts, latest tag, linked account)", "description": "", "parameters": [ { "name": "discordId", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "OK" }, "400": { "description": "Bad Request" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/moderation/user/{discordId}/actions": { "get": { "tags": [ "Admin · Moderation" ], "summary": "Full moderation action history for a user", "description": "", "parameters": [ { "name": "discordId", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "OK" }, "400": { "description": "Bad Request" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/moderation/user/{discordId}/appeals": { "get": { "tags": [ "Admin · Moderation" ], "summary": "Appeals submitted for a Discord user", "description": "", "parameters": [ { "name": "discordId", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Discord snowflake." } ], "responses": { "200": { "description": "Appeals for the user", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/AppealQueueItem" } } } } }, "400": { "description": "Bad Request" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/moderation/user/{discordId}/notes": { "get": { "tags": [ "Admin · Moderation" ], "summary": "Staff notes for a user (admin_only notes hidden from moderators)", "description": "", "parameters": [ { "name": "discordId", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "OK" }, "400": { "description": "Bad Request" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "tags": [ "Admin · Moderation" ], "summary": "Add a staff note (admin_only visibility requires the admin role)", "description": "", "parameters": [ { "name": "discordId", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "201": { "description": "Created" }, "400": { "description": "Bad Request" }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "visibility": { "example": "any" }, "body": { "example": "any" } } } } } } } }, "/api/v1/admin/modules": { "get": { "tags": [ "Admin · Modules" ], "summary": "List installed modules, their live state, and the source allowlist", "description": "", "responses": { "200": { "description": "Modules and the install source allowlist", "content": { "application/json": { "schema": { "type": "object", "properties": { "modules": { "type": "array", "items": { "type": "object", "additionalProperties": true } }, "sourceHosts": { "type": "array", "items": { "type": "string" } } } } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "tags": [ "Admin · Modules" ], "summary": "Install or upgrade a module from a release install-manifest URL", "description": "Downloads the artifact the manifest names, verifies its sha256, inspects the archive in full and unpacks it onto the modules volume. The module mounts on the next restart.", "responses": { "201": { "description": "Installed — restart to mount it", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "The URL, the manifest, the hash or the archive was refused", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" }, "502": { "description": "The source host could not be reached or answered badly", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "url" ], "properties": { "url": { "type": "string", "description": "https URL of the release install manifest, on an allowed host" } } } } } } } }, "/api/v1/admin/modules/restart": { "post": { "tags": [ "Admin · Modules" ], "summary": "Restart the server process so module changes take effect", "description": "Runs the same graceful shutdown a SIGTERM does. The process is brought back by the supervisor, which the shipped docker-compose.yml provides; a bare `npm start` will not come back.", "responses": { "202": { "description": "Shutting down", "content": { "application/json": { "schema": { "type": "object", "properties": { "restarting": { "type": "boolean" } } } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/modules/sources": { "put": { "tags": [ "Admin · Modules" ], "summary": "Replace the allowlist of hosts modules may be installed from", "description": "", "responses": { "200": { "description": "The new allowlist", "content": { "application/json": { "schema": { "type": "object", "properties": { "sourceHosts": { "type": "array", "items": { "type": "string" } } } } } } }, "400": { "description": "One of the entries is not a hostname", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "hosts" ], "properties": { "hosts": { "type": "string", "description": "Comma- or space-separated hostnames. An empty list forbids all installs." } } } } } } } }, "/api/v1/admin/modules/{id}": { "delete": { "tags": [ "Admin · Modules" ], "summary": "Uninstall a module, optionally deleting its data too", "description": "Removes the module directory and leaves its row disabled. With purge=true it also runs purge.sql first — which is the only moment it can, since purge.sql lives inside the directory being deleted.", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Module id." }, { "name": "purge", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "Also run the module’s purge.sql and drop its row. Destructive and irreversible." } ], "responses": { "200": { "description": "Uninstalled — restart to unmount it", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Purge was asked for and the module ships no purge.sql", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such module", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/modules/{id}/disable": { "post": { "tags": [ "Admin · Modules" ], "summary": "Stop a module now — runs its onShutdown, then its routes answer 404", "description": "The only module action that takes effect without a restart. Re-enabling needs one, because there is no onBoot re-dispatch.", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Module id." } ], "responses": { "200": { "description": "Disabled", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "404": { "description": "No such module", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/modules/{id}/enable": { "post": { "tags": [ "Admin · Modules" ], "summary": "Enable a module (takes effect on the next restart)", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Module id." } ], "responses": { "200": { "description": "Enabled — restart to start it", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "404": { "description": "No such module", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Conflict" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/modules/{id}/purge": { "post": { "tags": [ "Admin · Modules" ], "summary": "Run a disabled module’s purge.sql, dropping its tables and data", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Module id." } ], "responses": { "200": { "description": "Purged", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "string" }, "purged": { "type": "integer" } } } } } }, "400": { "description": "The module ships no purge.sql", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not Found" }, "409": { "description": "The module must be disabled first", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/pages": { "get": { "tags": [ "Admin · Pages" ], "summary": "List all CMS pages (summaries)", "description": "", "responses": { "200": { "description": "Page summaries", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "tags": [ "Admin · Pages" ], "summary": "Create a CMS page", "description": "", "responses": { "201": { "description": "Created page", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Invalid slug / title / blocks / metadata / settings", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Slug already exists", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "slug": { "type": "string" }, "title": { "type": "string" }, "status": { "type": "string", "enum": [ "draft", "published" ] }, "blocks": { "type": "array", "items": { "type": "object" } }, "metadata": { "type": "object" }, "settings": { "type": "object" } } } } } } } }, "/api/v1/admin/pages/{id}": { "get": { "tags": [ "Admin · Pages" ], "summary": "Get a CMS page by id (full, incl. blocks)", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Page id." } ], "responses": { "200": { "description": "The page", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "patch": { "tags": [ "Admin · Pages" ], "summary": "Update a CMS page (title, status, blocks, metadata, settings)", "description": "slug is immutable; disabling protection is rejected here (use /unprotect).", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Page id." } ], "responses": { "200": { "description": "Updated page", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Validation error (slug immutable, invalid blocks, etc.)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Disabling protection requires /unprotect", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } } }, "delete": { "tags": [ "Admin · Pages" ], "summary": "Delete a CMS page (blocked if protected)", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Page id." } ], "responses": { "200": { "description": "Deleted (echoes the id)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeletedId" } } } }, "400": { "description": "Bad Request" }, "403": { "description": "Page is protected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/pages/{id}/preview": { "post": { "tags": [ "Admin · Pages" ], "summary": "Mint a 1h draft-preview link for a page", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Page id." } ], "responses": { "200": { "description": "Preview token + path", "content": { "application/json": { "schema": { "type": "object", "properties": { "token": { "type": "string" }, "expiresInSeconds": { "type": "integer" }, "path": { "type": "string" } } } } } }, "400": { "description": "Bad Request" }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/pages/{id}/unprotect": { "post": { "tags": [ "Admin · Pages" ], "summary": "Disable page protection (password step-up re-auth)", "description": "Verifies the current admin password server-side, then flips protected → false.", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Page id." } ], "responses": { "200": { "description": "Updated page (protected=false)", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Password incorrect", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "password": { "type": "string" } }, "required": [ "password" ] } } } } } }, "/api/v1/admin/posts": { "get": { "tags": [ "Admin · Posts" ], "summary": "List all posts (including unpublished)", "description": "", "parameters": [ { "name": "category", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Optional category filter." } ], "responses": { "200": { "description": "Posts", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Post" } } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "tags": [ "Admin · Posts" ], "summary": "Create a post", "description": "", "responses": { "201": { "description": "Created post", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Post" } } } }, "400": { "description": "Validation error or unknown category", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostCreateRequest" } } } } } }, "/api/v1/admin/posts/upload": { "post": { "tags": [ "Admin · Posts" ], "summary": "Upload a post image (multipart)", "description": "", "responses": { "201": { "description": "Stored image URL", "content": { "application/json": { "schema": { "type": "object", "properties": { "image_url": { "type": "string", "example": "/uploads/1700000000-abcd.png" } } } } } }, "400": { "description": "No image / disallowed type", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "multipart/form-data": { "schema": { "type": "object", "properties": { "image": { "type": "string", "format": "binary" } } } } } } } }, "/api/v1/admin/posts/{id}": { "get": { "tags": [ "Admin · Posts" ], "summary": "Get a post by id", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Post id." } ], "responses": { "200": { "description": "The post", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Post" } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "tags": [ "Admin · Posts" ], "summary": "Update a post", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Post id." } ], "responses": { "200": { "description": "Updated post", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Post" } } } }, "400": { "description": "Validation error or unknown category", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostCreateRequest" } } } } }, "delete": { "tags": [ "Admin · Posts" ], "summary": "Delete a post", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Post id." } ], "responses": { "200": { "description": "Deleted (echoes the id)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeletedId" } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/posts/{id}/announce": { "get": { "tags": [ "Admin · Posts" ], "summary": "Get the announcement pipeline status for a post", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Post id." } ], "responses": { "200": { "description": "The announce job for the post, or null if never announced", "content": { "application/json": { "schema": { "type": "object", "nullable": true, "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/posts/{id}/announce/retry": { "post": { "tags": [ "Admin · Posts" ], "summary": "Retry one announcement delivery leg", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Post id." } ], "responses": { "200": { "description": "Updated announce job", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "404": { "description": "No announcement job for this post", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "leg": { "type": "string", "description": "A registered delivery leg id, as returned by GET /announce." } }, "required": [ "leg" ] } } } } } }, "/api/v1/admin/posts/{id}/publish": { "patch": { "tags": [ "Admin · Posts" ], "summary": "Publish / unpublish a post", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Post id." } ], "responses": { "200": { "description": "Updated post", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Post" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublishRequest" } } } } } }, "/api/v1/admin/settings": { "get": { "tags": [ "Admin · Settings" ], "summary": "Get all site settings (admin only)", "description": "", "responses": { "200": { "description": "All settings", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "tags": [ "Admin · Settings" ], "summary": "Update site settings (admin only)", "description": "Writes the given keys. The JSON-valued theming keys (theme_visual, brand_assets, nav_public, nav_admin, nav_player) accept an object or its stringified form, are validated strictly with the offending field named in the 400, and are stored stringified with unusable fields dropped. Nav overrides key coded entries by their existing route and carry only label/order/hidden/group/section; whether a key names a route the nav declares is settled client-side at merge time. nav_public may additionally carry admin-created dropdown `sections` and admin-authored `links` — the only place an arbitrary path may be named, and therefore restricted to same-origin paths (no scheme, no protocol-relative host). Sections and links are dropped for the other two navs, which cannot render them.", "responses": { "200": { "description": "Updated settings", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Body must be an object of key/value settings", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true, "description": "An object of key/value settings." } } } } } }, "/api/v1/admin/settings/brand-asset/{slot}": { "post": { "tags": [ "Admin · Settings" ], "summary": "Upload a brand asset and set it as the override (admin only)", "description": "Stores the image and writes the brand_assets settings row in one call, so an upload never leaves an unreferenced file. Favicons must be PNG (max 512 KB); logos max 1 MB; heroes max 8 MB. Absent slots keep falling back to the BRAND_* env defaults — uploading a logo does not clear a hero.", "parameters": [ { "name": "slot", "in": "path", "required": true, "schema": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "logo", "hero", "favicon" ], "items": { "type": "string" } } } }, "description": "Which asset to replace" } ], "responses": { "201": { "description": "Stored file URL and the updated overrides", "content": { "application/json": { "schema": { "type": "object", "properties": { "url": { "type": "string", "example": "/uploads/1712345678901-ab12cd34.png" }, "brand_assets": { "type": "object", "properties": { "logo": { "type": "string" }, "hero": { "type": "string" }, "favicon": { "type": "string" } } } } } } } }, "400": { "description": "No file, unknown slot, disallowed type, or over the slot size cap", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "multipart/form-data": { "schema": { "type": "object", "properties": { "image": { "type": "string", "format": "binary" } } } } } } } }, "/api/v1/admin/settings/{key}": { "delete": { "tags": [ "Admin · Settings" ], "summary": "Reset one setting to its default (admin only)", "description": "Deletes the settings row so the surface falls back to its BRAND_* env / theme.css / hardcoded default. Restricted to the resettable keys (theme_visual, brand_assets, nav_public, nav_admin, nav_player, hero_layout_draft). Idempotent: resetting a key that was never set succeeds.", "parameters": [ { "name": "key", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Settings key to reset" } ], "responses": { "200": { "description": "Setting reset", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Message" } } } }, "400": { "description": "Setting is not resettable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/site-mode": { "put": { "tags": [ "Admin · Dashboard" ], "summary": "Set site mode (admin only)", "description": "Switch the site between live and maintenance.", "responses": { "200": { "description": "Updated site mode", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SiteModeState" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SiteModeRequest" } } } } } }, "/api/v1/admin/uploads": { "post": { "tags": [ "Admin · Posts" ], "summary": "Upload an image for rich-text editors (multipart)", "description": "", "responses": { "201": { "description": "Stored file URL", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UploadResponse" } } } }, "400": { "description": "No file / disallowed type", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "multipart/form-data": { "schema": { "type": "object", "properties": { "image": { "type": "string", "format": "binary" } } } } } } } }, "/api/v1/admin/users": { "get": { "tags": [ "Admin · Users" ], "summary": "List users (admin only)", "description": "", "responses": { "200": { "description": "Users", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/User" } } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "tags": [ "Admin · Users" ], "summary": "Create a user (admin only)", "description": "", "responses": { "201": { "description": "Created user", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/User" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Username already taken", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserCreateRequest" } } } } } }, "/api/v1/admin/users/{id}": { "put": { "tags": [ "Admin · Users" ], "summary": "Update a user (admin only)", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "User id." } ], "responses": { "200": { "description": "Updated user", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/User" } } } }, "400": { "description": "Validation error, or cannot demote the last admin", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Username already taken", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserCreateRequest" } } } } }, "delete": { "tags": [ "Admin · Users" ], "summary": "Delete a user (admin only)", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "User id." } ], "responses": { "200": { "description": "Deleted (echoes the id)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeletedId" } } } }, "400": { "description": "Cannot delete your own account or the last admin", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "get": { "tags": [ "Admin · Users" ], "summary": "Get a single user (admin only)", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "User id." } ], "responses": { "200": { "description": "The user", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/User" } } } }, "400": { "description": "Bad Request" }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/users/{id}/mfa/reset": { "post": { "tags": [ "Admin · Users" ], "summary": "Reset a user’s MFA (admin only)", "description": "Recovers a locked-out user: turns TOTP off, revokes every trusted device, and clears their recovery codes. The user can then sign in with their password alone and re-enroll.", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "User id." } ], "responses": { "200": { "description": "MFA reset", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OkFlag" } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/users/{id}/trusted-devices": { "get": { "tags": [ "Admin · Users" ], "summary": "List a user’s trusted devices (admin only)", "description": "Active (unrevoked, unexpired) trusted devices for the target user — the browsers/apps allowed to skip that user’s TOTP step. Never returns tokens.", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "User id." } ], "responses": { "200": { "description": "Trusted devices", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/TrustedDevice" } } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "delete": { "tags": [ "Admin · Users" ], "summary": "Revoke all of a user’s trusted devices (admin only)", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "User id." } ], "responses": { "200": { "description": "Revoked count", "content": { "application/json": { "schema": { "type": "object", "properties": { "revoked": { "type": "integer" } } } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/users/{id}/trusted-devices/{deviceId}": { "delete": { "tags": [ "Admin · Users" ], "summary": "Revoke one of a user’s trusted devices (admin only)", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "User id." }, { "name": "deviceId", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Trusted-device id." } ], "responses": { "200": { "description": "Revoked (idempotent)", "content": { "application/json": { "schema": { "type": "object", "properties": { "revoked": { "type": "boolean" } } } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/wiki": { "get": { "tags": [ "Admin · Wiki" ], "summary": "List all wiki pages (including unpublished)", "description": "", "parameters": [ { "name": "q", "in": "query", "schema": { "type": "string" } }, { "name": "category", "in": "query", "schema": { "type": "string" } }, { "name": "tag", "in": "query", "schema": { "type": "string" } }, { "name": "status", "in": "query", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Wiki pages", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/WikiPage" } } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "tags": [ "Admin · Wiki" ], "summary": "Create a wiki page", "description": "", "responses": { "201": { "description": "Created wiki page", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WikiPage" } } } }, "400": { "description": "Validation error or unknown category", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Slug already exists", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WikiPageCreateRequest" } } } } } }, "/api/v1/admin/wiki/categories": { "get": { "tags": [ "Admin · Wiki" ], "summary": "List wiki categories", "description": "", "responses": { "200": { "description": "Wiki categories", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/WikiCategory" } } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "tags": [ "Admin · Wiki" ], "summary": "Create a wiki category", "description": "", "responses": { "201": { "description": "Created category", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WikiCategory" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Slug already exists", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WikiCategoryCreateRequest" } } } } } }, "/api/v1/admin/wiki/categories/{id}": { "put": { "tags": [ "Admin · Wiki" ], "summary": "Update a wiki category", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Category id." } ], "responses": { "200": { "description": "Updated category", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WikiCategory" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Slug already exists", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WikiCategoryCreateRequest" } } } } }, "delete": { "tags": [ "Admin · Wiki" ], "summary": "Delete a wiki category", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Category id." } ], "responses": { "200": { "description": "Deleted (echoes the id)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeletedId" } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/wiki/tags": { "get": { "tags": [ "Admin · Wiki" ], "summary": "List wiki tags", "description": "", "responses": { "200": { "description": "Wiki tags", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string" } } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/wiki/{slug}": { "get": { "tags": [ "Admin · Wiki" ], "summary": "Get a wiki page by slug", "description": "", "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Wiki page slug." } ], "responses": { "200": { "description": "The wiki page", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WikiPage" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "tags": [ "Admin · Wiki" ], "summary": "Update a wiki page (creates a revision)", "description": "", "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Wiki page slug." } ], "responses": { "200": { "description": "Updated wiki page", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WikiPage" } } } }, "400": { "description": "Validation error or unknown category", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/WikiPageCreateRequest" }, { "type": "object", "properties": { "change_note": { "type": "string", "maxLength": 280 } } } ] } } } } }, "delete": { "tags": [ "Admin · Wiki" ], "summary": "Delete a wiki page", "description": "", "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Wiki page slug." } ], "responses": { "200": { "description": "Deleted (echoes the slug)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeletedSlug" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/wiki/{slug}/publish": { "patch": { "tags": [ "Admin · Wiki" ], "summary": "Publish / unpublish a wiki page", "description": "", "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Wiki page slug." } ], "responses": { "200": { "description": "Updated wiki page", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WikiPage" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublishRequest" } } } } } }, "/api/v1/admin/wiki/{slug}/revisions": { "get": { "tags": [ "Admin · Wiki" ], "summary": "List revisions of a wiki page", "description": "", "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Wiki page slug." } ], "responses": { "200": { "description": "Revisions", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/wiki/{slug}/revisions/{id}": { "get": { "tags": [ "Admin · Wiki" ], "summary": "Get a single wiki revision", "description": "", "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Wiki page slug." }, { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Revision id." } ], "responses": { "200": { "description": "The revision", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/wiki/{slug}/revisions/{id}/restore": { "post": { "tags": [ "Admin · Wiki" ], "summary": "Restore a wiki page to a revision", "description": "", "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Wiki page slug." }, { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Revision id to restore." } ], "responses": { "200": { "description": "Restored wiki page", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WikiPage" } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/auth/invite/{token}": { "get": { "tags": [ "Auth" ], "summary": "Look up an email invite by token", "description": "Returns the pre-assigned email + role for a valid, pending, unexpired invite so the accept form can render. 404 for anything not currently acceptable.", "parameters": [ { "name": "token", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Invite details", "content": { "application/json": { "schema": { "type": "object", "properties": { "email": { "type": "string" }, "role": { "type": "string" } } } } } }, "400": { "description": "Bad Request" }, "404": { "description": "Invalid or expired invite", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } } } }, "/api/v1/auth/invite/{token}/accept": { "post": { "tags": [ "Auth" ], "summary": "Accept an email invite (creates the account at the invited role)", "description": "Creates the website user at the invite’s pre-assigned role and logs them in (sets the session cookie). Bypasses the player_registration gate — the invite is its own authority. Rate limited + honeypot-guarded like registration.", "parameters": [ { "name": "token", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Account created and session issued", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LoginResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "404": { "description": "Invalid or expired invite", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Username taken or invite already used", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "username", "password" ], "properties": { "username": { "type": "string" }, "password": { "type": "string" } } } } } } } }, "/api/v1/auth/login": { "post": { "tags": [ "Auth" ], "summary": "Log in with username and password", "description": "On success sets the httpOnly session cookie. If the account has 2FA enabled, returns { totpRequired, challenge } instead and no cookie is set — complete login at POST /login/totp. Rate limited and behind bot/backoff guards.", "responses": { "200": { "description": "Session issued, or TOTP challenge required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LoginResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "401": { "description": "Incorrect username or password", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "429": { "description": "Too many attempts (rate limited / backoff)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LoginRequest" } } } } } }, "/api/v1/auth/login/totp": { "post": { "tags": [ "Auth" ], "summary": "Complete login with a TOTP or recovery code", "description": "Second step for 2FA accounts. Exchange the challenge from /login plus either the current authenticator code OR a single-use recovery code for a session cookie. Set trustDevice to remember this browser and skip TOTP on future logins (30 days); if the trusted-device limit is reached the session is still issued and the response carries { trustLimitReached, devices } so the user can revoke one first.", "responses": { "200": { "description": "Session issued (optionally with a trusted-device-limit prompt)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LoginResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "401": { "description": "Invalid code or expired challenge", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Too many attempts (rate limited / backoff)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TotpLoginRequest" } } } } } }, "/api/v1/auth/logout": { "post": { "tags": [ "Auth" ], "summary": "Log out (clear the cookie and revoke this session)", "description": "", "responses": { "200": { "description": "Logged out", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Message" } } } } } } }, "/api/v1/auth/me": { "get": { "tags": [ "Auth" ], "summary": "Current authenticated user", "description": "", "responses": { "200": { "description": "The signed-in user", "content": { "application/json": { "schema": { "type": "object", "properties": { "user": { "$ref": "#/components/schemas/User" } } } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/auth/me/account": { "get": { "tags": [ "Auth · Me" ], "summary": "Get the current account (self, any role)", "description": "", "responses": { "200": { "description": "The current account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerAccount" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Account not active", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/auth/me/account/identities": { "get": { "tags": [ "Auth · Me" ], "summary": "List linked SSO identities (self)", "description": "", "responses": { "200": { "description": "Linked identities", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/LinkedIdentity" } } } } }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/auth/me/account/identities/{provider}": { "delete": { "tags": [ "Auth · Me" ], "summary": "Unlink an SSO identity (self)", "description": "", "parameters": [ { "name": "provider", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Provider id." } ], "responses": { "200": { "description": "Unlinked", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UnlinkedFlag" } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" }, "404": { "description": "No linked account for that provider", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/auth/me/account/password": { "patch": { "tags": [ "Auth · Me" ], "summary": "Change or set the current account’s password (self, any role)", "description": "If the account already has a password, currentPassword is required and verified. SSO-provisioned accounts with no password may set an initial one without a current password. On success the caller’s own session is re-issued (they stay logged in) while older web sessions are revoked.", "responses": { "200": { "description": "Password changed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OkFlag" } } } }, "400": { "description": "Validation error or wrong current password", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "429": { "description": "Too many changes (rate limited)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChangePasswordRequest" } } } } } }, "/api/v1/auth/me/account/recovery-codes/generate": { "post": { "tags": [ "Auth · Me" ], "summary": "Regenerate recovery codes (self, password step-up)", "description": "Generates a fresh set of single-use recovery codes, invalidating any prior set, and returns them ONCE. Requires the current password (accounts that have one); refuses when two-factor is off. Behind the login backoff/bot guards since a wrong password is credential-guessing.", "responses": { "200": { "description": "New recovery codes (shown once)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RecoveryCodes" } } } }, "400": { "description": "Wrong password, or two-factor not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "429": { "description": "Too Many Requests" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "currentPassword": { "type": "string" } } } } } } } }, "/api/v1/auth/me/account/recovery-codes/status": { "get": { "tags": [ "Auth · Me" ], "summary": "Remaining recovery-code count (self)", "description": "", "responses": { "200": { "description": "Remaining unused codes", "content": { "application/json": { "schema": { "type": "object", "properties": { "remaining": { "type": "integer" } } } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/auth/me/account/totp/disable": { "post": { "tags": [ "Auth · Me" ], "summary": "Disable 2FA by confirming a code", "description": "Requires a valid current authenticator code (proves control of the authenticator); it does not take a password.", "responses": { "200": { "description": "2FA disabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TotpState" } } } }, "400": { "description": "Not enabled, or invalid code", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TotpCodeRequest" } } } } } }, "/api/v1/auth/me/account/totp/enable": { "post": { "tags": [ "Auth · Me" ], "summary": "Enable 2FA by confirming a code", "description": "", "responses": { "200": { "description": "2FA enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TotpState" } } } }, "400": { "description": "Setup not started, or invalid code", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" }, "409": { "description": "Two-factor already enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TotpCodeRequest" } } } } } }, "/api/v1/auth/me/account/totp/setup": { "post": { "tags": [ "Auth · Me" ], "summary": "Begin 2FA enrollment (returns secret + QR)", "description": "", "responses": { "200": { "description": "otpauth URL and QR data to scan", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TotpSetup" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "409": { "description": "Two-factor already enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/auth/me/account/username": { "patch": { "tags": [ "Auth · Me" ], "summary": "Change the current account’s username (self, any role)", "description": "", "responses": { "200": { "description": "Updated username (session re-issued)", "content": { "application/json": { "schema": { "type": "object", "properties": { "username": { "type": "string" } } } } } }, "400": { "description": "Validation error or unavailable username", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "409": { "description": "Username already taken", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Too many changes (rate limited)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChangeUsernameRequest" } } } } } }, "/api/v1/auth/me/devices": { "post": { "tags": [ "Auth · Me" ], "summary": "Register a push device (endpoint) for the current user", "description": "Registers a UnifiedPush/ntfy endpoint (or an FCM token) so the backend can deliver opt-in push tickles. The endpoint must be an allowed HTTPS relay URL — private/loopback hosts and non-allowed origins are rejected 400. Idempotent per (user, endpoint).", "responses": { "201": { "description": "Device registered", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PushDevice" } } } }, "400": { "description": "Validation error or disallowed endpoint", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RegisterDeviceRequest" } } } } }, "get": { "tags": [ "Auth · Me" ], "summary": "List the current user’s registered push devices", "description": "", "responses": { "200": { "description": "Registered devices", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/PushDevice" } } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/auth/me/devices/{id}": { "delete": { "tags": [ "Auth · Me" ], "summary": "Unregister a push device", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Device id (must belong to the caller)." } ], "responses": { "200": { "description": "Unregistered", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OkFlag" } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" }, "404": { "description": "No such device for this user", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/auth/me/notifications/streams": { "get": { "tags": [ "Auth · Me" ], "summary": "List subscribable notification streams (catalog)", "description": "The catalog of push streams. `personal`/`requiresLinkedAccount` streams are delivered only to the owning user and need a linked game account.", "responses": { "200": { "description": "Stream catalog", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotificationStreams" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/auth/me/notifications/subscriptions": { "get": { "tags": [ "Auth · Me" ], "summary": "Get the current user’s notification subscriptions", "description": "", "responses": { "200": { "description": "Subscribed stream ids", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotificationSubscriptions" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "tags": [ "Auth · Me" ], "summary": "Replace the current user’s notification subscriptions", "description": "Sets the full opted-in stream set (applied to all the user’s devices). Unknown stream ids are ignored; the stored set is echoed back.", "responses": { "200": { "description": "Updated subscriptions", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotificationSubscriptions" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotificationSubscriptions" } } } } } }, "/api/v1/auth/me/sessions": { "get": { "tags": [ "Auth · Me" ], "summary": "List active mobile device sessions (self)", "description": "Active (unrevoked, unexpired) mobile bearer sessions — one per live device — for the Active Devices screen. Never returns tokens.", "responses": { "200": { "description": "Active device sessions", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/DeviceSession" } } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/auth/me/sessions/{id}": { "delete": { "tags": [ "Auth · Me" ], "summary": "Revoke one mobile device session (self)", "description": "Revokes a single device by its session id (ownership-scoped). Revoking stops future token renewals; an already-issued access token remains valid until it expires (see the documented revocation-latency window).", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "The session row id from GET /auth/me/sessions." } ], "responses": { "200": { "description": "Revoked (idempotent)", "content": { "application/json": { "schema": { "type": "object", "properties": { "revoked": { "type": "boolean" } } } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/auth/me/trusted-devices": { "get": { "tags": [ "Auth · Me" ], "summary": "List trusted devices (self)", "description": "Active (unrevoked, unexpired) trusted devices — the browsers/apps allowed to skip the TOTP step at login. Never returns tokens.", "responses": { "200": { "description": "Active trusted devices", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/TrustedDevice" } } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "tags": [ "Auth · Me" ], "summary": "Trust the current device (self)", "description": "Marks the current browser/app as trusted so future logins skip the TOTP step (30 days). Web receives an httpOnly trust cookie; native to store. Returns 409 { error: \"trusted_device_limit\", devices } when the per-user cap is reached — revoke one first, then retry.", "responses": { "200": { "description": "Device trusted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TrustDeviceResult" } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "409": { "description": "Trusted-device limit reached", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TrustedDeviceLimit" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "deviceName": { "type": "string" } } } } } } }, "delete": { "tags": [ "Auth · Me" ], "summary": "Revoke all trusted devices (self)", "description": "Untrust every device; future logins on all of them require the full TOTP step again. Also clears this browser’s trust cookie.", "responses": { "200": { "description": "Revoked count", "content": { "application/json": { "schema": { "type": "object", "properties": { "revoked": { "type": "integer" } } } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/auth/me/trusted-devices/{id}": { "delete": { "tags": [ "Auth · Me" ], "summary": "Revoke one trusted device (self)", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Trusted-device id from GET /auth/me/trusted-devices." } ], "responses": { "200": { "description": "Revoked (idempotent)", "content": { "application/json": { "schema": { "type": "object", "properties": { "revoked": { "type": "boolean" } } } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/auth/mobile/login": { "post": { "tags": [ "Auth · Mobile" ], "summary": "Native login → access + refresh tokens", "description": "Bearer-token login for native clients. Single-request 2FA: if the account has TOTP on and no/invalid code is supplied, returns 401 { totpRequired: true } and the client retries with a code (or a single-use recoveryCode). A previously trusted device may present the X-Trust-Token header to skip the code entirely. Set trustDevice to remember this device (the response then carries trustToken to store); if the trusted-device limit is reached the tokens are still issued and the response carries { trustLimitReached, devices }.", "responses": { "200": { "description": "Access + refresh tokens (optionally with trustToken / a trusted-device-limit prompt)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MobileTokenResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "401": { "description": "Invalid credentials, or a TOTP code is required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Too many attempts (rate limited / backoff)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MobileLoginRequest" } } } } } }, "/api/v1/auth/mobile/logout": { "post": { "tags": [ "Auth · Mobile" ], "summary": "Revoke the current (or all) refresh tokens", "description": "Requires a valid bearer access token. Revokes the given refresh token, or every session for the user when { all: true }. Idempotent.", "responses": { "200": { "description": "Logged out", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Message" } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Missing or invalid bearer token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "bearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MobileLogoutRequest" } } } } } }, "/api/v1/auth/mobile/refresh": { "post": { "tags": [ "Auth · Mobile" ], "summary": "Rotate a refresh token for a fresh token pair", "description": "Refresh tokens are single-use: the presented token is revoked and a new access + refresh pair is issued. Reusing a rotated token fails with 401.", "responses": { "200": { "description": "New access + refresh tokens", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MobileTokenResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "401": { "description": "Invalid or expired session", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Too many refresh attempts", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MobileRefreshRequest" } } } } } }, "/api/v1/auth/mobile/sso/exchange": { "post": { "tags": [ "Auth · Mobile" ], "summary": "Exchange an SSO authorization code for mobile tokens", "description": "Redeems the single-use authorization code returned to the app callback, together with the PKCE code_verifier, for the SAME access + refresh pair as /auth/mobile/login. The code is single-use and PKCE-bound: a wrong verifier, an expired/used code, or a reused code all fail 401. If the user ticked \"trust this device\" on the TOTP form during this flow, the response also carries { trustToken } for the app to store and replay via X-Trust-Token — minted here rather than passed through the deep link so it never appears in a URL.", "responses": { "200": { "description": "Access + refresh tokens (optionally with a trustToken to persist)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MobileTokenResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "401": { "description": "Invalid/expired/used code or failed PKCE verification", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Too many attempts (rate limited)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MobileSsoExchangeRequest" } } } } } }, "/api/v1/auth/mobile/sso/start": { "get": { "tags": [ "Auth · Mobile" ], "summary": "Begin native SSO login (redirect to the IdP)", "description": "Opened by the Android app in a Custom Tab. Validates the provider is enabled and the redirect_uri is an exact match of a registered app callback, seeds a short-lived bridge session carrying the app PKCE challenge + state, and 302-redirects into the existing website SSO flow. On success the callback redirects to `redirect_uri?code=…&state=…` (a one-time code, never a token). Errors are surfaced to the app as `redirect_uri?error=…&state=…`.", "parameters": [ { "name": "provider", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Provider id from GET /auth/providers (e.g. google, discord)." }, { "name": "code_challenge", "in": "query", "required": true, "schema": { "type": "string" }, "description": "App-generated PKCE S256 challenge (base64url)." }, { "name": "state", "in": "query", "required": true, "schema": { "type": "string" }, "description": "App-generated opaque CSRF value, echoed on the callback for the app to verify." }, { "name": "redirect_uri", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The app callback; must EXACTLY match a registered value (default runicgateway://auth/callback)." } ], "responses": { "302": { "description": "Redirect to the identity provider (or back to the app callback on error)" }, "400": { "description": "Unrecognized redirect URI or validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Too many attempts (rate limited)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/auth/password/forgot": { "post": { "tags": [ "Auth" ], "summary": "Request a password-reset link by email", "description": "Emails a single-use, ~1h reset link to every active account on the address. Always returns the same generic 200 whether or not the email matches (no account enumeration). Email is non-unique, so multiple accounts may each receive a link naming their username. Rate limited per IP.", "responses": { "200": { "description": "Generic acknowledgement (sent if the account exists)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Message" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "429": { "description": "Too many requests", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "email" ], "properties": { "email": { "type": "string", "format": "email" } } } } } } } }, "/api/v1/auth/password/reset/{token}": { "get": { "tags": [ "Auth" ], "summary": "Validate a password-reset link", "description": "Returns the target username for a valid, pending, unexpired reset link so the reset form can render. 404 for anything not currently usable (never distinguishes expired from used from never-existed).", "parameters": [ { "name": "token", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Reset link is valid", "content": { "application/json": { "schema": { "type": "object", "properties": { "username": { "type": "string" } } } } } }, "400": { "description": "Bad Request" }, "404": { "description": "Invalid or expired reset link", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } } }, "post": { "tags": [ "Auth" ], "summary": "Set a new password from a reset link", "description": "Consumes the single-use link and sets the new password. Rotates the hash and revokes every existing session (web + mobile). Does NOT sign the user in — they log in fresh afterwards (so a 2FA account still passes TOTP). Rate limited per IP.", "parameters": [ { "name": "token", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Password changed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Message" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "404": { "description": "Invalid, expired, or already-used reset link", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Too many attempts", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "password" ], "properties": { "password": { "type": "string", "minLength": 8, "maxLength": 64 } } } } } } } }, "/api/v1/auth/providers": { "get": { "tags": [ "Auth · SSO" ], "summary": "List enabled SSO providers", "description": "Public discovery used by the login page to render provider buttons. Never exposes secrets.", "responses": { "200": { "description": "Enabled, valid providers", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Provider" } } } } }, "500": { "description": "Internal Server Error" } } } }, "/api/v1/auth/register": { "post": { "tags": [ "Auth" ], "summary": "Register a player account", "description": "Creates a self-service player account and logs it in (sets the session cookie). Available only when an admin has enabled password registration (player_registration = password|both); otherwise returns 403. Rate limited and behind bot/backoff guards; a hidden honeypot field must stay empty.", "responses": { "200": { "description": "Account created and session issued", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LoginResponse" } } } }, "400": { "description": "Validation error or unavailable username", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "403": { "description": "Registration is not open", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Username already taken", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Too many attempts (rate limited / backoff)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RegisterRequest" } } } } } }, "/api/v1/auth/sso/totp": { "post": { "tags": [ "Auth · SSO" ], "summary": "Complete an SSO login with a TOTP code", "description": "Second step when a linked account has 2FA enabled. Reads the staged pending-TOTP cookie set by the callback plus the current authenticator code, and on success sets the session cookie. Set trustDevice to remember this browser and skip TOTP on future SSO sign-ins (30 days) — on the mobile flow this browser is the app Custom Tab, and the app additionally receives its own trustToken at /auth/mobile/sso/exchange. If the trusted-device limit is reached the sign-in still completes and the response carries { trustLimitReached, devices }. Rate limited and behind bot/backoff guards.", "responses": { "200": { "description": "Session issued (web), or a deep link to redeem (mobile bridge); optionally with a trusted-device-limit prompt", "content": { "application/json": { "schema": { "type": "object", "properties": { "user": { "$ref": "#/components/schemas/SafeUser" }, "returnTo": { "type": "string" }, "redirect": { "type": "string" }, "trustLimitReached": { "type": "boolean" }, "devices": { "type": "array", "items": { "$ref": "#/components/schemas/TrustedDevice" } } } } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Invalid code or expired challenge", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "409": { "description": "Conflict" }, "429": { "description": "Too many attempts (rate limited / backoff)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string" }, "trustDevice": { "type": "boolean" }, "deviceName": { "type": "string" } } } } } } } }, "/api/v1/auth/sso/{provider}/callback": { "get": { "tags": [ "Auth · SSO" ], "summary": "OAuth redirect target — completes login or linking", "description": "The provider redirects here with code + state. On success sets the session cookie (login) or links the identity (link), then 302-redirects into /admin. Login is link-only: unknown identities are refused (sso_error=not_linked).", "parameters": [ { "name": "provider", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Provider id (e.g. google, discord)." }, { "name": "error", "in": "query", "schema": { "type": "string" } }, { "name": "code", "in": "query", "required": false, "schema": { "type": "string" }, "description": "OAuth authorization code." }, { "name": "state", "in": "query", "required": false, "schema": { "type": "string" }, "description": "OAuth state (matched against the tx cookie)." } ], "responses": { "302": { "description": "Redirect into /admin on success, or back to login/account with an error code" } } } }, "/api/v1/auth/sso/{provider}/link": { "get": { "tags": [ "Auth · SSO" ], "summary": "Begin linking an SSO identity to the current account", "description": "Requires an authenticated session; the signed transaction captures the acting user so the callback can attach the external identity.", "parameters": [ { "name": "provider", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Provider id (e.g. google, discord)." } ], "responses": { "302": { "description": "Redirect to the identity provider (or back to the account page on error)" }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/auth/sso/{provider}/start": { "get": { "tags": [ "Auth · SSO" ], "summary": "Begin SSO login (redirect to the IdP)", "description": "Sets a short-lived signed transaction cookie and 302-redirects to the provider authorize URL. On error redirects back to the login page with an sso_error query param.", "parameters": [ { "name": "provider", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Provider id (e.g. google, discord)." }, { "name": "returnTo", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Internal /admin path to return to after login." } ], "responses": { "302": { "description": "Redirect to the identity provider (or back to the login page on error)" } } } }, "/api/v1/player/account": { "get": { "tags": [ "Player" ], "summary": "Get the current player account (self)", "description": "", "responses": { "200": { "description": "The player account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerAccount" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Account not active (disabled/banned)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/player/account/identities": { "get": { "tags": [ "Player" ], "summary": "List linked SSO identities (self)", "description": "", "responses": { "200": { "description": "Linked identities", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/LinkedIdentity" } } } } }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/player/account/identities/{provider}": { "delete": { "tags": [ "Player" ], "summary": "Unlink an SSO identity (self)", "description": "", "parameters": [ { "name": "provider", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Provider id." } ], "responses": { "200": { "description": "Unlinked", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UnlinkedFlag" } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" }, "404": { "description": "No linked account for that provider", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/player/account/password": { "patch": { "tags": [ "Player" ], "summary": "Change or set the current player’s password", "description": "If the account already has a password, currentPassword is required and verified. SSO-provisioned accounts with no password may set an initial one without a current password. On success the caller’s session is re-issued (they stay logged in) while all other sessions are revoked.", "responses": { "200": { "description": "Password changed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OkFlag" } } } }, "400": { "description": "Validation error or wrong current password", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized" }, "403": { "description": "Account not active (disabled/banned)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Too many changes (rate limited)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChangePasswordRequest" } } } } } }, "/api/v1/player/account/totp/disable": { "post": { "tags": [ "Player" ], "summary": "Disable 2FA by confirming a code", "description": "Requires a valid current authenticator code (proves control of the authenticator); it does not take a password.", "responses": { "200": { "description": "2FA disabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TotpState" } } } }, "400": { "description": "Not enabled, or invalid code", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TotpCodeRequest" } } } } } }, "/api/v1/player/account/totp/enable": { "post": { "tags": [ "Player" ], "summary": "Enable 2FA by confirming a code", "description": "", "responses": { "200": { "description": "2FA enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TotpState" } } } }, "400": { "description": "Setup not started, or invalid code", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" }, "409": { "description": "Two-factor already enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TotpCodeRequest" } } } } } }, "/api/v1/player/account/totp/setup": { "post": { "tags": [ "Player" ], "summary": "Begin 2FA enrollment (returns secret + QR)", "description": "", "responses": { "200": { "description": "otpauth URL and QR data to scan", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TotpSetup" } } } }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" }, "409": { "description": "Two-factor already enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/player/account/username": { "patch": { "tags": [ "Player" ], "summary": "Change the current player’s username", "description": "", "responses": { "200": { "description": "Updated username (session cookie re-issued)", "content": { "application/json": { "schema": { "type": "object", "properties": { "username": { "type": "string" } } } } } }, "400": { "description": "Validation error or unavailable username", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "401": { "description": "Unauthorized" }, "403": { "description": "Account not active (disabled/banned)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Username already taken", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Too many changes (rate limited)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChangeUsernameRequest" } } } } } }, "/api/v1/player/appeals": { "get": { "tags": [ "Player · Appeals" ], "summary": "List the caller’s moderation appeals", "description": "", "responses": { "200": { "description": "The caller’s appeals", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Appeal" } } } } }, "401": { "description": "Unauthorized" }, "403": { "description": "Account not active (disabled/banned)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "tags": [ "Player · Appeals" ], "summary": "Submit a moderation appeal for one of the caller’s actions", "description": "Opens an appeal for a ban/mute mod_action that belongs to the caller (its target matches the caller’s linked Discord identity) and has no active appeal.", "responses": { "201": { "description": "Appeal created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Appeal" } } } }, "400": { "description": "Validation error, or the action type is not appealable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "401": { "description": "Unauthorized" }, "403": { "description": "The action does not belong to the caller", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Mod action not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "An appeal for this action is already open", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateAppealRequest" } } } } } }, "/api/v1/player/appeals/eligible": { "get": { "tags": [ "Player · Appeals" ], "summary": "List the caller’s ban/mute actions eligible for appeal", "description": "The caller’s ban/mute mod_actions that have no active appeal. Returns an empty array when the caller has no linked Discord account (the UI shows a “link Discord” hint).", "responses": { "200": { "description": "Appealable actions", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/AppealEligibleAction" } } } } }, "401": { "description": "Unauthorized" }, "403": { "description": "Account not active (disabled/banned)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/player/appeals/{id}/withdraw": { "post": { "tags": [ "Player · Appeals" ], "summary": "Withdraw one of the caller’s pending appeals", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Appeal id (must belong to the caller)." } ], "responses": { "200": { "description": "The withdrawn appeal", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Appeal" } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" }, "404": { "description": "No such appeal for the caller", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Appeal is already resolved", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/public/contact": { "post": { "tags": [ "Public" ], "summary": "Send a contact message", "description": "Emails the site owner (or falls back to a mailto). Rate limited.", "responses": { "200": { "description": "Message sent", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Message" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "429": { "description": "Too many messages (rate limited)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "502": { "description": "Mail delivery failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContactRequest" } } } } } }, "/api/v1/public/modules": { "get": { "tags": [ "Public" ], "summary": "Installed modules (id, version, capabilities)", "description": "The modules this backend is currently SERVING, in scan order. A module that is disabled or failed to load is absent rather than listed with a state — its routes and nav are absent too, so the client renders a site without that capability. `capabilities` are opaque strings declared by the module for clients (the SPA, the Android app) to feature-detect against; treat an unknown one as absent. Database-free and never gated by site mode, so a client can feature-detect during maintenance.", "responses": { "200": { "description": "The started modules", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicModules" } } } }, "500": { "description": "Internal Server Error" } } } }, "/api/v1/public/pages/{id}/preview/{token}": { "get": { "tags": [ "Public" ], "summary": "Render a page from a draft-preview token", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Page id." }, { "name": "token", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Preview token from POST /admin/pages/:id/preview." } ], "responses": { "200": { "description": "The page (any status)", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "404": { "description": "Token invalid/expired or page missing", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } } } }, "/api/v1/public/pages/{slug}": { "get": { "tags": [ "Public" ], "summary": "Get a published CMS page by slug", "description": "Drafts 404 for the public; staff sessions see drafts. Gated by site mode.", "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Page slug." } ], "responses": { "200": { "description": "The page", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" }, "503": { "description": "Service Unavailable" } } } }, "/api/v1/public/posts/{category}": { "get": { "tags": [ "Public" ], "summary": "List published posts in a category", "description": "Gated by site mode: during maintenance only admins with a valid session see content.", "parameters": [ { "name": "category", "in": "path", "required": true, "schema": { "type": "string" }, "description": "news | five-on-friday | newsletter | screenshots" } ], "responses": { "200": { "description": "Published posts", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Post" } } } } }, "404": { "description": "Unknown category", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" }, "503": { "description": "Service Unavailable" } } } }, "/api/v1/public/posts/{category}/{idOrSlug}": { "get": { "tags": [ "Public" ], "summary": "Get a single published post", "description": "", "parameters": [ { "name": "category", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Post category." }, { "name": "idOrSlug", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Numeric id or slug." } ], "responses": { "200": { "description": "The post", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Post" } } } }, "404": { "description": "Unknown category or post not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" }, "503": { "description": "Service Unavailable" } } } }, "/api/v1/public/settings": { "get": { "tags": [ "Public" ], "summary": "Public site settings + branding", "description": "Whitelisted, non-sensitive settings plus the per-shard brand block (name/colors/logo/hero/favicon) a client themes itself from, and derived registration / game-account-signup availability flags.", "responses": { "200": { "description": "Public settings + branding", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicSettings" } } } }, "500": { "description": "Internal Server Error" } } } }, "/api/v1/public/status": { "get": { "tags": [ "Public" ], "summary": "Site mode / status", "description": "Current site mode (live or maintenance) so the client can show the maintenance page, plus a version block (service id + API/server versions) for a client first-run probe and version-mismatch guard.", "responses": { "200": { "description": "Site status", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicStatus" } } } }, "500": { "description": "Internal Server Error" } } } }, "/api/v1/public/version": { "get": { "tags": [ "Public" ], "summary": "Backend identity + version", "description": "Lightweight, DB-free descriptor of this backend: a stable service id and the API/server versions. A client uses it to recognize a Runic Gateway backend on first-run and to run a version-mismatch guard. Doubles as a cheap liveness check.", "responses": { "200": { "description": "Backend version", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicVersion" } } } } } } }, "/api/v1/public/wiki": { "get": { "tags": [ "Public" ], "summary": "List published wiki pages", "description": "", "parameters": [ { "name": "q", "in": "query", "schema": { "type": "string" } }, { "name": "category", "in": "query", "schema": { "type": "string" } }, { "name": "tag", "in": "query", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Published wiki pages", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/WikiPage" } } } } }, "500": { "description": "Internal Server Error" }, "503": { "description": "Service Unavailable" } } } }, "/api/v1/public/wiki/categories": { "get": { "tags": [ "Public" ], "summary": "List wiki categories", "description": "", "responses": { "200": { "description": "Wiki categories", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/WikiCategory" } } } } }, "500": { "description": "Internal Server Error" }, "503": { "description": "Service Unavailable" } } } }, "/api/v1/public/wiki/tags": { "get": { "tags": [ "Public" ], "summary": "List wiki tags", "description": "", "responses": { "200": { "description": "Wiki tags", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string" } } } } }, "500": { "description": "Internal Server Error" }, "503": { "description": "Service Unavailable" } } } }, "/api/v1/public/wiki/{slug}": { "get": { "tags": [ "Public" ], "summary": "Get a single published wiki page", "description": "", "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Wiki page slug." } ], "responses": { "200": { "description": "The wiki page", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WikiPage" } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" }, "503": { "description": "Service Unavailable" } } } }, "/api/v1/settings/nav": { "get": { "tags": [ "Settings" ], "summary": "Nav overrides for the admin and player layouts", "description": "Returns the stored nav_admin and nav_player overrides as raw JSON strings (null when the admin never overrode that nav). Any authenticated account may read them: AdminLayout renders for editors and moderators, PlayerPortalLayout for players, and none of them can read GET /admin/settings. Presentation-only — the role/feature filters in the layouts still decide what is actually shown.", "responses": { "200": { "description": "Nav overrides", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NavSettings" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/settings/theme/options": { "get": { "tags": [ "Settings" ], "summary": "Theme presets and the curated option lists", "description": "The closed sets an admin may choose from when theming the site: the three presets (with swatch colors), the curated Google Fonts shortlist per role, the shadow depths, and the editable color/radius field names. Served so the admin form can never offer a value the server would reject. Static — no database read.", "responses": { "200": { "description": "Theme option catalog", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ThemeOptions" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } } }, "components": { "securitySchemes": { "cookieAuth": { "type": "apiKey", "in": "cookie", "name": "rg_token", "description": "Session JWT set as an httpOnly cookie by POST /api/v1/auth/login." }, "bearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT", "description": "Access token from POST /api/v1/auth/mobile/login (or /refresh)." } }, "schemas": { "Error": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "message": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Not found" } } } } } } }, "ValidationError": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "errors": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "type": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "field" } } }, "msg": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Invalid value" } } }, "path": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "username" } } }, "location": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "body" } } } } } } } } } } } } }, "SafeUser": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 1 } } }, "username": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "admin" } } }, "role": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "admin", "editor" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "admin" } } } } } } }, "LoginRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "username", "password" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "username": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "admin" } } }, "password": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "password" }, "example": { "type": "string", "example": "super-secret" } } }, "company": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "Honeypot — must be empty for humans." }, "example": { "type": "string", "example": "" } } } } } } }, "RegisterRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "username", "password" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "username": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "minLength": { "type": "number", "example": 3 }, "maxLength": { "type": "number", "example": 32 }, "example": { "type": "string", "example": "newplayer" } } }, "password": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "password" }, "minLength": { "type": "number", "example": 8 }, "maxLength": { "type": "number", "example": 64 } } }, "email": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "email" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "player@example.com" } } }, "company": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "Honeypot — must be empty for humans." }, "example": { "type": "string", "example": "" } } } } } } }, "LoginResponse": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "Either a session (user) or, for 2FA accounts, a TOTP challenge to complete at /login/totp." }, "properties": { "type": "object", "properties": { "user": { "$ref": "#/components/schemas/SafeUser" }, "totpRequired": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } }, "challenge": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "Signed challenge token for the TOTP step." } } } } } } }, "TotpLoginRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "challenge" ], "items": { "type": "string" } }, "description": { "type": "string", "example": "Second step for 2FA login. Supply either code OR recoveryCode." }, "properties": { "type": "object", "properties": { "challenge": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "Token returned by /login when totpRequired." } } }, "code": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "Current authenticator code." }, "example": { "type": "string", "example": "123456" } } }, "recoveryCode": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "A single-use recovery code (alternative to code)." }, "example": { "type": "string", "example": "abcde-12345" } } }, "trustDevice": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "description": { "type": "string", "example": "Remember this browser so future logins skip the TOTP step (30 days)." }, "example": { "type": "boolean", "example": false } } }, "deviceName": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "Optional friendly label for the Trusted Devices list." }, "example": { "type": "string", "example": "My Laptop" } } } } } } }, "MobileLoginRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "username", "password" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "username": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "admin" } } }, "password": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "password" }, "example": { "type": "string", "example": "super-secret" } } }, "code": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "TOTP code (only when 2FA is enabled)." }, "example": { "type": "string", "example": "123456" } } }, "recoveryCode": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "Single-use recovery code (alternative to code)." }, "example": { "type": "string", "example": "abcde-12345" } } }, "trustDevice": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "description": { "type": "string", "example": "Remember this device so future logins skip the TOTP step; the response then carries trustToken." }, "example": { "type": "boolean", "example": false } } }, "device_name": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "Optional friendly device label for Active/Trusted Devices." }, "example": { "type": "string", "example": "Pixel 8" } } } } } } }, "MobileTokenResponse": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "accessToken": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "Short-lived bearer JWT." } } }, "refreshToken": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "Long-lived, revocable refresh token." } } }, "expiresIn": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "Access token lifetime as a duration string (zeit/ms format, e.g. \"15m\")." }, "example": { "type": "string", "example": "15m" } } }, "user": { "$ref": "#/components/schemas/SafeUser" }, "trustToken": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "description": { "type": "string", "example": "Present only when trustDevice was requested and accepted — store securely and send as X-Trust-Token on future logins to skip TOTP." } } }, "trustLimitReached": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "nullable": { "type": "boolean", "example": true }, "description": { "type": "string", "example": "Present (true) when trustDevice was requested but the device cap is reached; see devices." } } }, "devices": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "nullable": { "type": "boolean", "example": true }, "items": { "$ref": "#/components/schemas/TrustedDevice" }, "description": { "type": "string", "example": "The existing trusted devices, when trustLimitReached is set." } } } } } } }, "MobileRefreshRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "refreshToken" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "refreshToken": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } } } } } }, "MobileLogoutRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "refreshToken": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "Revoke a single session." } } }, "all": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "description": { "type": "string", "example": "Revoke every session for the user." }, "example": { "type": "boolean", "example": false } } } } } } }, "MobileSsoExchangeRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "code", "code_verifier" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "code": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "The single-use authorization code returned to the app callback." } } }, "code_verifier": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "The PKCE verifier for the challenge sent to /auth/mobile/sso/start." } } }, "device_name": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "Optional friendly device label for Active Devices." }, "example": { "type": "string", "example": "Pixel 8" } } } } } } }, "DeviceSession": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "description": { "type": "string", "example": "Session row id (pass to DELETE /auth/me/sessions/:id)." } } }, "deviceName": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "Pixel 8" } } }, "userAgent": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true } } }, "createdAt": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" } } }, "lastUsedAt": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" } } }, "expiresAt": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" } } } } } } }, "TrustedDevice": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "description": { "type": "string", "example": "Trusted-device id (pass to DELETE …/trusted-devices/:id)." } } }, "platform": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "web", "mobile" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "web" } } }, "deviceName": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "My Laptop" } } }, "userAgent": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true } } }, "createdAt": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" } } }, "lastUsedAt": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" } } }, "expiresAt": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" } } } } } } }, "TrustDeviceResult": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "trusted": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } }, "trustToken": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "description": { "type": "string", "example": "Native clients only — store securely and send as X-Trust-Token." } } } } } } }, "TrustedDeviceLimit": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "error": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "trusted_device_limit" } } }, "devices": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "items": { "$ref": "#/components/schemas/TrustedDevice" } } } } } } }, "RecoveryCodes": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "recoveryCodes": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "abcde-12345" } } } } } } } } }, "Message": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "message": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Logged out." } } } } } } }, "ContactRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "message" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "message": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "maxLength": { "type": "number", "example": 5000 }, "example": { "type": "string", "example": "When does the shard launch?" } } }, "email": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "email" }, "example": { "type": "string", "example": "player@example.com" } } }, "name": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "maxLength": { "type": "number", "example": 100 }, "example": { "type": "string", "example": "Lord British" } } } } } } }, "Provider": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "google" } } }, "name": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Google" } } }, "icon": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "Icon hint — the provider kind ('google' | 'discord' | 'oidc' | 'oauth2')." }, "example": { "type": "string", "example": "google" } } }, "loginUrl": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "Relative URL to begin the redirect flow." }, "example": { "type": "string", "example": "/api/v1/auth/sso/google/start" } } }, "priority": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "description": { "type": "string", "example": "Sort order (ascending)." }, "example": { "type": "number", "example": 1 } } } } } } }, "ProviderConfig": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "okta" } } }, "kind": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "google", "discord", "oidc", "oauth2" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "oidc" } } }, "name": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Okta" } } }, "enabled": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } }, "clientId": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } }, "hasSecret": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "description": { "type": "string", "example": "Whether a client secret is stored (the secret itself is never returned)." }, "example": { "type": "boolean", "example": true } } }, "authorizeUrl": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "uri" } } }, "tokenUrl": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "uri" } } }, "userinfoUrl": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "uri" } } }, "scopes": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "openid email profile" } } }, "priority": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 10 } } }, "builtin": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "description": { "type": "string", "example": "True for the fixed google/discord providers." }, "example": { "type": "boolean", "example": false } } }, "health": { "$ref": "#/components/schemas/ProviderHealth" } } } } }, "ProviderHealth": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "Config-completeness check that gates whether a provider is offered to end users." }, "properties": { "type": "object", "properties": { "valid": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } }, "missing": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "description": { "type": "string", "example": "Names of required config fields that are still missing." }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } }, "example": { "type": "array", "example": [], "items": {} } } } } } } }, "ProviderCreateRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "id", "kind", "name" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "pattern": { "type": "string", "example": "^[a-z0-9-]+$" }, "example": { "type": "string", "example": "okta" } } }, "kind": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "oidc", "oauth2" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "oidc" } } }, "name": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "maxLength": { "type": "number", "example": 80 }, "example": { "type": "string", "example": "Okta" } } }, "enabled": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } }, "clientId": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } }, "secret": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "password" } } }, "authorizeUrl": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "uri" } } }, "tokenUrl": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "uri" } } }, "userinfoUrl": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "uri" } } }, "scopes": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "maxLength": { "type": "number", "example": 500 }, "example": { "type": "string", "example": "openid email profile" } } }, "priority": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 10 } } } } } } }, "Post": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 12 } } }, "category": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "news" } } }, "title": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Server maintenance this weekend" } } }, "slug": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "server-maintenance-this-weekend" } } }, "excerpt": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true } } }, "body": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true } } }, "image_url": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "/uploads/1700000000-abcd.png" } } }, "published": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } }, "author_id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "number", "example": 1 } } }, "created_at": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" } } }, "updated_at": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" } } }, "published_at": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" }, "nullable": { "type": "boolean", "example": true } } } } } } }, "PostCreateRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "category", "title" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "category": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "news" } } }, "title": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "maxLength": { "type": "number", "example": 200 }, "example": { "type": "string", "example": "Server maintenance this weekend" } } }, "body": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } }, "image_url": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "Required for the screenshots category." } } }, "published": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": false } } } } } } }, "PublishRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "published" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "published": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } } } } } }, "UploadResponse": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "url": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "/uploads/1700000000-abcd.png" } } } } } } }, "WikiPage": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 3 } } }, "slug": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "getting-started" } } }, "title": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Getting Started" } } }, "excerpt": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } }, "body": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } }, "category_id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "number", "example": 2 } } }, "published": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } }, "tags": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } }, "example": { "type": "array", "example": [ "newbie", "guide" ], "items": { "type": "string" } } } }, "created_at": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" } } }, "updated_at": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" } } } } } } }, "WikiPageCreateRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "slug", "title" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "slug": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "pattern": { "type": "string", "example": "^[a-z0-9-]+$" }, "example": { "type": "string", "example": "getting-started" } } }, "title": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "maxLength": { "type": "number", "example": 200 }, "example": { "type": "string", "example": "Getting Started" } } }, "excerpt": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "maxLength": { "type": "number", "example": 400 } } }, "body": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } }, "category_id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "nullable": { "type": "boolean", "example": true } } }, "published": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": false } } }, "tags": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } } } } } } } }, "WikiCategory": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 2 } } }, "slug": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "guides" } } }, "title": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Guides" } } }, "description": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } }, "sort_order": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 0 } } } } } } }, "WikiCategoryCreateRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "slug", "title" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "slug": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "pattern": { "type": "string", "example": "^[a-z0-9-]+$" }, "example": { "type": "string", "example": "guides" } } }, "title": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "maxLength": { "type": "number", "example": 200 }, "example": { "type": "string", "example": "Guides" } } }, "description": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "maxLength": { "type": "number", "example": 400 } } }, "sort_order": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 0 } } } } } } }, "User": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 1 } } }, "username": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "admin" } } }, "role": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "admin", "editor", "moderator", "player" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "admin" } } }, "status": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "active", "disabled", "banned", "pending" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "active" } } }, "email": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "email" }, "nullable": { "type": "boolean", "example": true } } }, "email_verified": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": false } } }, "totp_enabled": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } }, "last_login_at": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" }, "nullable": { "type": "boolean", "example": true } } }, "created_at": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" } } } } } } }, "UserCreateRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "username", "password" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "username": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "minLength": { "type": "number", "example": 3 }, "maxLength": { "type": "number", "example": 32 }, "example": { "type": "string", "example": "editor1" } } }, "password": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "password" }, "minLength": { "type": "number", "example": 8 }, "maxLength": { "type": "number", "example": 64 } } }, "role": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "admin", "editor", "moderator", "player" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "editor" } } }, "status": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "active", "disabled", "banned", "pending" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "active" } } }, "email": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "email" }, "nullable": { "type": "boolean", "example": true } } } } } } }, "ChangeUsernameRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "username" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "username": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "minLength": { "type": "number", "example": 3 }, "maxLength": { "type": "number", "example": 32 }, "example": { "type": "string", "example": "newname" } } } } } } }, "ChangePasswordRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "newPassword" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "newPassword": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "password" }, "minLength": { "type": "number", "example": 8 }, "maxLength": { "type": "number", "example": 64 } } }, "currentPassword": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "password" }, "description": { "type": "string", "example": "Required when the account already has a password. Omit only for an SSO-provisioned account setting its first password." } } } } } } }, "PlayerAccount": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "Self-service player account (GET /player/account)." }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 42 } } }, "username": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "newplayer" } } }, "role": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "player" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "player" } } }, "email": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "email" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "player@example.com" } } }, "status": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "active", "disabled", "banned", "pending" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "active" } } }, "totp_enabled": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": false } } }, "has_password": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "description": { "type": "string", "example": "False for an SSO-provisioned account that has not set a password yet." }, "example": { "type": "boolean", "example": true } } } } } } }, "OkFlag": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "ok": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } } } } } }, "RegisterDeviceRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "endpoint" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "endpoint": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "uri" }, "description": { "type": "string", "example": "The UnifiedPush/ntfy endpoint URL the distributor handed the app (or an FCM token). Must be an allowed HTTPS relay origin — private/loopback hosts are rejected." }, "example": { "type": "string", "example": "https://ntfy.example.com/UP0a1b2c3d4e5f" } } }, "transport": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "unifiedpush", "fcm" ], "items": { "type": "string" } }, "default": { "type": "string", "example": "unifiedpush" }, "example": { "type": "string", "example": "unifiedpush" } } }, "platform": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "maxLength": { "type": "number", "example": 40 }, "example": { "type": "string", "example": "android" } } } } } } }, "PushDevice": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 7 } } }, "transport": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "unifiedpush", "fcm" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "unifiedpush" } } }, "endpoint": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "https://ntfy.example.com/UP0a1b2c3d4e5f" } } }, "platform": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "android" } } }, "createdAt": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" } } }, "lastSeenAt": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" } } } } } } }, "NotificationStream": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "One subscribable push stream from the catalog." }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "idoc.warning" } } }, "label": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "IDOC warnings" } } }, "description": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "A house falls into its final (IDOC) decay stage." } } }, "personal": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "description": { "type": "string", "example": "Owner-keyed — delivered only to the owning user, never fanned out publicly." }, "example": { "type": "boolean", "example": false } } }, "requiresLinkedAccount": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "description": { "type": "string", "example": "The stream needs a linked game account (personal streams)." }, "example": { "type": "boolean", "example": false } } } } } } }, "NotificationStreams": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "streams": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "items": { "$ref": "#/components/schemas/NotificationStream" } } } } } } }, "NotificationSubscriptions": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "The set of stream ids the user has opted into (used for both GET and PUT)." }, "properties": { "type": "object", "properties": { "streams": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } }, "example": { "type": "array", "example": [ "news.post", "idoc.warning", "vendor.sale" ], "items": { "type": "string" } } } } } } } }, "Appeal": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "A player-submitted moderation appeal (as returned to the player and in the staff queue)." }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 12 } } }, "mod_action_id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 340 } } }, "discord_user_id": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "216734083584917504" } } }, "action_type": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "ban", "mute" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "ban" } } }, "user_id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "number", "example": 42 } } }, "status": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "pending", "under_review", "approved", "denied", "withdrawn" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "pending" } } }, "submitted_text": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "I was banned by mistake — please review." } } }, "staff_response": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": {} } }, "handled_by_user_id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "nullable": { "type": "boolean", "example": true }, "example": {} } }, "handled_by_tag": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": {} } }, "reversal_status": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "none", "done", "failed" ], "items": { "type": "string" } }, "description": { "type": "string", "example": "Discord-reversal outcome. done/failed only after an approval; none otherwise." }, "example": { "type": "string", "example": "none" } } }, "submitted_at": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" } } }, "resolved_at": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" }, "nullable": { "type": "boolean", "example": true }, "example": {} } }, "action_target_tag": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "Rogue#1234" }, "description": { "type": "string", "example": "Snapshot of the original action target tag (from mod_actions)." } } }, "action_reason": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "Spam" } } }, "action_created_at": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" }, "nullable": { "type": "boolean", "example": true } } }, "action_duration_seconds": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "number", "example": 86400 } } }, "submitter_username": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "newplayer" } } } } } } }, "AppealQueueItem": { "type": "object", "properties": { "allOf": { "type": "array", "items": { "$ref": "#/components/schemas/Appeal" } }, "description": { "type": "string", "example": "A staff-queue appeal row — identical shape to Appeal, with the joined action/submitter columns populated." } } }, "AppealResolveResult": { "type": "object", "properties": { "allOf": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "reversal": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "What the approval attempted against Discord." }, "properties": { "type": "object", "properties": { "attempted": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } }, "ok": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } }, "reversal_status": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "none", "done", "failed" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "done" } } }, "bot_status": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "number", "example": 200 }, "description": { "type": "string", "example": "HTTP status from the bot internal call, or null when no call was made." } } }, "error": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": {} } } } } } } } } } } } } }, "AppealEligibleAction": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "A ban/mute mod_action the caller may appeal (no active appeal outstanding)." }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 340 }, "description": { "type": "string", "example": "mod_action id — pass as mod_action_id when submitting." } } }, "action_type": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "ban", "mute" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "ban" } } }, "target_tag": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "Rogue#1234" } } }, "reason": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "Spam" } } }, "duration_seconds": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "number", "example": 86400 } } }, "created_at": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" } } } } } } }, "CreateAppealRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "mod_action_id", "submitted_text" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "mod_action_id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 340 }, "description": { "type": "string", "example": "The ban/mute mod_action to appeal (must belong to the caller)." } } }, "submitted_text": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "minLength": { "type": "number", "example": 1 }, "maxLength": { "type": "number", "example": 4000 }, "example": { "type": "string", "example": "I was banned by mistake — please review." } } } } } } }, "ResolveAppealRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "status" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "status": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "approved", "denied" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "approved" } } }, "staff_response": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "maxLength": { "type": "number", "example": 4000 }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "Reviewed — reversing the ban." } } } } } } }, "TotpCodeRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "code" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "code": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "123456" } } } } } } }, "SiteModeRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "mode" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "mode": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "live", "maintenance" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "live" } } } } } } }, "UnbanRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "ip" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "ip": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "203.0.113.5" } } } } } } }, "AccountStatus": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "Self-service account security status (GET /admin/account)." }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 1 } } }, "username": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "admin" } } }, "role": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "admin", "editor" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "admin" } } }, "totp_enabled": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } } } } } }, "TotpSetup": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "Enrollment material returned by POST /account/totp/setup." }, "properties": { "type": "object", "properties": { "otpauthUrl": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "otpauth://totp/Runic Gateway:admin?secret=..." } } }, "qr": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "QR code as a data: URL." }, "example": { "type": "string", "example": "data:image/png;base64,iVBORw0KGgo..." } } } } } } }, "TotpState": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "Result of enabling/disabling 2FA. Enabling also returns the one-time recovery codes." }, "properties": { "type": "object", "properties": { "totp_enabled": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } }, "recoveryCodes": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "nullable": { "type": "boolean", "example": true }, "description": { "type": "string", "example": "Single-use recovery codes, shown ONCE on enable." }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "abcde-12345" } } } } } } } } }, "LinkedIdentity": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "provider": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "google" } } }, "email": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "email" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "user@example.com" } } }, "linked_at": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" } } } } } } }, "SiteModeState": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "Result of PUT /admin/site-mode." }, "properties": { "type": "object", "properties": { "site_mode": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "live", "maintenance" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "maintenance" } } }, "changed_at": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" } } }, "changed_by": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "admin" } } } } } } }, "PublicStatus": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "Public site status (GET /public/status)." }, "properties": { "type": "object", "properties": { "mode": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "live", "maintenance" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "live" } } }, "status_message": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "" } } }, "version": { "$ref": "#/components/schemas/PublicVersion" } } } } }, "PublicVersion": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "Backend identity + version (GET /public/version; also embedded in /public/status)." }, "properties": { "type": "object", "properties": { "service": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "runic-gateway" }, "description": { "type": "string", "example": "Stable backend identifier for first-run recognition." } } }, "api": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "v1" }, "description": { "type": "string", "example": "API contract version (matches the /api/v1 mount)." } } }, "server": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "1.0.0" }, "description": { "type": "string", "example": "Server package version (informational)." } } } } } } }, "PublicModules": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "Installed modules currently SERVING (GET /public/modules). A disabled or failed module is absent, not listed with a state — its routes and nav are absent too. Database-free and not site-mode gated." }, "properties": { "type": "object", "properties": { "modules": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "items": { "$ref": "#/components/schemas/PublicModule" } } } } } } }, "PublicModule": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "One started module, as published to anonymous clients." }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "uo" }, "description": { "type": "string", "example": "Module id — also the URL segment its routes live under (/api/v1/public/)." } } }, "name": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Ultima Online" }, "description": { "type": "string", "example": "Human label." } } }, "version": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "1.0.0" }, "description": { "type": "string", "example": "The module's own version (semver). Unrelated to the API version." } } }, "capabilities": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "description": { "type": "string", "example": "Opaque strings the module declares. Feature-detect against them; treat an unknown one as absent." }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "shard" } } } } } } } } }, "Brand": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "Per-shard branding (BRAND_* env, with admin overrides for name/contactEmail). A client themes itself from this — one instance runs as any shard. Asset fields (logo/hero/favicon) may be site-relative paths; resolve them against the site base URL." }, "properties": { "type": "object", "properties": { "name": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Runic Gateway" } } }, "shortName": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Runic Gateway" } } }, "tagline": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "an independent private Ultima Online shard" } } }, "description": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } }, "contactEmail": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "" } } }, "url": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "" } } }, "accent": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "#7f99bd" }, "description": { "type": "string", "example": "Seed/accent color (hex) for theming. **Effective** value: the admin theme (theme_visual) wins over BRAND_ACCENT_COLOR, so a client that themes from this tracks admin theming with no change." } } }, "logo": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "" }, "description": { "type": "string", "example": "Logo URL or site-relative path; empty = no logo. An uploaded brand_assets.logo overrides BRAND_LOGO." } } }, "hero": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "/assets/img/runic-emblem.png" }, "description": { "type": "string", "example": "Hero image URL or site-relative path. An uploaded brand_assets.hero overrides BRAND_HERO." } } }, "favicon": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "/assets/img/favicon.ico" }, "description": { "type": "string", "example": "Favicon URL or site-relative path. An uploaded brand_assets.favicon overrides BRAND_FAVICON." } } } } } } }, "PublicSettings": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "Public site settings + branding (GET /public/settings). Whitelisted string settings, plus derived availability flags and the brand block a client themes from. Additional whitelisted keys may appear." }, "properties": { "type": "object", "properties": { "site_title": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Runic Gateway" } } }, "status_message": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "" } } }, "maintenance_message": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "" } } }, "registration": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "password": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" } } }, "sso": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" } } } } } } }, "brand": { "$ref": "#/components/schemas/Brand" }, "theme": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "nullable": { "type": "boolean", "example": true }, "description": { "type": "string", "example": "The effective CSS custom properties for the admin theme, resolved server-side (:root ← preset ← custom). **Absent** when the admin never set a theme, which is what makes an untouched instance render from the shipped stylesheet unchanged. Keys are CSS variable names; every value comes from a closed set (hex color, curated font stack, bounded px length, listed shadow)." }, "additionalProperties": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } }, "example": { "type": "object", "properties": { "--accent": { "type": "string", "example": "#c9973f" }, "--bg": { "type": "string", "example": "#1a120b" }, "--radius-card": { "type": "string", "example": "2px" } } } } }, "push": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "Push-notification relay config (M7). `ntfyUrl` is the client-facing ntfy base URL the app registers its device topic against (from NTFY_PUBLIC_URL / NTFY_ALLOWED_ORIGINS); null when push is not configured for this shard." }, "properties": { "type": "object", "properties": { "ntfyUrl": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "https://ntfy.example.com" } } } } } } } } }, "additionalProperties": { "type": "boolean", "example": true } } }, "NavSettings": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "Nav overrides for the two authenticated layouts (GET /settings/nav). Each value is the stored JSON **string** — settings.value is TEXT — or null when that nav was never overridden. Parse fail-safe: treat malformed as absent and fall back to the hardcoded nav." }, "properties": { "type": "object", "properties": { "nav_admin": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "{\"/admin/posts\":{\"label\":\"Blog Posts\",\"order\":10}}" } } }, "nav_player": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": {} } } } } } }, "ThemeOptions": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "The closed sets an admin may choose from when theming the site (GET /settings/theme-options). Served so the admin form cannot offer a value PUT /admin/settings would reject. Static — derived from the server theme config, not the database." }, "properties": { "type": "object", "properties": { "presets": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "description": { "type": "string", "example": "Selectable presets and their full token maps, so a form can show what an unset field currently resolves to. `custom` has null tokens and means \"no preset base — the shipped theme plus whatever custom fields are set\"." }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "fantasy" } } }, "label": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Fantasy" } } }, "tokens": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "nullable": { "type": "boolean", "example": true }, "additionalProperties": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } }, "example": { "type": "object", "properties": { "--bg": { "type": "string", "example": "#1a120b" }, "--accent": { "type": "string", "example": "#c9973f" } } } } } } } } } } }, "colorFields": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "description": { "type": "string", "example": "Editable color fields, each paired with the CSS variable it drives." }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "name": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "accent" } } }, "token": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "--accent" } } } } } } } } }, "radiusFields": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "name": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "radiusCard" } } }, "token": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "--radius-card" } } } } } } } } }, "shippedTokens": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "What the stylesheet declares by default — the values an unset field resolves to when no preset is selected." }, "additionalProperties": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } } } }, "fonts": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "Curated Google Fonts shortlist per role. Each option's `value` is the full CSS font-family stack exactly as it will be applied — the stored value, so no stack is ever built from admin input." }, "additionalProperties": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "value": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } }, "label": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } } } } } } } } } }, "shadows": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "value": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } }, "label": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } } } } } } } }, "radiusMaxPx": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 999 } } } } } } }, "DeletedId": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 12 } } } } } } }, "DeletedSlug": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "slug": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "getting-started" } } } } } } }, "DeletedFlag": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "deleted": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } } } } } }, "UnlinkedFlag": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "unlinked": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } } } } } }, "UnbanResult": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "ip": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "203.0.113.5" } } }, "removed": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "description": { "type": "string", "example": "Whether the IP had an entry that was cleared." }, "example": { "type": "boolean", "example": true } } } } } } } } } }