Files
minecraft-account-manager/openapi.yaml
T
dmg 47782b3ccc
CI / validate (push) Successful in 7m15s
Release / release (push) Successful in 11m32s
feat(api): publish validated OpenAPI contract
2026-09-10 15:11:57 -04:00

1705 lines
56 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
openapi: 3.1.0
info:
title: Minecraft Account Manager API
version: 0.1.0
description: Supported application API contract. Root openapi.yaml is canonical and publicly
available at /openapi.yaml; no interactive UI. Includes identity, read-only suggestions and
Velocity integrations. Framework NextAuth routes, browser magic-link flows and server actions
are excluded, as are infrastructure health probes and unknown-route fallbacks. Admin APIs accept
session OR Keycloak bearer; any Authorization header disables session fallback. Machine tokens
require RS256, exact configured issuer, portal audience, expiry, subject and configured-client
administrator role. Obtain client_credentials tokens from
<KEYCLOAK_ISSUER_URL>/protocol/openid-connect/token using a separately authorized
service-account client; see docs/admin-api-authentication.md for safe in-memory usage. Machine
credentials do not grant Velocity access.
servers:
- url: https://portal.somc.club
description: Production portal.
- url: http://localhost:3000
description: Local development
tags:
- name: Identity
description: Administrator identity checks.
- name: Suggestions
description: Read-only configured Discord forum.
- name: Velocity
description: Proxy integrations using a distinct shared secret.
paths:
/api/admin/whoami:
get:
tags:
- Identity
operationId: getAdminIdentity
summary: Verify administrator identity
description: Returns only safe identity fields. No database or Discord access. Supplied
Authorization exclusively selects bearer verification. HEAD is provided by Next.js with the
same checks/status/headers and no body; other unsupported methods receive framework empty
405, OPTIONS is framework-generated.
security:
- AdminSession: []
- AdminBearer: []
responses:
"200":
$ref: "#/components/responses/AdminIdentity"
"401":
$ref: "#/components/responses/AdminUnauthorized"
"403":
$ref: "#/components/responses/AdminForbidden"
"503":
$ref: "#/components/responses/AdminAuthUnavailable"
head:
tags:
- Identity
operationId: headAdminIdentity
summary: Check administrator identity headers
description: Next.js delegates to GET, including authentication; HTTP HEAD has no response body.
security:
- AdminSession: []
- AdminBearer: []
responses:
"200":
$ref: "#/components/responses/ReadHead"
"401":
$ref: "#/components/responses/AdminUnauthorizedHead"
"403":
$ref: "#/components/responses/AdminForbiddenHead"
"503":
$ref: "#/components/responses/AdminAuthUnavailableHead"
/api/suggestions:
get:
tags:
- Suggestions
operationId: listSuggestions
summary: List suggestions
description: "Active posts are newest-created first; archives newest-archived first. Unknown, empty
or repeated query parameters are rejected. Threads may move between lists; this is not a
snapshot. Authorization is checked before every cached/fresh read. Process-local Discord
cache: 30 seconds, 200 entries, eight concurrent requests; responses remain no-store."
security:
- AdminSession: []
- AdminBearer: []
parameters:
- $ref: "#/components/parameters/limit_1"
- $ref: "#/components/parameters/status_2"
- $ref: "#/components/parameters/cursor_3"
responses:
"200":
$ref: "#/components/responses/Suggestions"
"400":
$ref: "#/components/responses/InvalidSuggestionRequest"
"401":
$ref: "#/components/responses/AdminUnauthorized"
"403":
$ref: "#/components/responses/AdminForbidden"
"404":
$ref: "#/components/responses/SuggestionNotFound"
"503":
$ref: "#/components/responses/SuggestionsUnavailable"
head:
tags:
- Suggestions
operationId: listSuggestionsHead
summary: Read headers without a body
description: Implicit Next.js HEAD runs GET with the same authentication, validation and
upstream/cache behavior, but suppresses the response body.
security:
- AdminSession: []
- AdminBearer: []
parameters:
- $ref: "#/components/parameters/limit_1"
- $ref: "#/components/parameters/status_2"
- $ref: "#/components/parameters/cursor_3"
responses:
"200":
$ref: "#/components/responses/ReadHead"
"400":
$ref: "#/components/responses/InvalidSuggestionRequestHead"
"401":
$ref: "#/components/responses/AdminUnauthorizedHead"
"403":
$ref: "#/components/responses/AdminForbiddenHead"
"404":
$ref: "#/components/responses/SuggestionNotFoundHead"
"503":
$ref: "#/components/responses/SuggestionsUnavailableHead"
post:
tags:
- Suggestions
operationId: listSuggestionsPostRejected
summary: Reject unsupported method
description: Read-only endpoint. Authentication is checked first; authorized requests return 405,
without evaluating query/body. No mutation is supported.
security:
- AdminSession: []
- AdminBearer: []
responses:
"401":
$ref: "#/components/responses/AdminUnauthorized"
"403":
$ref: "#/components/responses/AdminForbidden"
"405":
$ref: "#/components/responses/SuggestionsReadOnly"
"503":
$ref: "#/components/responses/AdminAuthUnavailable"
put:
tags:
- Suggestions
operationId: listSuggestionsPutRejected
summary: Reject unsupported method
description: Read-only endpoint. Authentication is checked first; authorized requests return 405,
without evaluating query/body. No mutation is supported.
security:
- AdminSession: []
- AdminBearer: []
responses:
"401":
$ref: "#/components/responses/AdminUnauthorized"
"403":
$ref: "#/components/responses/AdminForbidden"
"405":
$ref: "#/components/responses/SuggestionsReadOnly"
"503":
$ref: "#/components/responses/AdminAuthUnavailable"
patch:
tags:
- Suggestions
operationId: listSuggestionsPatchRejected
summary: Reject unsupported method
description: Read-only endpoint. Authentication is checked first; authorized requests return 405,
without evaluating query/body. No mutation is supported.
security:
- AdminSession: []
- AdminBearer: []
responses:
"401":
$ref: "#/components/responses/AdminUnauthorized"
"403":
$ref: "#/components/responses/AdminForbidden"
"405":
$ref: "#/components/responses/SuggestionsReadOnly"
"503":
$ref: "#/components/responses/AdminAuthUnavailable"
delete:
tags:
- Suggestions
operationId: listSuggestionsDeleteRejected
summary: Reject unsupported method
description: Read-only endpoint. Authentication is checked first; authorized requests return 405,
without evaluating query/body. No mutation is supported.
security:
- AdminSession: []
- AdminBearer: []
responses:
"401":
$ref: "#/components/responses/AdminUnauthorized"
"403":
$ref: "#/components/responses/AdminForbidden"
"405":
$ref: "#/components/responses/SuggestionsReadOnly"
"503":
$ref: "#/components/responses/AdminAuthUnavailable"
options:
tags:
- Suggestions
operationId: listSuggestionsOptionsRejected
summary: Reject unsupported method
description: Read-only endpoint. Authentication is checked first; authorized requests return 405,
without evaluating query/body. No mutation is supported.
security:
- AdminSession: []
- AdminBearer: []
responses:
"401":
$ref: "#/components/responses/AdminUnauthorized"
"403":
$ref: "#/components/responses/AdminForbidden"
"405":
$ref: "#/components/responses/SuggestionsReadOnly"
"503":
$ref: "#/components/responses/AdminAuthUnavailable"
/api/suggestions/{id}:
get:
tags:
- Suggestions
operationId: getSuggestion
summary: Get suggestion
description: "Metadata and original starter, or null when deleted. Query parameters are ignored by
this detail route. Unrelated forum threads are inaccessible. Authorization is checked before
every cached/fresh read. Process-local Discord cache: 30 seconds, 200 entries, eight
concurrent requests; responses remain no-store."
security:
- AdminSession: []
- AdminBearer: []
parameters:
- $ref: "#/components/parameters/id_4"
responses:
"200":
$ref: "#/components/responses/SuggestionDetail"
"400":
$ref: "#/components/responses/InvalidSuggestionRequest"
"401":
$ref: "#/components/responses/AdminUnauthorized"
"403":
$ref: "#/components/responses/AdminForbidden"
"404":
$ref: "#/components/responses/SuggestionNotFound"
"503":
$ref: "#/components/responses/SuggestionsUnavailable"
head:
tags:
- Suggestions
operationId: getSuggestionHead
summary: Read headers without a body
description: Implicit Next.js HEAD runs GET with the same authentication, validation and
upstream/cache behavior, but suppresses the response body.
security:
- AdminSession: []
- AdminBearer: []
parameters:
- $ref: "#/components/parameters/id_4"
responses:
"200":
$ref: "#/components/responses/ReadHead"
"400":
$ref: "#/components/responses/InvalidSuggestionRequestHead"
"401":
$ref: "#/components/responses/AdminUnauthorizedHead"
"403":
$ref: "#/components/responses/AdminForbiddenHead"
"404":
$ref: "#/components/responses/SuggestionNotFoundHead"
"503":
$ref: "#/components/responses/SuggestionsUnavailableHead"
post:
tags:
- Suggestions
operationId: getSuggestionPostRejected
summary: Reject unsupported method
description: Read-only endpoint. Authentication is checked first; authorized requests return 405,
without evaluating query/body. No mutation is supported.
security:
- AdminSession: []
- AdminBearer: []
parameters:
- $ref: "#/components/parameters/id_4"
responses:
"401":
$ref: "#/components/responses/AdminUnauthorized"
"403":
$ref: "#/components/responses/AdminForbidden"
"405":
$ref: "#/components/responses/SuggestionsReadOnly"
"503":
$ref: "#/components/responses/AdminAuthUnavailable"
put:
tags:
- Suggestions
operationId: getSuggestionPutRejected
summary: Reject unsupported method
description: Read-only endpoint. Authentication is checked first; authorized requests return 405,
without evaluating query/body. No mutation is supported.
security:
- AdminSession: []
- AdminBearer: []
parameters:
- $ref: "#/components/parameters/id_4"
responses:
"401":
$ref: "#/components/responses/AdminUnauthorized"
"403":
$ref: "#/components/responses/AdminForbidden"
"405":
$ref: "#/components/responses/SuggestionsReadOnly"
"503":
$ref: "#/components/responses/AdminAuthUnavailable"
patch:
tags:
- Suggestions
operationId: getSuggestionPatchRejected
summary: Reject unsupported method
description: Read-only endpoint. Authentication is checked first; authorized requests return 405,
without evaluating query/body. No mutation is supported.
security:
- AdminSession: []
- AdminBearer: []
parameters:
- $ref: "#/components/parameters/id_4"
responses:
"401":
$ref: "#/components/responses/AdminUnauthorized"
"403":
$ref: "#/components/responses/AdminForbidden"
"405":
$ref: "#/components/responses/SuggestionsReadOnly"
"503":
$ref: "#/components/responses/AdminAuthUnavailable"
delete:
tags:
- Suggestions
operationId: getSuggestionDeleteRejected
summary: Reject unsupported method
description: Read-only endpoint. Authentication is checked first; authorized requests return 405,
without evaluating query/body. No mutation is supported.
security:
- AdminSession: []
- AdminBearer: []
parameters:
- $ref: "#/components/parameters/id_4"
responses:
"401":
$ref: "#/components/responses/AdminUnauthorized"
"403":
$ref: "#/components/responses/AdminForbidden"
"405":
$ref: "#/components/responses/SuggestionsReadOnly"
"503":
$ref: "#/components/responses/AdminAuthUnavailable"
options:
tags:
- Suggestions
operationId: getSuggestionOptionsRejected
summary: Reject unsupported method
description: Read-only endpoint. Authentication is checked first; authorized requests return 405,
without evaluating query/body. No mutation is supported.
security:
- AdminSession: []
- AdminBearer: []
parameters:
- $ref: "#/components/parameters/id_4"
responses:
"401":
$ref: "#/components/responses/AdminUnauthorized"
"403":
$ref: "#/components/responses/AdminForbidden"
"405":
$ref: "#/components/responses/SuggestionsReadOnly"
"503":
$ref: "#/components/responses/AdminAuthUnavailable"
/api/suggestions/{id}/messages:
get:
tags:
- Suggestions
operationId: listSuggestionMessages
summary: List suggestion messages
description: "Newest-first discussion, including the starter if reached. A final empty page is
possible. Unknown, empty or repeated query parameters are rejected. Authorization is checked
before every cached/fresh read. Process-local Discord cache: 30 seconds, 200 entries, eight
concurrent requests; responses remain no-store."
security:
- AdminSession: []
- AdminBearer: []
parameters:
- $ref: "#/components/parameters/id_4"
- $ref: "#/components/parameters/limit_1"
- $ref: "#/components/parameters/cursor_5"
responses:
"200":
$ref: "#/components/responses/SuggestionMessages"
"400":
$ref: "#/components/responses/InvalidSuggestionRequest"
"401":
$ref: "#/components/responses/AdminUnauthorized"
"403":
$ref: "#/components/responses/AdminForbidden"
"404":
$ref: "#/components/responses/SuggestionNotFound"
"503":
$ref: "#/components/responses/SuggestionsUnavailable"
head:
tags:
- Suggestions
operationId: listSuggestionMessagesHead
summary: Read headers without a body
description: Implicit Next.js HEAD runs GET with the same authentication, validation and
upstream/cache behavior, but suppresses the response body.
security:
- AdminSession: []
- AdminBearer: []
parameters:
- $ref: "#/components/parameters/id_4"
- $ref: "#/components/parameters/limit_1"
- $ref: "#/components/parameters/cursor_5"
responses:
"200":
$ref: "#/components/responses/ReadHead"
"400":
$ref: "#/components/responses/InvalidSuggestionRequestHead"
"401":
$ref: "#/components/responses/AdminUnauthorizedHead"
"403":
$ref: "#/components/responses/AdminForbiddenHead"
"404":
$ref: "#/components/responses/SuggestionNotFoundHead"
"503":
$ref: "#/components/responses/SuggestionsUnavailableHead"
post:
tags:
- Suggestions
operationId: listSuggestionMessagesPostRejected
summary: Reject unsupported method
description: Read-only endpoint. Authentication is checked first; authorized requests return 405,
without evaluating query/body. No mutation is supported.
security:
- AdminSession: []
- AdminBearer: []
parameters:
- $ref: "#/components/parameters/id_4"
responses:
"401":
$ref: "#/components/responses/AdminUnauthorized"
"403":
$ref: "#/components/responses/AdminForbidden"
"405":
$ref: "#/components/responses/SuggestionsReadOnly"
"503":
$ref: "#/components/responses/AdminAuthUnavailable"
put:
tags:
- Suggestions
operationId: listSuggestionMessagesPutRejected
summary: Reject unsupported method
description: Read-only endpoint. Authentication is checked first; authorized requests return 405,
without evaluating query/body. No mutation is supported.
security:
- AdminSession: []
- AdminBearer: []
parameters:
- $ref: "#/components/parameters/id_4"
responses:
"401":
$ref: "#/components/responses/AdminUnauthorized"
"403":
$ref: "#/components/responses/AdminForbidden"
"405":
$ref: "#/components/responses/SuggestionsReadOnly"
"503":
$ref: "#/components/responses/AdminAuthUnavailable"
patch:
tags:
- Suggestions
operationId: listSuggestionMessagesPatchRejected
summary: Reject unsupported method
description: Read-only endpoint. Authentication is checked first; authorized requests return 405,
without evaluating query/body. No mutation is supported.
security:
- AdminSession: []
- AdminBearer: []
parameters:
- $ref: "#/components/parameters/id_4"
responses:
"401":
$ref: "#/components/responses/AdminUnauthorized"
"403":
$ref: "#/components/responses/AdminForbidden"
"405":
$ref: "#/components/responses/SuggestionsReadOnly"
"503":
$ref: "#/components/responses/AdminAuthUnavailable"
delete:
tags:
- Suggestions
operationId: listSuggestionMessagesDeleteRejected
summary: Reject unsupported method
description: Read-only endpoint. Authentication is checked first; authorized requests return 405,
without evaluating query/body. No mutation is supported.
security:
- AdminSession: []
- AdminBearer: []
parameters:
- $ref: "#/components/parameters/id_4"
responses:
"401":
$ref: "#/components/responses/AdminUnauthorized"
"403":
$ref: "#/components/responses/AdminForbidden"
"405":
$ref: "#/components/responses/SuggestionsReadOnly"
"503":
$ref: "#/components/responses/AdminAuthUnavailable"
options:
tags:
- Suggestions
operationId: listSuggestionMessagesOptionsRejected
summary: Reject unsupported method
description: Read-only endpoint. Authentication is checked first; authorized requests return 405,
without evaluating query/body. No mutation is supported.
security:
- AdminSession: []
- AdminBearer: []
parameters:
- $ref: "#/components/parameters/id_4"
responses:
"401":
$ref: "#/components/responses/AdminUnauthorized"
"403":
$ref: "#/components/responses/AdminForbidden"
"405":
$ref: "#/components/responses/SuggestionsReadOnly"
"503":
$ref: "#/components/responses/AdminAuthUnavailable"
/api/velocity/access:
post:
tags:
- Velocity
operationId: decideVelocityAccess
summary: Decide Minecraft admission
description: "Uses a separately provisioned per-server shared bearer secret (hashed in plugin
credentials), NOT a Keycloak machine JWT or browser session. The Bearer prefix is
case-sensitive. Requires fresh timestamp and unique request UUID; replay retention is five
minutes. Unknown body properties are stripped. GET/PUT/PATCH/DELETE return RFC 9457 405 with
Allow: POST; implicit HEAD uses GET without a body. OPTIONS is framework-generated."
security:
- VelocitySecret: []
requestBody:
required: true
description: JSON payload; all documented fields required. Example timestamp is illustrative and
must be replaced with current time.
content:
application/json:
schema:
$ref: "#/components/schemas/VelocityAccessRequest"
examples:
representative:
$ref: "#/components/examples/AccessRequest"
responses:
"200":
$ref: "#/components/responses/VelocityDecision"
"400":
$ref: "#/components/responses/InvalidAccessRequest"
"401":
$ref: "#/components/responses/VelocityAccessUnauthorized"
"409":
$ref: "#/components/responses/AccessReplay"
"415":
$ref: "#/components/responses/VelocityUnsupportedMedia"
"503":
$ref: "#/components/responses/VelocityAccessUnavailable"
get:
tags:
- Velocity
operationId: rejectVelocityaccessget
summary: Reject unsupported method
description: Always returns 405 without accessing credentials or the database.
security: []
responses:
"405":
$ref: "#/components/responses/VelocityMethodNotAllowed"
put:
tags:
- Velocity
operationId: rejectVelocityaccessput
summary: Reject unsupported method
description: Always returns 405 without accessing credentials or the database.
security: []
responses:
"405":
$ref: "#/components/responses/VelocityMethodNotAllowed"
patch:
tags:
- Velocity
operationId: rejectVelocityaccesspatch
summary: Reject unsupported method
description: Always returns 405 without accessing credentials or the database.
security: []
responses:
"405":
$ref: "#/components/responses/VelocityMethodNotAllowed"
delete:
tags:
- Velocity
operationId: rejectVelocityaccessdelete
summary: Reject unsupported method
description: Always returns 405 without accessing credentials or the database.
security: []
responses:
"405":
$ref: "#/components/responses/VelocityMethodNotAllowed"
head:
tags:
- Velocity
operationId: rejectVelocityaccesshead
summary: Reject unsupported method
description: Next.js delegates HEAD to the GET rejection and suppresses its body.
security: []
responses:
"405":
$ref: "#/components/responses/VelocityMethodNotAllowedHead"
/api/velocity/connection:
post:
tags:
- Velocity
operationId: recordVelocityConnection
summary: Record a confirmed connection
description: "Uses a separately provisioned per-server shared bearer secret (hashed in plugin
credentials), NOT a Keycloak machine JWT or browser session. The Bearer prefix is
case-sensitive. Requires fresh timestamp and unique request UUID; replay retention is five
minutes. Unknown body properties are stripped. GET/PUT/PATCH/DELETE return RFC 9457 405 with
Allow: POST; implicit HEAD uses GET without a body. OPTIONS is framework-generated."
security:
- VelocitySecret: []
requestBody:
required: true
description: JSON payload; all documented fields required. Example timestamp is illustrative and
must be replaced with current time.
content:
application/json:
schema:
$ref: "#/components/schemas/VelocityConnectionRequest"
examples:
representative:
$ref: "#/components/examples/ConnectionRequest"
responses:
"204":
$ref: "#/components/responses/ConnectionRecorded"
"400":
$ref: "#/components/responses/InvalidConnectionRequest"
"401":
$ref: "#/components/responses/VelocityConnectionUnauthorized"
"404":
$ref: "#/components/responses/UnknownMinecraftAccount"
"409":
$ref: "#/components/responses/ConnectionReplay"
"415":
$ref: "#/components/responses/VelocityUnsupportedMedia"
"500":
$ref: "#/components/responses/FrameworkConnectionFailure"
"503":
$ref: "#/components/responses/VelocityConnectionUnavailable"
get:
tags:
- Velocity
operationId: rejectVelocityconnectionget
summary: Reject unsupported method
description: Always returns 405 without accessing credentials or the database.
security: []
responses:
"405":
$ref: "#/components/responses/VelocityMethodNotAllowed"
put:
tags:
- Velocity
operationId: rejectVelocityconnectionput
summary: Reject unsupported method
description: Always returns 405 without accessing credentials or the database.
security: []
responses:
"405":
$ref: "#/components/responses/VelocityMethodNotAllowed"
patch:
tags:
- Velocity
operationId: rejectVelocityconnectionpatch
summary: Reject unsupported method
description: Always returns 405 without accessing credentials or the database.
security: []
responses:
"405":
$ref: "#/components/responses/VelocityMethodNotAllowed"
delete:
tags:
- Velocity
operationId: rejectVelocityconnectiondelete
summary: Reject unsupported method
description: Always returns 405 without accessing credentials or the database.
security: []
responses:
"405":
$ref: "#/components/responses/VelocityMethodNotAllowed"
head:
tags:
- Velocity
operationId: rejectVelocityconnectionhead
summary: Reject unsupported method
description: Next.js delegates HEAD to the GET rejection and suppresses its body.
security: []
responses:
"405":
$ref: "#/components/responses/VelocityMethodNotAllowedHead"
components:
securitySchemes:
AdminSession:
type: apiKey
in: cookie
name: __Secure-next-auth.session-token
description: Existing NextAuth administrator session. Local HTTP uses next-auth.session-token
instead. Player sessions do not qualify. Used only when Authorization is absent.
AdminBearer:
type: http
scheme: bearer
bearerFormat: JWT
description: Keycloak RS256 access token with configured portal aud and
resource_access[portal-client].roles permission. Never send a client secret here.
VelocitySecret:
type: http
scheme: bearer
description: Separate per-server shared secret, NOT a JWT. Send the exact case-sensitive Bearer
prefix. No session or machine-token alternative.
schemas:
Snowflake:
type: string
pattern: ^[1-9]\d{16,19}$
description: Discord snowflake as a string, never a JSON number. Examples are synthetic.
Problem:
type: object
required:
- type
- title
- status
properties:
type:
type: string
format: uri
title:
type: string
status:
type: integer
minimum: 100
maximum: 599
detail:
type: string
instance:
type: string
extensions:
type: object
additionalProperties: true
properties:
issues:
type: array
items:
type: object
required:
- path
- message
- code
properties:
path:
type: string
message:
type: string
code:
type: string
description: RFC 9457. Validation issues are nested in extensions.issues, not at the top level.
Identity:
oneOf:
- type: object
required:
- authenticationMethod
- subject
- name
- email
properties:
authenticationMethod:
const: bearer
subject:
type: string
minLength: 1
name:
type: "null"
email:
type: "null"
- type: object
required:
- authenticationMethod
- subject
- name
- email
properties:
authenticationMethod:
const: session
subject:
type: "null"
name:
type:
- string
- "null"
email:
type:
- string
- "null"
Tag:
type: object
required:
- id
- name
properties:
id:
$ref: "#/components/schemas/Snowflake"
name:
type: string
Suggestion:
type: object
required:
- id
- title
- authorId
- createdAt
- archived
- locked
- tags
- messageCount
- discordUrl
properties:
id:
$ref: "#/components/schemas/Snowflake"
title:
type: string
authorId:
$ref: "#/components/schemas/Snowflake"
createdAt:
type: string
format: date-time
archived:
type: boolean
locked:
type: boolean
tags:
type: array
items:
$ref: "#/components/schemas/Tag"
messageCount:
type: integer
description: Approximate Discord message count, not votes.
discordUrl:
type: string
format: uri
Message:
type: object
required:
- id
- author
- content
- createdAt
- editedAt
- reactions
- discordUrl
properties:
id:
$ref: "#/components/schemas/Snowflake"
author:
type: object
required:
- id
- name
properties:
id:
$ref: "#/components/schemas/Snowflake"
name:
type: string
content:
type: string
description: Literal text; may be empty due to Message Content intent. No rendered Markdown,
attachments, or embeds.
createdAt:
type: string
format: date-time
editedAt:
type:
- string
- "null"
format: date-time
reactions:
type: array
items:
type: object
required:
- emoji
- count
properties:
emoji:
type: string
count:
type: integer
discordUrl:
type: string
format: uri
SuggestionDetail:
allOf:
- $ref: "#/components/schemas/Suggestion"
- type: object
required:
- originalPost
properties:
originalPost:
anyOf:
- $ref: "#/components/schemas/Message"
- type: "null"
description: Null if the starter was deleted.
SuggestionPage:
type: object
required:
- items
- nextCursor
properties:
items:
type: array
items:
$ref: "#/components/schemas/Suggestion"
nextCursor:
type:
- string
- "null"
description: Null at end; active thread ID or archived UTC timestamp with up to six fractional
digits. Treat as opaque; URL-encode and keep the same status.
MessagePage:
type: object
required:
- items
- nextCursor
properties:
items:
type: array
items:
$ref: "#/components/schemas/Message"
nextCursor:
type:
- string
- "null"
description: Message ID or null. A full last page can yield a cursor followed by an empty page.
VelocityConnectionRequest:
type: object
required:
- requestId
- serverId
- minecraftUuid
- username
- occurredAt
properties:
requestId:
type: string
format: uuid
serverId:
type: string
minLength: 1
maxLength: 100
minecraftUuid:
type: string
pattern: ^[0-9a-fA-F]{32}$
username:
type: string
pattern: ^[A-Za-z0-9_]{3,16}$
occurredAt:
type: string
format: date-time
description: RFC 3339 timestamp; UTC Z recommended, numeric offsets accepted. Must be within 45
seconds of server time.
VelocityAccessRequest:
allOf:
- $ref: "#/components/schemas/VelocityConnectionRequest"
- type: object
required:
- ipAddress
properties:
ipAddress:
anyOf:
- type: string
format: ipv4
- type: string
format: ipv6
VelocityDecision:
type: object
required:
- allowed
- message
properties:
allowed:
type: boolean
message:
type: string
examples:
MachineIdentity:
summary: Synthetic, non-live example
value:
authenticationMethod: bearer
subject: example-machine-subject
name: null
email: null
BrowserIdentity:
summary: Synthetic, non-live example
value:
authenticationMethod: session
subject: null
name: Example operator
email: null
Suggestions:
summary: Synthetic, non-live example
value:
items:
- id: "100000000000000009"
title: Add a community garden
authorId: "100000000000000003"
createdAt: 2026-09-10T00:00:00Z
archived: false
locked: false
tags:
- id: "100000000000000004"
name: Idea
messageCount: 2
discordUrl: https://discord.com/channels/100000000000000001/100000000000000009
nextCursor: null
DeletedStarter:
summary: Synthetic, non-live example
value:
id: "100000000000000009"
title: Add a community garden
authorId: "100000000000000003"
createdAt: 2026-09-10T00:00:00Z
archived: false
locked: false
tags:
- id: "100000000000000004"
name: Idea
messageCount: 2
discordUrl: https://discord.com/channels/100000000000000001/100000000000000009
originalPost: null
Messages:
summary: Synthetic, non-live example
value:
items:
- id: "100000000000000010"
author:
id: "100000000000000003"
name: Example player
content: A shared garden would be fun.
createdAt: 2026-09-10T00:00:00Z
editedAt: null
reactions:
- emoji: 👍
count: 2
discordUrl: https://discord.com/channels/100000000000000001/100000000000000009/100000000000000010
nextCursor: "100000000000000010"
ConnectionRequest:
summary: Synthetic, non-live example
value:
requestId: 00000000-0000-4000-8000-000000000001
serverId: example-proxy
minecraftUuid: "00000000000040008000000000000001"
username: ExamplePlayer
occurredAt: 2026-09-10T00:00:00Z
AccessRequest:
summary: Synthetic, non-live example
value:
requestId: 00000000-0000-4000-8000-000000000001
serverId: example-proxy
minecraftUuid: "00000000000040008000000000000001"
username: ExamplePlayer
occurredAt: 2026-09-10T00:00:00Z
ipAddress: 192.0.2.10
Allowed:
summary: Synthetic, non-live example
value:
allowed: true
message: Account approved.
Denied:
summary: Synthetic, non-live example
value:
allowed: false
message: Please register before joining.
Unauthorized:
summary: Synthetic, non-live example
value:
type: urn:error:unauthorized
title: Unauthorized
status: 401
detail: Administrator authentication is required.
instance: /api/admin/whoami
responses:
AdminIdentity:
description: Successful read.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
content:
application/json:
schema:
$ref: "#/components/schemas/Identity"
examples:
representative:
$ref: "#/components/examples/MachineIdentity"
browser:
$ref: "#/components/examples/BrowserIdentity"
AdminUnauthorized:
description: Missing session or invalid supplied credentials; no redirect and no session fallback.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
WWW-Authenticate:
description: Bearer challenge.
schema:
type: string
const: Bearer realm="admin-api"
content:
application/problem+json:
schema:
allOf:
- $ref: "#/components/schemas/Problem"
- type: object
properties:
status:
const: 401
type:
enum:
- urn:error:unauthorized
examples:
unauthorized:
$ref: "#/components/examples/Unauthorized"
AdminForbidden:
description: Authenticated identity lacks the required administrator permission.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
content:
application/problem+json:
schema:
allOf:
- $ref: "#/components/schemas/Problem"
- type: object
properties:
status:
const: 403
type:
enum:
- urn:error:forbidden
AdminAuthUnavailable:
description: Authentication configuration, session service, or JWKS is unavailable.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
content:
application/problem+json:
schema:
allOf:
- $ref: "#/components/schemas/Problem"
- type: object
properties:
status:
const: 503
type:
enum:
- urn:error:admin-auth-unavailable
ReadHead:
description: Successful read.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
AdminUnauthorizedHead:
description: Missing session or invalid supplied credentials; no redirect and no session fallback.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
WWW-Authenticate:
description: Bearer challenge.
schema:
type: string
const: Bearer realm="admin-api"
AdminForbiddenHead:
description: Authenticated identity lacks the required administrator permission.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
AdminAuthUnavailableHead:
description: Authentication configuration, session service, or JWKS is unavailable.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
Suggestions:
description: Successful read.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
content:
application/json:
schema:
$ref: "#/components/schemas/SuggestionPage"
examples:
representative:
$ref: "#/components/examples/Suggestions"
InvalidSuggestionRequest:
description: Invalid ID, status, cursor, limit or query parameters.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
content:
application/problem+json:
schema:
allOf:
- $ref: "#/components/schemas/Problem"
- type: object
properties:
status:
const: 400
type:
enum:
- urn:error:invalid-request
SuggestionNotFound:
description: Suggestion/message missing, deleted or outside the configured forum. Forum
configuration failures instead use 503.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
content:
application/problem+json:
schema:
allOf:
- $ref: "#/components/schemas/Problem"
- type: object
properties:
status:
const: 404
type:
enum:
- urn:error:suggestion-not-found
SuggestionsUnavailable:
description: Authentication unavailable, forum unconfigured, Discord unavailable, rate limited or
busy. Retry-After is present only for rate limiting/capacity; seconds, no automatic retries.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
Retry-After:
description: Optional; positive whole seconds for discord-rate-limited or discord-busy.
schema:
type: integer
minimum: 1
content:
application/problem+json:
schema:
allOf:
- $ref: "#/components/schemas/Problem"
- type: object
properties:
status:
const: 503
type:
enum:
- urn:error:admin-auth-unavailable
- urn:error:suggestions-not-configured
- urn:error:discord-unavailable
- urn:error:discord-rate-limited
- urn:error:discord-busy
InvalidSuggestionRequestHead:
description: Invalid ID, status, cursor, limit or query parameters.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
SuggestionNotFoundHead:
description: Suggestion/message missing, deleted or outside the configured forum. Forum
configuration failures instead use 503.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
SuggestionsUnavailableHead:
description: Authentication unavailable, forum unconfigured, Discord unavailable, rate limited or
busy. Retry-After is present only for rate limiting/capacity; seconds, no automatic retries.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
Retry-After:
description: Optional; positive whole seconds for discord-rate-limited or discord-busy.
schema:
type: integer
minimum: 1
SuggestionsReadOnly:
description: Suggestions are read-only.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
Allow:
description: Supported reads.
schema:
type: string
const: GET, HEAD
content:
application/problem+json:
schema:
allOf:
- $ref: "#/components/schemas/Problem"
- type: object
properties:
status:
const: 405
type:
enum:
- urn:error:method-not-allowed
SuggestionDetail:
description: Successful read.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
content:
application/json:
schema:
$ref: "#/components/schemas/SuggestionDetail"
examples:
representative:
$ref: "#/components/examples/DeletedStarter"
SuggestionMessages:
description: Successful read.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
content:
application/json:
schema:
$ref: "#/components/schemas/MessagePage"
examples:
representative:
$ref: "#/components/examples/Messages"
VelocityDecision:
description: Admission decision. Policy denial is still HTTP 200, with allowed=false and a
user-facing message.
content:
application/json:
schema:
$ref: "#/components/schemas/VelocityDecision"
examples:
allowed:
$ref: "#/components/examples/Allowed"
denied:
$ref: "#/components/examples/Denied"
InvalidAccessRequest:
description: Malformed JSON or invalid body. Includes extensions.issues.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
content:
application/problem+json:
schema:
allOf:
- $ref: "#/components/schemas/Problem"
- type: object
properties:
status:
const: 400
type:
enum:
- urn:error:invalid-velocity-access-request
VelocityAccessUnauthorized:
description: Missing, invalid or revoked shared server secret, or timestamp outside ±45 seconds. No
admin bearer challenge.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
content:
application/problem+json:
schema:
allOf:
- $ref: "#/components/schemas/Problem"
- type: object
properties:
status:
const: 401
type:
enum:
- urn:error:unauthorized
- urn:error:expired-velocity-access-request
AccessReplay:
description: Request ID already processed. Replay IDs are shared by both Velocity endpoints; use a
fresh UUID for each request.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
content:
application/problem+json:
schema:
allOf:
- $ref: "#/components/schemas/Problem"
- type: object
properties:
status:
const: 409
type:
enum:
- urn:error:replayed-velocity-access-request
VelocityUnsupportedMedia:
description: Content-Type must be application/json (parameters permitted). Credential presence is
checked first.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
content:
application/problem+json:
schema:
allOf:
- $ref: "#/components/schemas/Problem"
- type: object
properties:
status:
const: 415
type:
enum:
- urn:error:unsupported-media-type
VelocityAccessUnavailable:
description: Access decision could not be completed; unexpected failures are sanitized.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
content:
application/problem+json:
schema:
allOf:
- $ref: "#/components/schemas/Problem"
- type: object
properties:
status:
const: 503
type:
enum:
- urn:error:service-unavailable
VelocityMethodNotAllowed:
description: Only POST is supported; method rejection happens without credential verification.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
Allow:
description: Supported method.
schema:
type: string
const: POST
content:
application/problem+json:
schema:
allOf:
- $ref: "#/components/schemas/Problem"
- type: object
properties:
status:
const: 405
type:
enum:
- urn:error:method-not-allowed
VelocityMethodNotAllowedHead:
description: Only POST is supported; method rejection happens without credential verification.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
Allow:
description: Supported method.
schema:
type: string
const: POST
ConnectionRecorded:
description: Connection recorded; no body.
InvalidConnectionRequest:
description: Malformed JSON or invalid body. Includes extensions.issues.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
content:
application/problem+json:
schema:
allOf:
- $ref: "#/components/schemas/Problem"
- type: object
properties:
status:
const: 400
type:
enum:
- urn:error:invalid-velocity-connection-request
VelocityConnectionUnauthorized:
description: Missing, invalid or revoked shared server secret, or timestamp outside ±45 seconds. No
admin bearer challenge.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
content:
application/problem+json:
schema:
allOf:
- $ref: "#/components/schemas/Problem"
- type: object
properties:
status:
const: 401
type:
enum:
- urn:error:unauthorized
- urn:error:expired-velocity-connection-request
UnknownMinecraftAccount:
description: Account no longer registered.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
content:
application/problem+json:
schema:
allOf:
- $ref: "#/components/schemas/Problem"
- type: object
properties:
status:
const: 404
type:
enum:
- urn:error:unknown-minecraft-account
ConnectionReplay:
description: Request ID already processed. Replay IDs are shared by both Velocity endpoints; use a
fresh UUID for each request.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
content:
application/problem+json:
schema:
allOf:
- $ref: "#/components/schemas/Problem"
- type: object
properties:
status:
const: 409
type:
enum:
- urn:error:replayed-velocity-connection-request
FrameworkConnectionFailure:
description: Unhandled credential lookup failure before the transaction catch; framework-owned
response, no stable JSON contract.
VelocityConnectionUnavailable:
description: Transaction failure. Credential database lookup errors occur before the handler catch
and may produce a framework 500, not a Problem Details response.
headers:
Cache-Control:
description: Do not cache this response.
schema:
type: string
const: no-store
content:
application/problem+json:
schema:
allOf:
- $ref: "#/components/schemas/Problem"
- type: object
properties:
status:
const: 503
type:
enum:
- urn:error:service-unavailable
parameters:
limit_1:
name: limit
in: query
description: One decimal integer (13 digits), 1100; defaults to 25. Empty and repeated parameters
are rejected.
schema:
type: integer
minimum: 1
maximum: 100
default: 25
status_2:
name: status
in: query
description: Select active or public archived posts. Keep unchanged when following a cursor.
schema:
type: string
enum:
- active
- archived
default: active
cursor_3:
name: cursor
in: query
description: "Opaque nextCursor: active snowflake or archived UTC Z timestamp (06 fractional
digits). URL-encode it."
schema:
type: string
minLength: 1
id_4:
name: id
in: path
required: true
description: Thread in the configured forum only.
schema:
$ref: "#/components/schemas/Snowflake"
cursor_5:
name: cursor
in: query
description: Opaque message ID from nextCursor; fetch messages before it.
schema:
$ref: "#/components/schemas/Snowflake"