{ "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 — a private Ultima Online shard.\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 · 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": "Public · Shard", "description": "Live shard data ingested from the uo-link sidecar (status, feed, economy, IDOC, characters)" }, { "name": "Public · Atlas", "description": "Spawn atlas / bestiary — static shard content parsed from the shard's own ServUO tree, independent of the sidecar" }, { "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 · Shard", "description": "Link an in-game account and read its roster / vendors (uo-link)" }, { "name": "Player · Appeals", "description": "Player-submitted moderation appeals" }, { "name": "Admin · Dashboard", "description": "Dashboard summary and site mode" }, { "name": "Admin · Posts", "description": "News / five-on-friday / newsletter / screenshots + uploads" }, { "name": "Admin · Wiki", "description": "Wiki pages, categories, tags and revisions" }, { "name": "Admin · Settings", "description": "Site settings (admin only)" }, { "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 · Shard", "description": "uo-link sidecar connection config, live status and town crier (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": {} }, "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/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 (town crier or Discord)", "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", "enum": [ "towncrier", "discord" ] } }, "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": "", "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/shard/account": { "post": { "tags": [ "Admin · Account" ], "summary": "Create a game account and link it to the caller (staff self-service)", "description": "Same as POST /player/shard/account but for a signed-in staff user — provisions a game account (own username + password) and links it. Gated by game_account_signup + the shard’s mode; the password is never stored or logged.", "responses": { "201": { "description": "Account created and linked", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "403": { "description": "Game-account signup unavailable (site or shard)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Account name already taken", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": {} } }, "/api/v1/admin/shard/accounts": { "get": { "tags": [ "Admin · Account" ], "summary": "List the caller’s linked game accounts (self)", "description": "", "responses": { "200": { "description": "Linked accounts", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ShardLink" } } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/shard/atlas": { "get": { "tags": [ "Admin · Shard" ], "summary": "Spawn atlas status: path, drift, counts, pending review (admin only)", "description": "Where the ServUO tree is, whether it can be read, whether its source files have drifted from the loaded atlas, and any refresh staged for approval. The public /atlas/meta route reports the game world only; the filesystem detail is here.", "responses": { "200": { "description": "Atlas status", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AtlasStatus" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/shard/atlas/approve": { "post": { "tags": [ "Admin · Shard" ], "summary": "Approve a staged atlas refresh that removes a facet (admin only)", "description": "Re-parses the tree and applies it, facet loss included. Only the decision was stored, never the parsed world, so what lands matches the tree at approval time — an operator who has since fixed a half-copied mount gets the corrected import.", "responses": { "200": { "description": "What happened", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AtlasRefreshResult" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/shard/atlas/import": { "post": { "tags": [ "Admin · Shard" ], "summary": "Re-import the spawn atlas from the ServUO tree (admin only)", "description": "Applies a map change without a restart. `force` reimports even when the source hashes match what is loaded. A refresh that would REMOVE a facet is still staged for approval rather than applied — that decision is never taken implicitly. An unreadable tree answers 200 with status \"unavailable\" rather than 500: the refresh contract reports outcomes instead of throwing, and the admin needs to be told what is wrong with the path.", "responses": { "200": { "description": "What happened", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AtlasRefreshResult" } } } }, "400": { "description": "Bad Request" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": false, "content": { "application/json": { "schema": { "type": "object", "properties": { "force": { "type": "boolean", "description": "Reimport even if the tree is unchanged." } } } } } } } }, "/api/v1/admin/shard/atlas/path": { "put": { "tags": [ "Admin · Shard" ], "summary": "Set the ServUO tree the atlas reads from (admin only)", "description": "Persisted as a setting, which wins over the SERVUO_PATH deploy default so the mount can move without a redeploy. Blank clears it and the atlas is simply skipped on the next boot. Deliberately does not import as a side effect — the response carries the refreshed status so the panel can offer that as the next step.", "responses": { "200": { "description": "Atlas status after the change", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AtlasStatus" } } } }, "400": { "description": "Bad Request" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "path" ], "properties": { "path": { "type": "string", "description": "Absolute path to the ServUO server root. Blank disables the atlas." } } } } } } } }, "/api/v1/admin/shard/atlas/reject": { "post": { "tags": [ "Admin · Shard" ], "summary": "Reject a staged atlas refresh (admin only)", "description": "Keeps the current atlas and remembers the decision against those exact source hashes, so a declined refresh does not re-prompt on every restart. Changing the tree asks again.", "responses": { "200": { "description": "Rejected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AtlasRefreshResult" } } } }, "404": { "description": "Nothing is awaiting review", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/shard/audit": { "get": { "tags": [ "Admin · Shard" ], "summary": "Recent in-game moderation audit events (admin/moderator)", "description": "", "parameters": [ { "name": "limit", "in": "query", "schema": { "type": "string" } } ], "responses": { "200": { "description": "admin.audit events, newest first", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ShardEvent" } } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/shard/ban": { "post": { "tags": [ "Admin · Shard" ], "summary": "Ban an account, timed or indefinite (admin/moderator)", "description": "", "responses": { "200": { "description": "Banned", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "403": { "description": "Protected target or write plane disabled", "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": { "account": { "type": "string" }, "serial": { "type": "string" }, "durationSec": { "type": "integer" }, "reason": { "type": "string" } } } } } } } }, "/api/v1/admin/shard/broadcast": { "post": { "tags": [ "Admin · Shard" ], "summary": "Broadcast a system message to everyone online (admin/moderator)", "description": "", "responses": { "200": { "description": "Broadcast", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "text": { "type": "string" }, "hue": { "type": "integer" } }, "required": [ "text" ] } } } } } }, "/api/v1/admin/shard/char/{serial}": { "get": { "tags": [ "Admin · Account" ], "summary": "Character sheet (self-linked characters; admins: any character)", "description": "", "parameters": [ { "name": "serial", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Mobile serial, e.g. 0x24C." } ], "responses": { "200": { "description": "Character profile", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "403": { "description": "Character not on an account linked to the caller", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" }, "502": { "description": "Bad Gateway" }, "503": { "description": "Service Unavailable" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/shard/houses": { "get": { "tags": [ "Admin · Shard" ], "summary": "Full house registry — owner, price, decay (admin/moderator)", "description": "The complete house registry. The public endpoint shows only IDOC houses with location; this staff view carries owner/price/co-owner/decay detail.", "responses": { "200": { "description": "Houses, ordered by name", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ShardHouse" } } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/shard/kick": { "post": { "tags": [ "Admin · Shard" ], "summary": "Kick every live session of an account (admin/moderator)", "description": "", "responses": { "200": { "description": "Kicked", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "403": { "description": "Protected target or write plane disabled", "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": { "account": { "type": "string" }, "serial": { "type": "string" } } } } } } } }, "/api/v1/admin/shard/link": { "post": { "tags": [ "Admin · Account" ], "summary": "Link an in-game account with a one-time code (self)", "description": "", "responses": { "200": { "description": "Linked", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ShardLinkResult" } } } }, "400": { "description": "Unknown or expired code", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" }, "502": { "description": "Bad Gateway" }, "503": { "description": "Service Unavailable" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ShardLinkRequest" } } } } } }, "/api/v1/admin/shard/pages": { "get": { "tags": [ "Admin · Shard" ], "summary": "Open help-page (support) queue (admin/moderator)", "description": "", "responses": { "200": { "description": "Open pages", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/shard/pages/{id}/close": { "post": { "tags": [ "Admin · Shard" ], "summary": "Resolve a help page without a reply (admin/moderator)", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Page id (sender serial)." } ], "responses": { "200": { "description": "Closed", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/shard/pages/{id}/respond": { "post": { "tags": [ "Admin · Shard" ], "summary": "Reply to a help page, optionally closing it (admin/moderator)", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Page id (sender serial)." } ], "responses": { "200": { "description": "Responded", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "404": { "description": "Unknown page", "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": { "message": { "type": "string" }, "close": { "type": "boolean" } }, "required": [ "message" ] } } } } } }, "/api/v1/admin/shard/roster/{account}": { "get": { "tags": [ "Admin · Account" ], "summary": "Character roster for an account (self; admins: any account)", "description": "", "parameters": [ { "name": "account", "in": "path", "required": true, "schema": { "type": "string" }, "description": "A game account linked to the caller." } ], "responses": { "200": { "description": "Account roster", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "403": { "description": "Account not linked to the caller", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/shard/sales": { "get": { "tags": [ "Admin · Account" ], "summary": "Recent player-vendor sales for the caller’s linked accounts (self)", "description": "", "responses": { "200": { "description": "Vendor sales", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ShardVendorSale" } } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/shard/unban": { "post": { "tags": [ "Admin · Shard" ], "summary": "Clear an account ban (admin/moderator)", "description": "", "responses": { "200": { "description": "Unbanned", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "account": { "type": "string" } }, "required": [ "account" ] } } } } } }, "/api/v1/admin/shard/vendors/{account}": { "get": { "tags": [ "Admin · Account" ], "summary": "Player vendors for an account (self; admins: any account)", "description": "", "parameters": [ { "name": "account", "in": "path", "required": true, "schema": { "type": "string" }, "description": "A game account linked to the caller." } ], "responses": { "200": { "description": "Vendor snapshot", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "403": { "description": "Account not linked to the caller", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/shard/visibility": { "get": { "tags": [ "Admin · Shard" ], "summary": "Get per-feature shard visibility config (admin only)", "description": "The effective config (compiled defaults merged with stored overrides) plus the vocabulary the admin UI renders from: the audience ladder and the always-locked fields. Defaults reproduce pre-v3 behavior.", "responses": { "200": { "description": "Visibility config", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ShardVisibilityConfig" } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "tags": [ "Admin · Shard" ], "summary": "Update per-feature shard visibility config (admin only)", "description": "Patch one or more features. Unknown feature names, unknown rungs, and any attempt to configure a locked field (acct / webId — admin-only always) are rejected with 400 rather than silently dropped.", "responses": { "200": { "description": "Updated config", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ShardVisibilityConfig" } } } }, "400": { "description": "Unknown feature, rung, or a locked field", "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/ShardVisibilityUpdate" } } } } } }, "/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/uo-link/config": { "get": { "tags": [ "Admin · Shard" ], "summary": "Get uo-link config + live status + ingestion stats (admin only)", "description": "", "responses": { "200": { "description": "Masked config, health and ingestion stats", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "403": { "description": "Admin role required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "tags": [ "Admin · Shard" ], "summary": "Save uo-link connection config (admin only)", "description": "token is write-only — omit/blank it to keep the existing one. Saving (re)starts the WS ingest client.", "responses": { "200": { "description": "Updated config + live status", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Validation error, or missing token while enabling", "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": { "baseUrl": { "type": "string" }, "wsUrl": { "type": "string" }, "token": { "type": "string" }, "protocol": { "type": "integer" }, "enabled": { "type": "boolean" } } } } } } } }, "/api/v1/admin/uo-link/stream": { "get": { "tags": [ "Admin · Shard" ], "summary": "Full live shard event stream incl. audit/cheat (SSE, admin only)", "description": "", "responses": { "200": { "description": "An SSE stream (Content-Type: text/event-stream)." } } } }, "/api/v1/admin/uo-link/towncrier": { "post": { "tags": [ "Admin · Shard" ], "summary": "Publish / replace a town-crier message (admin only)", "description": "", "responses": { "200": { "description": "Posted", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Rejected (over caps)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" }, "502": { "description": "Bad Gateway" }, "503": { "description": "Shard unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TownCrierRequest" } } } } } }, "/api/v1/admin/uo-link/towncrier/{id}": { "delete": { "tags": [ "Admin · Shard" ], "summary": "Remove a town-crier message (admin only)", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Town-crier message id." } ], "responses": { "200": { "description": "Removed", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "404": { "description": "Unknown id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" }, "502": { "description": "Bad Gateway" }, "503": { "description": "Service Unavailable" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/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}/shard/accounts": { "get": { "tags": [ "Admin · Users" ], "summary": "A user’s linked game accounts (admin only)", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "User id." } ], "responses": { "200": { "description": "Linked accounts", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ShardLink" } } } } }, "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}/shard/houses": { "get": { "tags": [ "Admin · Users" ], "summary": "Houses owned by a user’s accounts (admin only)", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "User id." } ], "responses": { "200": { "description": "Houses (IDOC first)", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } }, "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}/shard/link/{account}": { "delete": { "tags": [ "Admin · Users" ], "summary": "Unlink a game account from this user (admin only)", "description": "Severs a game account’s tie to the website user from the site side (sidecar DELETE /link/{account}) and drops the local mirror. actor is stamped from the session.", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "User id." }, { "name": "account", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Game account to unlink." } ], "responses": { "200": { "description": "Unlinked", "content": { "application/json": { "schema": { "type": "object", "properties": { "account": { "type": "string" }, "unlinked": { "type": "boolean" } } } } } }, "400": { "description": "Bad Request" }, "403": { "description": "Protected staff account (refused by shard)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not linked", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" }, "502": { "description": "Bad Gateway" }, "503": { "description": "Service Unavailable" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/admin/users/{id}/shard/online": { "get": { "tags": [ "Admin · Users" ], "summary": "A user’s characters currently online (admin only)", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "User id." } ], "responses": { "200": { "description": "Online characters", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } }, "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}/shard/sales": { "get": { "tags": [ "Admin · Users" ], "summary": "Recent vendor sales on a user’s accounts (admin only)", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "User id." } ], "responses": { "200": { "description": "Vendor sales", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ShardVendorSale" } } } } }, "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}/shard/standing": { "get": { "tags": [ "Admin · Users" ], "summary": "A user’s shard standing — governorships held and guilds led (admin only)", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "User id." } ], "responses": { "200": { "description": "Standing { governorOf, guildsLed }", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "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}/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": {} } }, "/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/player/shard/account": { "post": { "tags": [ "Player · Shard" ], "summary": "Create a game account (hybrid signup) and link it to the caller", "description": "Provisions a new game account with its own username + password and auto-links it to the signed-in website user. Available only when game_account_signup is enabled and the shard accepts website signups. The password is hashed on the shard and never stored or logged by the site.", "responses": { "201": { "description": "Account created and linked", "content": { "application/json": { "schema": { "type": "object", "properties": { "account": { "type": "string" }, "linked": { "type": "boolean" } } } } } }, "400": { "description": "Validation error or rejected name/password", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } } }, "401": { "description": "Unauthorized" }, "403": { "description": "Game-account signup unavailable (site or shard)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Account name already taken", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Per-IP account cap reached", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" }, "503": { "description": "Shard unavailable — retry", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": {} } }, "/api/v1/player/shard/accounts": { "get": { "tags": [ "Player · Shard" ], "summary": "List the caller’s linked game accounts", "description": "", "responses": { "200": { "description": "Linked accounts", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ShardLink" } } } } }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/player/shard/char/{serial}": { "get": { "tags": [ "Player · Shard" ], "summary": "Character sheet — only for a character on the caller’s linked account", "description": "", "parameters": [ { "name": "serial", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Mobile serial, e.g. 0x24C." } ], "responses": { "200": { "description": "Character profile", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Unauthorized" }, "403": { "description": "Character not on an account linked to the caller", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" }, "502": { "description": "Bad Gateway" }, "503": { "description": "Shard unavailable — retry", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/player/shard/houses": { "get": { "tags": [ "Player · Shard" ], "summary": "The caller’s own houses (home status)", "description": "Houses owned by the caller’s linked accounts, with decay/IDOC status. Only the caller’s own houses — never anyone else’s.", "responses": { "200": { "description": "The caller’s houses", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ShardHouse" } } } } }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/player/shard/link": { "post": { "tags": [ "Player · Shard" ], "summary": "Link an in-game account with a one-time code", "description": "The player runs [link in game to get a code, then submits it here. The server confirms it with the sidecar and mirrors the link.", "responses": { "200": { "description": "Linked", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ShardLinkResult" } } } }, "400": { "description": "Unknown or expired code", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" }, "502": { "description": "Bad Gateway" }, "503": { "description": "Shard unavailable — retry", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ShardLinkRequest" } } } } } }, "/api/v1/player/shard/roster/{account}": { "get": { "tags": [ "Player · Shard" ], "summary": "Character roster for a linked account", "description": "", "parameters": [ { "name": "account", "in": "path", "required": true, "schema": { "type": "string" }, "description": "A game account linked to the caller." } ], "responses": { "200": { "description": "Account roster", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Unauthorized" }, "403": { "description": "Account not linked to the caller", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" }, "503": { "description": "Shard unavailable — retry", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/player/shard/sales": { "get": { "tags": [ "Player · Shard" ], "summary": "Recent player-vendor sales for the caller’s linked accounts", "description": "", "responses": { "200": { "description": "Vendor sales", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ShardVendorSale" } } } } }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" }, "500": { "description": "Internal Server Error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/player/shard/vendors/{account}": { "get": { "tags": [ "Player · Shard" ], "summary": "Player vendors for a linked account", "description": "", "parameters": [ { "name": "account", "in": "path", "required": true, "schema": { "type": "string" }, "description": "A game account linked to the caller." } ], "responses": { "200": { "description": "Vendor snapshot", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Unauthorized" }, "403": { "description": "Account not linked to the caller", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" }, "503": { "description": "Shard unavailable — retry", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/v1/public/atlas/champions": { "get": { "tags": [ "Public · Atlas" ], "summary": "Configured champion altars (the roster, not the live board)", "description": "Where the altars are and what each one summons — \"there is an Unholy Terror altar in Deceit\". `randomType` marks altars whose champion is drawn at activation. Do not conflate this with GET /public/shard/champs, which is the live sidecar-fed board (\"it is on level 3 right now\").", "parameters": [ { "name": "facet", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Limit to one facet." } ], "responses": { "200": { "description": "Altars, by facet then name", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/AtlasChampion" } } } } }, "400": { "description": "Bad Request" }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" }, "503": { "description": "Service Unavailable" } } } }, "/api/v1/public/atlas/creatures": { "get": { "tags": [ "Public · Atlas" ], "summary": "Search the bestiary (paginated)", "description": "Every creature the shard spawns, most numerous first. `total` is how many can be alive at once across all spawners; `points` is how many spawners mention it; `facets` maps facet name to that creature\\'s share on it. Static content parsed from the shard\\'s ServUO tree — unaffected by the shard being offline.", "parameters": [ { "name": "q", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Substring match on the creature name (max 60 chars)." }, { "name": "facet", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Limit to creatures spawning on this facet. Facet names come from the shard's own files; an unknown one returns an empty page." }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Page size, 1..100 (default 50)." }, { "name": "offset", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Rows to skip (default 0)." } ], "responses": { "200": { "description": "A page of creatures plus the unpaginated total", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AtlasCreaturePage" } } } }, "400": { "description": "Bad Request" }, "403": { "description": "The atlas feature is gated above this caller", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "The atlas feature is disabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" }, "503": { "description": "Service Unavailable" } } } }, "/api/v1/public/atlas/creatures/{slug}": { "get": { "tags": [ "Public · Atlas" ], "summary": "One creature: where it spawns, and what spawns with it", "description": "The answer the atlas exists to give. `places` is the aggregate — \"lizardman → Shrines, Isamu-Jima, Yew\" — resolved by point-in-rect against the shard\\'s own region rectangles, falling back to the nearest landmark, else \"Wilderness\". `spawners` lists the individual spawn points (bounded; `spawnersTruncated` says when the list was cut), and `alsoHere` is what shares those spawners.", "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Creature slug, e.g. lizardman." }, { "name": "facet", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Restrict places and spawners to one facet." }, { "name": "points", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Max spawners to return, 1..1000 (default 200)." } ], "responses": { "200": { "description": "The creature", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AtlasCreature" } } } }, "400": { "description": "Bad Request" }, "403": { "description": "Forbidden" }, "404": { "description": "No such creature in this atlas (or the feature is disabled)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal Server Error" }, "503": { "description": "Service Unavailable" } } } }, "/api/v1/public/atlas/landmarks": { "get": { "tags": [ "Public · Atlas" ], "summary": "Points of interest (dungeon levels, town markers)", "description": "From the shard\\'s Data/Locations files. `group` is the innermost enclosing parent (\"Covetous\"), which is the label worth showing over the individual marker (\"Level 1\").", "parameters": [ { "name": "facet", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Limit to one facet." }, { "name": "q", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Substring match on the landmark name or its group." } ], "responses": { "200": { "description": "Landmarks, by facet then group", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/AtlasLandmark" } } } } }, "400": { "description": "Bad Request" }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" }, "503": { "description": "Service Unavailable" } } } }, "/api/v1/public/atlas/meta": { "get": { "tags": [ "Public · Atlas" ], "summary": "What atlas is loaded: facets, counts, when it was imported", "description": "Drives the facet filter and the \"parsed from the shard\\'s own files on \" line. Reports the game world only — the ServUO path, the per-file hashes and any pending refresh are operator detail and live on the admin status route.", "responses": { "200": { "description": "Atlas metadata", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AtlasMeta" } } } }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" }, "503": { "description": "Service Unavailable" } } } }, "/api/v1/public/atlas/regions": { "get": { "tags": [ "Public · Atlas" ], "summary": "Named regions and their rectangles", "description": "Flattened out of the shard\\'s nested Regions.xml. `priority` and the rectangles are what placed each spawn point, kept so the placement can be re-derived rather than taken on trust.", "parameters": [ { "name": "facet", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Limit to one facet." }, { "name": "q", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Substring match on the region name." } ], "responses": { "200": { "description": "Regions, by facet then name", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/AtlasRegion" } } } } }, "400": { "description": "Bad Request" }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" }, "503": { "description": "Service Unavailable" } } } }, "/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/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/shard/champs": { "get": { "tags": [ "Public · Shard" ], "summary": "Current champion-spawn board (all categories)", "description": "The live board of every champion / mini-champ / sea-boss spawn. Update in place via the champ.update / champ.remove frames on /shard/stream.", "responses": { "200": { "description": "Champion spawns, ordered by name", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" } } } }, "/api/v1/public/shard/economy": { "get": { "tags": [ "Public · Shard" ], "summary": "Gold-supply time series (oldest → newest)", "description": "", "parameters": [ { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Max samples (default 100, max 1000)." } ], "responses": { "200": { "description": "Economy samples", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ShardEconomyPoint" } } } } }, "400": { "description": "Bad Request" }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" } } } }, "/api/v1/public/shard/features": { "get": { "tags": [ "Public · Shard" ], "summary": "Shard features visible to the caller (drives client nav)", "description": "The caller\\'s audience rung plus the shard features they may reach, so a client can hide nav entries instead of rendering links that 403. Reports only what the caller can see — the list itself does not disclose gated features.", "responses": { "200": { "description": "Visible features", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ShardFeatures" } } } }, "500": { "description": "Internal Server Error" } } } }, "/api/v1/public/shard/feed": { "get": { "tags": [ "Public · Shard" ], "summary": "Recent notable shard events (from the ingested log)", "description": "The stored-history twin of /shard/stream, and it reaches the same verdict: which kinds are returned is resolved against the caller\\'s audience rung under the live visibility config, and each event\\'s payload is field-projected against its own kind\\'s feature. Kinds the caller may not read are omitted (an explicit ?kind= for one of them returns []), and acct/webId never appear below admin.", "parameters": [ { "name": "kind", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter to a single event kind, e.g. vendor.sale. Returns [] if the caller may not read that kind." }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Max rows (default 100, max 1000)." } ], "responses": { "200": { "description": "Events, newest first", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ShardEvent" } } } } }, "400": { "description": "Bad Request" }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" } } } }, "/api/v1/public/shard/governors": { "get": { "tags": [ "Public · Shard" ], "summary": "Current town-governor board (City Loyalty)", "description": "One entry per city with its governor and election phase. Empty if the shard does not run the City Loyalty system. Live via city.update on /shard/stream.", "responses": { "200": { "description": "Cities, ordered by name", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" } } } }, "/api/v1/public/shard/governors/{city}/history": { "get": { "tags": [ "Public · Shard" ], "summary": "Governor term history for a city", "description": "", "parameters": [ { "name": "city", "in": "path", "required": true, "schema": { "type": "string" }, "description": "City name, e.g. Britain." }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Max terms (default 100, max 500)." } ], "responses": { "200": { "description": "Terms, newest first", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } }, "400": { "description": "Bad Request" }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" } } } }, "/api/v1/public/shard/guilds": { "get": { "tags": [ "Public · Shard" ], "summary": "Current guild board (rosters, alliances, leaders)", "description": "The live board of every guild. Update in place via the guild.update / guild.remove / guild.join frames on /shard/stream.", "responses": { "200": { "description": "Guilds, ordered by name", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" } } } }, "/api/v1/public/shard/houses": { "get": { "tags": [ "Public · Shard" ], "summary": "House registry (owner, co-owners, price, decay)", "description": "Every house seen via the house.update registry feed. `price` is the placement value, not a for-sale flag. Live via house.update / house.remove on /shard/stream.", "responses": { "200": { "description": "Houses, ordered by name", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ShardHouse" } } } } }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" } } } }, "/api/v1/public/shard/idoc": { "get": { "tags": [ "Public · Shard" ], "summary": "Houses currently in danger (IDOC)", "description": "Location-level board of the houses about to collapse. Owner identity and price are gated by the `houses` feature\\'s field rules (default `staff`), and the owner\\'s game account is admin-only always — so an anonymous caller sees name, region and coordinates only.", "responses": { "200": { "description": "IDOC houses", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ShardHouse" } } } } }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" } } } }, "/api/v1/public/shard/online": { "get": { "tags": [ "Public · Shard" ], "summary": "Staff online now (linked staff accounts; location is admin/moderator-only)", "description": "", "responses": { "200": { "description": "Online players", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ShardOnlinePlayer" } } } } }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" } } } }, "/api/v1/public/shard/points": { "get": { "tags": [ "Public · Shard" ], "summary": "Points / loyalty leaderboards, one board per point system", "description": "Every points/loyalty leaderboard the shard publishes (Queen\\'s Loyalty, Void Pool, the nine city loyalties, Clean Up Britannia, …), each with its display name, max points, participant count and top N. Served from our own store, so it renders while the shard is down; live via points.board on /shard/stream. A board\\'s display name may arrive as a literal (`nameString`) or a cliloc id (`nameNumber`) — resolve clilocs client-side.", "responses": { "200": { "description": "Boards, ordered by display name", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ShardPointsBoard" } } } } }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" } } } }, "/api/v1/public/shard/points/{system}": { "get": { "tags": [ "Public · Shard" ], "summary": "One points system\\'s leaderboard", "description": "A single board by the shard\\'s own PointsType name (e.g. `QueensLoyalty`, `CleanUpBritannia`). Returns 404 when the shard has never published that system — distinct from a published board that nobody has scored in yet, which returns 200 with an empty `top`.", "parameters": [ { "name": "system", "in": "path", "required": true, "schema": { "type": "string" }, "description": "PointsType name, e.g. QueensLoyalty" } ], "responses": { "200": { "description": "The board", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ShardPointsBoard" } } } }, "400": { "description": "Malformed system name" }, "403": { "description": "Forbidden" }, "404": { "description": "The shard has never published that system" }, "500": { "description": "Internal Server Error" } } } }, "/api/v1/public/shard/presence": { "get": { "tags": [ "Public · Shard" ], "summary": "Online population aggregate (count + per-facet + per-region)", "description": "The latest presence.online snapshot powering the \"Players Online\" widget. Live via presence.online on /shard/stream.", "responses": { "200": { "description": "Population snapshot", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" } } } }, "/api/v1/public/shard/ruleset": { "get": { "tags": [ "Public · Shard" ], "summary": "The shard\\'s published ruleset (expansion, systems, caps, limits)", "description": "How this shard is actually configured, published by the shard itself as one world.ruleset frame: expansion, which optional systems are on, skill/stat caps, account and house limits, champion scroll rules and the save/restart schedule. Served from our own store, so it renders while the shard is down; live via world.ruleset on /shard/stream. Returns `null` if the shard has never published one (an older plugin, or Bridge.RulesetEnabled=false) — distinct from a published ruleset, and the page renders it differently.", "responses": { "200": { "description": "The ruleset, or null if never published", "content": { "application/json": { "schema": { "type": "object", "nullable": true, "additionalProperties": true } } } }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" } } } }, "/api/v1/public/shard/status": { "get": { "tags": [ "Public · Shard" ], "summary": "Shard connection state, online count and latest economy", "description": "", "responses": { "200": { "description": "Shard status", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ShardStatus" } } } }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" } } } }, "/api/v1/public/shard/stream": { "get": { "tags": [ "Public · Shard" ], "summary": "Live shard event stream (Server-Sent Events, filtered by audience)", "description": "text/event-stream of live events. The caller\\'s audience rung is resolved once at subscribe time and frozen for the connection; each frame is then gated on its feature and field-projected, so sensitive kinds and fields (staff audit, cheat detection, login attempts, IPs, acct/webId) never reach a caller below their configured rung.", "responses": { "200": { "description": "An SSE stream (Content-Type: text/event-stream)." } } } }, "/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" } } } } }, "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)." } } } } } } }, "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." } } }, "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." } } }, "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." } } }, "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." } } } } } } }, "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" } } } } } } }, "gameAccountSignup": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": false } } }, "brand": { "$ref": "#/components/schemas/Brand" }, "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 } } }, "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 } } } } } } }, "ShardStatus": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "Public shard status (GET /public/shard/status)." }, "properties": { "type": "object", "properties": { "enabled": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } }, "status": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "connected" }, "description": { "type": "string", "example": "connected | reconnecting | disconnected | error" } } }, "pluginConnected": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "description": { "type": "string", "example": "Is the shard link up right now?" }, "example": { "type": "boolean", "example": true } } }, "lastEventAt": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" }, "nullable": { "type": "boolean", "example": true } } }, "onlineCount": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 12 } } }, "economy": { "$ref": "#/components/schemas/ShardEconomyPoint" } } } } }, "ShardEvent": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "A logged shard event." }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 4821 } } }, "kind": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "vendor.sale" } } }, "t": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "description": { "type": "string", "example": "Event time, epoch ms." }, "example": { "type": "number", "example": 1783720195626 } } }, "bootId": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "boot-abc123" } } }, "payload": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "additionalProperties": { "type": "boolean", "example": true }, "description": { "type": "string", "example": "The full event object." } } }, "createdAt": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" } } } } } } }, "ShardEconomyPoint": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "nullable": { "type": "boolean", "example": true }, "description": { "type": "string", "example": "One gold-supply sample." }, "properties": { "type": "object", "properties": { "accounts": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "number", "example": 240 } } }, "gold": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "number", "example": 1028983421 } } }, "t": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "description": { "type": "string", "example": "Sample time, epoch ms." }, "example": { "type": "number", "example": 1783720000000 } } } } } } }, "ShardOnlinePlayer": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "A LINKED player online now (only accounts linked to a website user are listed)." }, "properties": { "type": "object", "properties": { "serial": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "0x24C" } } }, "name": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Darrow" } } }, "map": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "Trammel" } } }, "x": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "number", "example": 1402 } } }, "y": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "number", "example": 1604 } } }, "z": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "number", "example": 0 } } } } } } }, "ShardVendorSale": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "A player-vendor sale (visible only to the linked owner)." }, "properties": { "type": "object", "properties": { "t": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "description": { "type": "string", "example": "Sale time, epoch ms." }, "example": { "type": "number", "example": 1783720195626 } } }, "itemType": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Longsword" } } }, "amount": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 1 } } }, "price": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 100 } } }, "commission": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "number", "example": 5 } } }, "ownerAcct": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "whitlocktech" } } } } } } }, "ShardHouse": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "A house at its current decay stage." }, "properties": { "type": "object", "properties": { "serial": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "0x4004705F" } } }, "stage": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "IDOC" } } }, "map": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "Trammel" } } }, "x": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "nullable": { "type": "boolean", "example": true } } }, "y": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "nullable": { "type": "boolean", "example": true } } }, "z": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "nullable": { "type": "boolean", "example": true } } }, "region": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true } } }, "name": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "An Unnamed House" } } }, "ownerSerial": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true } } }, "ownerAcct": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true } } }, "builtOn": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" }, "nullable": { "type": "boolean", "example": true } } }, "lastRefreshed": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" }, "nullable": { "type": "boolean", "example": true } } }, "isIdoc": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } }, "updatedAt": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" } } } } } } }, "ShardPointsBoard": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "One point system's leaderboard (Protocol 3.0 points.board). The shard carries ~25 separate point currencies; each publishes its own board. The display name may arrive as a literal string, a cliloc id, or both — resolve clilocs client-side." }, "properties": { "type": "object", "properties": { "system": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "QueensLoyalty" }, "description": { "type": "string", "example": "The shard's PointsType name; the board's stable key." } } }, "nameString": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "Queen's Loyalty" } } }, "nameNumber": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "number", "example": 1114938 }, "description": { "type": "string", "example": "Cliloc id, 0 when the name is a literal." } } }, "maxPoints": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "number", "example": 30000 } } }, "players": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "number", "example": 842 }, "description": { "type": "string", "example": "Players actually holding points in this system." } } }, "showOnGump": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true }, "description": { "type": "string", "example": "The shard's own 'is this player-facing?' flag." } } }, "top": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "description": { "type": "string", "example": "The ranked players, best first. Capped by the shard (10 by default). Empty when nobody has scored yet." }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "rank": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 1 } } }, "serial": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "0x1A2B" } } }, "name": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Darrow" }, "description": { "type": "string", "example": "Omitted when the leaderboards `name` field is gated above the caller." } } }, "points": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 29500 } } } } } } } } }, "t": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "nullable": { "type": "boolean", "example": true }, "description": { "type": "string", "example": "Frame time, epoch ms." } } }, "updatedAt": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" } } } } } } }, "ShardFeatures": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "The shard features the caller may reach, plus the audience rung they resolved to. Drives client nav so it never renders a link that would 403." }, "properties": { "type": "object", "properties": { "level": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "anonymous", "logged_in", "player", "staff", "admin" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "anonymous" } } }, "features": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } }, "example": { "type": "array", "example": [ "status", "activity", "champs", "guilds", "governors", "houses", "presence" ], "items": { "type": "string" } } } } } } } }, "ShardFeatureVisibility": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "Visibility settings for one shard feature." }, "properties": { "type": "object", "properties": { "enabled": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } }, "audience": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "anonymous", "logged_in", "player", "staff", "admin" ], "items": { "type": "string" } }, "description": { "type": "string", "example": "Minimum rung that may reach this feature. Each rung implies the ones below it." }, "example": { "type": "string", "example": "anonymous" } } }, "stream": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "description": { "type": "string", "example": "Whether this feature's event kinds fan out over SSE at all." }, "example": { "type": "boolean", "example": true } } }, "fieldRules": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "additionalProperties": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } }, "description": { "type": "string", "example": "Per-field rung overrides for the sensitive fields this feature exposes. acct / webId are admin-only always and are rejected here." }, "example": { "type": "object", "properties": { "location": { "type": "string", "example": "staff" } } } } } } } } }, "ShardVisibilityConfig": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "ladder": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } }, "example": { "type": "array", "example": [ "anonymous", "logged_in", "player", "staff", "admin" ], "items": { "type": "string" } } } }, "lockedFields": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } }, "example": { "type": "array", "example": [ "acct", "webId" ], "items": { "type": "string" } } } }, "defaults": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "additionalProperties": { "$ref": "#/components/schemas/ShardFeatureVisibility" } } }, "features": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "additionalProperties": { "$ref": "#/components/schemas/ShardFeatureVisibility" } } } } } } }, "ShardVisibilityUpdate": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "features" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "features": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "additionalProperties": { "$ref": "#/components/schemas/ShardFeatureVisibility" }, "example": { "type": "object", "properties": { "market": { "type": "object", "properties": { "enabled": { "type": "boolean", "example": true }, "audience": { "type": "string", "example": "player" }, "stream": { "type": "boolean", "example": false }, "fieldRules": { "type": "object", "properties": { "ownerName": { "type": "string", "example": "player" } } } } } } } } } } } } }, "AtlasCreature": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "A creature in the bestiary. `places`/`points`/`alsoHere` are present only on the single-creature route." }, "properties": { "type": "object", "properties": { "slug": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "lizardman" } } }, "name": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Lizardman" } } }, "total": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "description": { "type": "string", "example": "How many can be alive at once, summed across every spawner." }, "example": { "type": "number", "example": 214 } } }, "points": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "description": { "type": "string", "example": "How many spawners mention this creature." }, "example": { "type": "number", "example": 62 } } }, "facets": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "additionalProperties": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" } } }, "description": { "type": "string", "example": "This creature's share per facet." }, "example": { "type": "object", "properties": { "Felucca": { "type": "number", "example": 96 }, "Trammel": { "type": "number", "example": 88 }, "Tokuno": { "type": "number", "example": 30 } } } } }, "art": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "description": { "type": "string", "example": "Operator-supplied art under uploads/atlas/. NULL on a fresh import — the repo ships no creature art." } } }, "places": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "description": { "type": "string", "example": "Where it spawns, aggregated by resolved place. The answer the atlas exists to give." }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "facet": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Trammel" } } }, "label": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "Resolved region, else nearest landmark group, else \"Wilderness\"." }, "example": { "type": "string", "example": "Shrines" } } }, "spawners": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 7 } } }, "maxAlive": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 21 } } } } } } } } }, "spawners": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "description": { "type": "string", "example": "The individual spawners. Named separately from `points` (the count) so one key never means two things." }, "items": { "$ref": "#/components/schemas/AtlasSpawner" } } }, "spawnersTruncated": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "description": { "type": "string", "example": "True when the spawner list was cut at the requested bound." }, "example": { "type": "boolean", "example": false } } }, "alsoHere": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "description": { "type": "string", "example": "Creatures sharing a spawner with this one." }, "items": { "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": "lizardman-warrior" } } }, "name": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Lizardman Warrior" } } }, "shared": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 12 } } } } } } } } } } } } }, "AtlasSpawner": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "One ServUO spawner, with the place its coordinates resolved to." }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" } } }, "facet": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Felucca" } } }, "name": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "description": { "type": "string", "example": "The spawner's own name in the ServUO file." } } }, "x": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 5411 } } }, "y": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 1234 } } }, "width": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" } } }, "height": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" } } }, "range": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "description": { "type": "string", "example": "Spawn radius." } } }, "maxCount": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "description": { "type": "string", "example": "How many of THIS creature this spawner keeps alive." }, "example": { "type": "number", "example": 3 } } }, "minDelay": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "description": { "type": "string", "example": "Respawn window, in SECONDS. Normalised at parse time — the source stores minutes or seconds per record, decided by its own DelayInSec flag." }, "example": { "type": "number", "example": 300 } } }, "maxDelay": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 600 } } }, "todStart": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "description": { "type": "string", "example": "Meaningless unless todMode is non-zero." } } }, "todEnd": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" } } }, "todMode": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" } } }, "region": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "Despise" } } }, "landmark": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "Covetous" } } }, "label": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "description": { "type": "string", "example": "Region, else landmark group, else \"Wilderness\"." }, "example": { "type": "string", "example": "Despise" } } } } } } }, "AtlasCreaturePage": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "total": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "description": { "type": "string", "example": "Matching creatures before pagination." }, "example": { "type": "number", "example": 800 } } }, "limit": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 50 } } }, "offset": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 0 } } }, "creatures": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "items": { "$ref": "#/components/schemas/AtlasCreature" } } } } } } }, "AtlasRegion": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "A named region, flattened out of the shard's nested Regions.xml." }, "properties": { "type": "object", "properties": { "facet": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Felucca" } } }, "name": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Despise" } } }, "type": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "description": { "type": "string", "example": "ServUO region class." }, "example": { "type": "string", "example": "DungeonRegion" } } }, "priority": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 50 } } }, "parent": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "Britain" } } }, "rects": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "description": { "type": "string", "example": "The rectangles that placed each spawn point." }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "additionalProperties": { "type": "boolean", "example": true } } } } } } } } }, "AtlasLandmark": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "facet": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Trammel" } } }, "name": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Level 1" } } }, "group": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "description": { "type": "string", "example": "Innermost enclosing parent — the label worth showing." }, "example": { "type": "string", "example": "Covetous" } } }, "x": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 5411 } } }, "y": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 1234 } } }, "z": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 0 } } } } } } }, "AtlasChampion": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "A CONFIGURED champion altar. Not the live board — see GET /public/shard/champs for that." }, "properties": { "type": "object", "properties": { "slug": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "felucca-deceit" } } }, "name": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Deceit" } } }, "group": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "description": { "type": "string", "example": "Spawn group; one altar active per group." }, "example": { "type": "string", "example": "Dungeons" } } }, "type": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "description": { "type": "string", "example": "NULL when the champion is drawn at activation." }, "example": { "type": "string", "example": "UnholyTerror" } } }, "randomType": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": false } } }, "facet": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "Felucca" } } }, "x": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" } } }, "y": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" } } }, "z": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" } } }, "radius": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 60 } } }, "label": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "Deceit" } } } } } } }, "AtlasMeta": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "What atlas is loaded. Game-world facts only: the ServUO path, source hashes and any pending refresh are operator detail and live on the admin status route." }, "properties": { "type": "object", "properties": { "importedAt": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" }, "nullable": { "type": "boolean", "example": true } } }, "generatedAt": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" }, "nullable": { "type": "boolean", "example": true } } }, "counts": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "nullable": { "type": "boolean", "example": true }, "additionalProperties": { "type": "boolean", "example": true }, "example": { "type": "object", "properties": { "facets": { "type": "number", "example": 6 }, "points": { "type": "number", "example": 6455 }, "creatures": { "type": "number", "example": 800 }, "regions": { "type": "number", "example": 387 }, "landmarks": { "type": "number", "example": 558 }, "champions": { "type": "number", "example": 25 }, "unresolvedPoints": { "type": "number", "example": 1086 } } } } }, "facets": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } }, "example": { "type": "array", "example": [ "Felucca", "Ilshenar", "Malas", "TerMur", "Tokuno", "Trammel" ], "items": { "type": "string" } } } } } } } }, "AtlasStatus": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "Admin view of atlas state: where the tree is, whether it is readable, whether it has drifted from what is loaded, and any refresh staged for review." }, "properties": { "type": "object", "properties": { "configured": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } }, "path": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "/srv/servuo" } } }, "treeReadable": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } }, "drift": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "nullable": { "type": "boolean", "example": true }, "description": { "type": "string", "example": "True when the tree's source hashes differ from the loaded atlas. NULL when the tree could not be read." }, "example": { "type": "boolean", "example": false } } }, "facets": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } } } }, "importedAt": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" }, "nullable": { "type": "boolean", "example": true } } }, "counts": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "nullable": { "type": "boolean", "example": true }, "additionalProperties": { "type": "boolean", "example": true } } }, "pending": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "nullable": { "type": "boolean", "example": true }, "description": { "type": "string", "example": "A refresh that was parsed but NOT applied because it would remove a facet. `status` is pending or rejected." }, "additionalProperties": { "type": "boolean", "example": true } } } } } } }, "AtlasRefreshResult": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "Outcome of a refresh. Reported rather than thrown, so an unreadable tree is an answer and not a 500." }, "properties": { "type": "object", "properties": { "status": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "enum": { "type": "array", "example": [ "skipped", "unavailable", "unchanged", "imported", "needsReview", "failed", "rejected", "none" ], "items": { "type": "string" } }, "example": { "type": "string", "example": "imported" } } }, "reason": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true } } }, "path": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true } } }, "counts": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "nullable": { "type": "boolean", "example": true }, "additionalProperties": { "type": "boolean", "example": true } } }, "addedFacets": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } } } }, "removedFacets": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "string" } } } } } } } } }, "ShardLinkRequest": { "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" }, "description": { "type": "string", "example": "The one-time code shown by [link in game." }, "example": { "type": "string", "example": "AB12CD" } } } } } } }, "ShardLinkResult": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "properties": { "type": "object", "properties": { "linked": { "type": "object", "properties": { "type": { "type": "string", "example": "boolean" }, "example": { "type": "boolean", "example": true } } }, "account": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "whitlocktech" } } } } } } }, "ShardLink": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "description": { "type": "string", "example": "A linked in-game account (GET /player/shard/accounts)." }, "properties": { "type": "object", "properties": { "account": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "example": { "type": "string", "example": "whitlocktech" } } }, "userId": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "example": { "type": "number", "example": 42 } } }, "charName": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "nullable": { "type": "boolean", "example": true }, "example": { "type": "string", "example": "Darrow" } } }, "linkedAt": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "format": { "type": "string", "example": "date-time" } } } } } } }, "TownCrierRequest": { "type": "object", "properties": { "type": { "type": "string", "example": "object" }, "required": { "type": "array", "example": [ "id", "lines" ], "items": { "type": "string" } }, "properties": { "type": "object", "properties": { "id": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "maxLength": { "type": "number", "example": 64 }, "description": { "type": "string", "example": "Re-posting the same id replaces the prior entry." }, "example": { "type": "string", "example": "news-42" } } }, "lines": { "type": "object", "properties": { "type": { "type": "string", "example": "array" }, "items": { "type": "object", "properties": { "type": { "type": "string", "example": "string" }, "maxLength": { "type": "number", "example": 200 } } }, "example": { "type": "array", "example": [ "Hear ye!", "Market tax is now 5%." ], "items": { "type": "string" } } } }, "durationSec": { "type": "object", "properties": { "type": { "type": "string", "example": "integer" }, "minimum": { "type": "number", "example": 1 }, "maximum": { "type": "number", "example": 86400 }, "example": { "type": "number", "example": 3600 } } } } } } } } } }