/** * This file was auto-generated by openapi-typescript. * Do not make direct changes to the file. */ export interface paths { "/healthz": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Liveness probe. * @description Unauthenticated on both faces; kubelet and Cloudflare hold no token. */ get: operations["healthz"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/readyz": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Readiness probe (internal face only — readiness is an internal concern). */ get: operations["readyz"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/metrics": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Prometheus metrics (felis_* collectors) on the internal face. * @description Scrape-only infrastructure route, not a product API: the internal listener is ClusterIP-only and a Prometheus scrape carries no token, the same stance as the probes. Serves the felis_* exposition documented in troubleshooting §14; the external face never serves it. */ get: operations["metrics"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/session/minecraft/hasJoined": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Multi-source session verifier (Felis-nano hasJoined multiplexer). * @description Velocity is pointed here with -Dmojang.sessionserver and sends the request itself. Unauthenticated — the vanilla sessionserver protocol carries no token. The query is fanned out to the configured Yggdrasil roots in priority order (the Mojang identity source first); the first source to validate the serverId hash wins. A non-identity source's self-asserted UUID is rewritten into a per-source namespace (UUIDv3) before return, so it can never land in Mojang's UUID space. A rejected or barred login is 204, which Velocity answers with its online-mode-only kick. Any other non-200 status makes Velocity report the auth servers as down. */ get: operations["hasJoined"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List all servers (velocity route table). */ get: operations["listServers"]; put?: never; /** * Create a server (admin). * @description Requires a staff session on the operator console host; the image must be whitelisted. An image in the platform registry is stored pinned to the digest its tag names at creation (name:tag@sha256:…), so a later push over the tag never moves the server; 400 image_not_in_registry when the registry lacks the tag, 503 registry_unavailable when it cannot be asked. */ post: operations["createServer"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/internal/servers/{name}/ready": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Backend readiness callback — the server reports it is accepting players. */ post: operations["serverReadyCallback"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/internal/submissions/{id}/context": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Stream a submission's stored build-context tarball to the build Pod. * @description The build Job's fetch initContainer cannot mount the control-plane uploads PVC (a PVC does not cross namespaces) and holds no object-store credentials, so the API that stored the blob streams it here. Served on the internal face (service token, no Zero Trust). */ get: operations["internalSubmissionContext"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/internal/file-uploads/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Stream one staged file upload to the Job landing it (one-time bearer token). * @description PUT /api/v1/servers/{name}/files/upload stages the body on felis-api's disk and creates a Job to land it in the world volume; the Job fetches the bytes here. The Job holds no service token, so the route is public on the internal face and the bearer token minted with the upload is the whole check. The token opens its upload once. An unknown id, a wrong or missing token and a spent token are all the same 404, so the route says nothing about which uploads exist. */ get: operations["internalFileUpload"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/internal/servers/{name}/join-event": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Player-join event by online-mode UUID (activity tracking / idle reset). */ post: operations["joinEvent"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/internal/servers/{name}/wake": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Domain-autostart wake driven by velocity for a joining player (spec §9.1). * @description velocity holds no web Principal, so it drives the wake lever with its service token, identifying the player by online-mode UUID. Gated by the server's autostartPolicy and the per-server wake cooldown. A server whose start failed with its automatic retries spent answers 409 start_failed: a join never resets the retry budget, so velocity queues no one for it. */ post: operations["internalWake"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/internal/servers/{name}/status": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Server status projection (velocity polls this after a wake). */ get: operations["internalStatus"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/internal/servers/{name}/claim": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Lobby "Claim & Start" by online-mode UUID (spec §12). * @description The felis-paper lobby holds no token, so velocity claims on its behalf, binding the unowned server to the player's linked account. */ post: operations["internalClaim"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/internal/servers/{name}/menu": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Lobby menu projection — status plus the ownership-derived `claimable`. */ get: operations["internalMenuStatus"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/internal/player/menu-access/{mc_uuid}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * What one player may start, for every user server — the lobby menu's per-player verdicts. * @description One call per menu open; each tile's live state stays in the shared per-server projection (…/menu). A server that is up is open to every linked player, so the lobby shows Join there whatever the verdict. The verdicts follow the internal wake's gates without the transient ones (cooldown, running-server cap): `retiring` (given up or being deleted), `start_failed` (automatic restarts spent), `owner` (the player's own), `wake` (the autostart policy admits the player), `owner_only`, and `allowlist` (the player is not on the list). */ get: operations["internalMenuAccess"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/internal/account/link/code": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Mint a one-time account-link code for a verified online-mode UUID (spec §10). * @description Internal-only — the code is born from a UUID the web never holds. */ post: operations["createLinkCode"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/internal/account/link/status/{mc_uuid}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Poll whether an in-game UUID has finished linking — the QR scan-to-login completion check (spec §B3). * @description Internal-only, read-only. After a new player scans the QR-encoded link code and the web verify writes the durable account_links row, velocity polls this for the UUID it minted against and admits the player on linked:true. Keyed by the verified UUID (not the scanned code), so it consumes nothing and is safe to poll repeatedly; an unlinked or never-seen UUID returns linked:false. The response is deliberately just the boolean — the plugin keys everything on the UUID it already holds, so no identity detail crosses back. */ get: operations["linkStatus"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/internal/account/migrate/start": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Put the account linked to a verified in-game UUID into migrate mode (spec §B3 inherit, in-game side). * @description Internal-only. The in-game /felis migrate command calls this for the running player's verified UUID: it resolves the linked account and opens a fresh migration in the initiated state, superseding any earlier unfinished attempt by the same source. The web side then drives a fresh step-up confirmation. The transfer itself moves server ownership only — never the mc_uuid link nor web credentials — so this endpoint starts a flow, it does not move anything. */ post: operations["migrateStart"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/internal/player/reclaim": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Record a Mojang-priority username reclaim — bar the squatter UUID and stash its data (spec §B3). * @description Internal-only. Velocity records a username-collision reclaim: the non-genuine squatter UUID is barred and its world/player data stashed for a 30-day window so a new account can inherit it. Idempotent — a repeat reclaim of an already-barred UUID is a no-op. The bar is keyed by UUID, never the contested name, so the genuine Mojang player always passes. */ post: operations["reclaimUsername"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/internal/player/blacklist/{mc_uuid}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Report whether an in-game UUID was barred by a prior reclaim (spec §B3). * @description Internal-only. The velocity login gate calls it to reject a barred squatter before letting them in; the genuine Mojang UUID — same username, different UUID — is never on the list and always passes. */ get: operations["checkUsernameBlacklist"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/internal/op-login/pending": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List live pending op.console login requests, oldest first (spec §B). * @description Internal-only. Lists the requests awaiting an in-game vouch. Today no plugin consumes it — the staff member reads the request id off the op.console page and an admin approves it with /felis web op approve ; the route exists so velocity can later push the waiting list to online admins. No pending request is secret to the operator crew. */ get: operations["opLoginPending"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/internal/op-login/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Show an in-game admin whose op.console login a request is (spec §B). * @description Internal-only. velocity's /felis web op approve reads this and shows the admin the account, its address, and when and from where the sign-in was started, then asks them to confirm by typing the account name (see approve). The approver's online-mode UUID gets the same check as approve (a linked admin or owner, else 403 not_admin), since the command runs for any player and a staff address must not be readable by one. A request that is unknown, expired, approved or consumed is 404. */ get: operations["opLoginShow"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/internal/op-login/{id}/approve": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Record an in-game admin's vouch for a pending op.console login (spec §B). * @description Internal-only second factor: velocity submits the online-mode UUID of the in-game admin running /felis web op approve , and the account name they typed after seeing the request (GET /api/v1/internal/op-login/{id}). The API resolves the UUID to a linked admin or owner account (else 403 not_admin), requires the typed name to match the request's account ignoring case (else 409 op_login_mismatch, audited, request left pending) and flips the request approved. A missing or no-longer-pending request is 404. Self-approval is allowed — an online staff member vouching as their own admin identity is a genuine second factor distinct from the mailbox. */ post: operations["opLoginApprove"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/internal/servers/{name}/backup": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Break-glass on-demand world backup (service token; server must be stopped). * @description The break-glass console (root on the node, holding the service token) POSTs here to snapshot a stopped world while the API is alive — it goes through the API rather than direct-to-CRD because rendering the backup Job needs deployment coordinates only felis-api holds. Same RWO stopped-gate and async 202 as the external backupNow; there is no Principal (trusted machine caller), and the action is audited to "break-glass". */ post: operations["internalBackupNow"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/wake": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Wake your own server. * @description On a server whose start Failed (phase Failed, desiredState Running) this is a retry: the operator recreates the pod and starts it over with a fresh automatic-restart budget. It is audited as retry_start. */ post: operations["wake"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/stop": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Stop your own server. */ post: operations["stop"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/claim": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Claim an unowned server for your linked account. */ post: operations["claim"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/command": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Run a console command via RCON (spec §8 write). Owner/admin only. * @description The RCON password is never accepted or returned (spec §286). */ post: operations["command"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/console": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Stream the running pod's log over SSE (spec §8 read, §262). Owner/admin only. */ get: operations["serverConsole"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/access/whitelist": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List whitelisted players via RCON (spec §7). Owner/admin only. * @description Runs "whitelist list" against the live server and returns a best-effort parse plus the raw reply. The RCON password is never accepted or returned (spec §286). */ get: operations["accessWhitelistList"]; put?: never; /** * Add or remove a player from the whitelist (spec §7). Owner/admin only. * @description Translates to the RCON "whitelist add|remove " command. The player name is validated against the Minecraft username charset before it is built into a command. The RCON password is never accepted or returned (spec §286). */ post: operations["accessWhitelist"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/access/players": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List online players via RCON (spec §7). Owner/admin only. * @description Runs "list" against the live server and returns the online/max tally, a best-effort parse of the online player names, and the raw reply. This is the only source of WHO is online — Status.Players carries the count alone. The RCON password is never accepted or returned (spec §286). */ get: operations["accessPlayers"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/access/ban": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List banned players via RCON (spec §7). Owner/admin only. * @description Runs "banlist" against the live server and returns a best-effort parse plus the raw reply. The RCON password is never accepted or returned (spec §286). */ get: operations["accessBanList"]; put?: never; /** * Ban or pardon a player (spec §7). Owner/admin only. * @description Translates to the RCON "ban|pardon " command. Carries no reason field (a free-text reason would be an injection vector; the audit log records intent). The RCON password is never accepted or returned (§286). */ post: operations["accessBan"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/access/kick": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Kick a player off the running server (spec §7). Owner/admin only. * @description Translates to the RCON "kick " command. Unlike ban it does not block rejoining. Carries no reason field (a free-text reason would be an injection vector; the audit log records intent). The RCON password is never accepted or returned (spec §286). */ post: operations["accessKick"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/access/permission": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Set or unset a LuckPerms permission node (spec §7). Owner/admin only. * @description Translates to "lp user permission set [world=]" (or unset). An omitted value defaults to true (grant), not false (deny). Player, node and world are charset-validated before the command is assembled. The RCON password is never accepted or returned (spec §286). */ post: operations["accessPermission"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/access/group": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Add or remove a player's LuckPerms parent group (spec §7). Owner/admin only. * @description Translates to "lp user parent add|remove ". Player and group are charset-validated before the command is assembled. The RCON password is never accepted or returned (spec §286). */ post: operations["accessGroup"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/access/luckperms/{player}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Read a player's LuckPerms groups and permission nodes (spec §7). Owner/admin only. * @description Translates to "lp user permission info" over RCON and parses the paginated, colour-coded reply (up to 10 pages) into structured entries. Parent groups (granted group. nodes without a world context) are split out from plain permission nodes. The raw concatenated RCON output is echoed back for anything the parser cannot represent. */ get: operations["accessLuckPermsInfo"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/allowlist": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * The server's wake allowlist, newest first. Owner/admin only. * @description Who may wake the server while it sleeps under autostartPolicy=allowlist. A player lands here by joining the server once; entries whose wake right was taken away stay listed with can_wake false, so it can be given back. Felis keeps this list itself, so it answers whether the server is running or not. A change of owner (claim, reaper release, account deletion) empties it. */ get: operations["listAllowlist"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/allowlist/{uuid}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; /** * Take a player's wake right away or give it back. Owner/admin only. * @description can_wake false keeps the entry on the list with its wake right revoked, so the player's next join does not restore it; true gives it back. Repeating either is harmless. Audited as allowlist.revoke / allowlist.restore. */ put: operations["setAllowlistWake"]; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/retirement": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; /** * Give the server up (owner) or delete it (admin). The reaper carries it out. * @description The server is stopped and the request recorded; the reaper, the one component that deletes a world, carries it out on its next daily run. It archives the world as a released backup (kept for the reaper's retention, recorded against the owner), deletes the world volume and releases the server for anyone to claim; with delete it also removes the server, which frees its name and subdomain. Until then the server cannot be woken or claimed and still counts against the owner's quota, and the request can be cancelled. Repeating it keeps the first request time, and a deletion stays a deletion. confirm must repeat the server's name. Audited as server.release / server.delete. */ put: operations["retireServer"]; post?: never; /** * Cancel a pending retirement. Only an admin cancels a deletion. * @description The server stays stopped; its owner starts it again when they want it. Cancelling when nothing is pending changes nothing. Audited as server.retire_cancel. */ delete: operations["cancelRetire"]; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/status": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Status of a server; the full record for its owner and staff. * @description Anyone signed in may ask. The owner and staff get the whole projection; anyone else gets what the game's own server list shows: name, subdomain, displayName, phase, ready, playersOnline and playersMax. */ get: operations["status"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/auth/options": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Identifier-first login discovery — which methods can this email use (spec §B, * @description Public, pre-session discovery for the SPA's identifier-first form: given a typed email, report which console login methods the account can use (passkey and/or email-OTP) so the UI prompts for the right authenticator. This is the deliberate counter-slice to the anti-enumeration login doors — the ONE sanctioned place account existence is disclosed, so an unknown address returns an empty methods array. It never reveals staffness: methods are computed identically for every resolved account (no role branch), so a staff and a player address in the same credential state return byte-identical bodies. passkey is offered only when a verifier is wired. Sends no mail and mutates nothing; bounded by the per-address sign-in rate limit (429 rate_limited). Gated on local_auth_enabled. */ post: operations["authOptions"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/auth/passkey/login/begin": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Begin a passwordless passkey (WebAuthn) login (spec §14, §B). * @description First leg of the public, pre-session passkey assertion door: the caller supplies the email that selects the account and, on success, receives the raw PublicKeyCredentialRequestOptions to hand to navigator.credentials.get(). The matching challenge is stashed server-side and redeemed by finish. Mounted Public (no prior principal) and gated on local_auth_enabled. An unknown address and a known account with no enrolled passkey both return the SAME 400 no_passkey, so the door is not an existence oracle; the per-address sign-in rate limit bounds probing. Each begin stashes a ceremony of its own beside the account's other live ones, so a begin by anyone who knows the address never cancels its owner's. One network (an IPv4 address or IPv6 /48) holds at most 32 live login challenges (429 too_many_challenges past that). */ post: operations["passkeyLoginBegin"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/auth/passkey/login/finish": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Complete a passkey (WebAuthn) login and mint a session (spec §14, §B). * @description Second leg of the public passkey door: the caller returns the email (to re-select the account) and the raw navigator.credentials.get() assertion. The live login challenge whose value the assertion signed (response.clientDataJSON) is consumed atomically and the assertion is verified against it; on success a host-only felis_session cookie is minted. Both players and staff may log in this way — a passkey is a two-factor authenticator (possession + user verification), strong enough to stand alone without the in-game approval op-login requires. User verification is checked per credential: the passkey must have verified the user when it was bound, and this assertion must verify the user now. Every failure mode (unknown address, no live challenge for the signed value, expired challenge, bad assertion, a credential or assertion without user verification, a cloned authenticator) collapses into one uniform passkey_login_invalid, so the door reveals nothing. */ post: operations["passkeyLoginFinish"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/auth/passkey/login/discoverable/begin": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Begin a usernameless (discoverable) passkey login (spec §14, §B, task * @description First leg of the truly from-zero passkey door: unlike the email-first sibling above, the caller supplies NO identifier — the request has no body (only the application/json Content-Type is required as the cross-origin CSRF guard). The response is the WebAuthn PublicKeyCredentialRequestOptions with an EMPTY allowCredentials, plus an opaque login_id: the authenticator picks a resident credential it holds for this RP and the account is revealed only by the userHandle inside the signed assertion at finish. The challenge cannot be user-keyed, so it is stashed under login_id in a non-user-keyed store and echoed back at finish. Mounted Public and gated on local_auth_enabled. One client is bounded by the per-address sign-in rate limit (429 rate_limited), one network (an IPv4 address or IPv6 /48) to 32 live challenges, and the table by a hard global cap of 16384 (both 429 too_many_challenges). Inert for a credential until its owner enrolls a resident passkey; email-OTP and username-first passkey remain the fallbacks, so no authenticator is ever locked out. */ post: operations["passkeyLoginDiscoverableBegin"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/auth/passkey/login/discoverable/finish": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Complete a usernameless (discoverable) passkey login and mint a session (spec §14, §B, task * @description Second leg of the from-zero door: the caller returns the opaque login_id from begin (the only link to the stashed challenge, since it is not user-keyed) and the raw navigator.credentials.get() assertion — and NOTHING that names an account. The stashed challenge is consumed atomically and the assertion is verified against it; the account is resolved from the authenticator-revealed userHandle (the account's stable id), never from anything the client supplied, and the session is minted for the account the assertion actually resolved AND verified to. Both players and staff may log in this way, with the same per-credential user-verification check as the username-first door. Every failure mode — a missing/expired/consumed login_id, a bad assertion, no user verification, a cloned authenticator, AND a userHandle that resolves to no account — collapses into one uniform passkey_login_invalid, so the door reveals nothing (not even whether the handle was well-formed). */ post: operations["passkeyLoginDiscoverableFinish"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/auth/email/start": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Begin a passwordless email-OTP login — mail a one-time code (spec §B). * @description Public, pre-session console door: the caller supplies an email and, if it resolves to a verified account, a one-time code is mailed under the login purpose. An address with no account returns the SAME 202 with no code minted, and the per-recipient cooldown is kept on that path too, so probing reveals nothing (existence is learnt only at the sanctioned /auth/options oracle). One code is mailed per recipient per minute: a start inside that window gets the same 202 (expires_at of the live code) and mails nothing. A start never cancels the codes already mailed; the three newest live codes all work, and signing in with one spends the rest. An account that spent its daily wrong-code budget (10 per 24h, across every code) also gets the same 202 and no mail until the window ends. Gated on local_auth_enabled. */ post: operations["loginEmailStart"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/auth/email/verify": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Redeem an email-OTP login code into a session (spec §B). * @description Public, pre-session: resolves the address to an account, verifies the code under the login purpose, and on success mints a host-only felis_session. An unknown address, a wrong or expired code, and an attempt-exhausted code all return the IDENTICAL 400 invalid_code, so the door is not an existence or lockout oracle. The 10th wrong code in 24h locks the door for that account until the window ends (the right code then also reads as invalid_code); the owner is told by mail once, and the lock is audited as auth.otp.locked. Staff are refused (403) — but only AFTER a valid code is redeemed, so only the account owner can ever reach that refusal. */ post: operations["loginEmailVerify"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/auth/op-login/start": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Begin an op.console staff login — mail an OTP, open an approval request (spec §B). * @description Public, pre-session first leg of the two-factor operator door: resolves the staff address, opens an op_login request, and mails a one-time code under the op_login purpose, returning the request handle the browser polls. A non-staff or unknown address gets the SAME 202 with a random, non-persisted handle and no mail, so this never becomes a staff-enumeration oracle. A staff account that spent its daily wrong-code budget gets the same neutral 202. One code is mailed per recipient per minute: a staff start inside that window opens a real request but mails nothing, and the code already in the inbox finishes it. A start never cancels the codes already mailed (the three newest live codes all work). Gated on local_auth_enabled. */ post: operations["opLoginStart"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/auth/op-login/status/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Poll whether an op.console login request has been approved in-game (spec §B). * @description Public, pre-session read the browser polls after start. Returns approved:true only for a genuinely approved, live, unconsumed request; every other case — unknown, expired, denied, or already-consumed handle — reads approved:false, so a fabricated handle polls false forever and only an in-game admin vouch can flip it true. */ get: operations["opLoginStatus"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/auth/op-login/finish": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Redeem an approved op.console request plus its mailed code into a staff session (spec §B). * @description Public, pre-session final leg: mints a host-only staff session only when BOTH factors have landed — the request is approved-and-live AND the mailed code verifies. Every failure (unknown handle, not-yet-approved, wrong or locked code, an account past its daily wrong-code budget, an account disabled or deleted since the start, lost race) collapses into one uniform 400 op_login_invalid, so a code-less caller learns nothing. Admin is re-asserted before the session is issued. */ post: operations["opLoginFinish"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/auth/setup/redeem": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Redeem a one-time setup token into a lockdown session (spec §B). * @description Public, pre-session first-run door: consumes the one-time setup token minted by the felis TUI (stored and looked up by SHA-256 hash, like session cookies), mints a host-only felis_session, and returns the remaining setup steps so the SPA can drive the wizard. An unknown, consumed, or expired token returns a uniform 400 setup_token_invalid. Gated on local_auth_enabled. The token is spent in the same transaction that stores the session, so a redemption that fails with 500 leaves the link working for another try. */ post: operations["setupRedeem"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/auth/setup/status": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Report the caller's own setup progress (spec §B). * @description App-tier read the SPA polls after each setup wizard step (email verify, passkey enroll) to decide whether the first-run lockdown can lift. It reads only the principal's own state and is reachable during setup lockdown (the rest of the API is fenced until setup completes). */ get: operations["setupStatus"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/auth/logout": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Revoke the current local session and clear the cookie. * @description Revokes the presented session and clears the cookie (spec §B). Mounted Public and idempotent: it reads the cookie directly, so it works even when the session has already expired and never errors on a missing one. */ post: operations["logout"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/auth/bind": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Redeem a Bind Code into a player account + session (public console bootstrap, spec §10/§B). * @description The one public, pre-account entrypoint of the player console (console.): an account-less player redeems the one-time Bind Code they generated in the in-game Login Lobby, and the platform creates their player account (role=user), binds it to the verified in-game UUID, and mints a host-only session cookie. Safe to expose unauthenticated because the code is minted internal-face only, against an online-mode-verified UUID, with a short TTL and single use — possession already proves control of a Minecraft identity. An already-linked player UUID logs that player back in (idempotent); a UUID that belongs to staff is refused (403) — operators authenticate at op.console behind Zero Trust, so this never mints a session for an admin identity. Requires local sessions to be enabled (same toggle as login). */ post: operations["bindRedeem"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/me": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * The caller's own identity and tier (drives panel navigation). * @description Returns the authenticated principal's user id, email, role and the server-computed is_admin (Principal.IsAdmin(): role admin reached on the operator console host). The panel reads this once at boot to decide which surfaces to render. It is UX truth, not a security control — admin routes are independently gated server-side, so a hidden nav item never widens access. */ get: operations["me"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/me/servers": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List the servers the caller owns or may claim. */ get: operations["myServers"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/updates/window": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Read the SysAdmin-set auto-update maintenance window (admin). * @description Advisory: Felis applies no update on its own. `felis update` on the host reads this window, reports where now sits against it, and warns before an apply outside it. The single platform-wide maintenance window during which Felis may apply a Scheduled component's update to itself (decision core internal/updates). An unset window — never set, or explicitly cleared — reads back as {start:null,end:null}. API+persistence only: nothing consumes the window until the INTEGRATION runner and executors are wired, so setting it changes no behavior yet. */ get: operations["getUpdateWindow"]; /** * Set or clear the SysAdmin auto-update maintenance window (admin). * @description Persist the maintenance window as an absolute [start,end) interval. Both ends must be set with end strictly after start, or both null to clear the window to unset. A half-set (exactly one end) or inverted/empty (end not after start) body is rejected 400, mirroring the decision core's fail-closed Window so a malformed schedule can never be stored. No forced auto-update: setting a window only permits an apply inside it; outside, a Scheduled component degrades to notify. */ put: operations["setUpdateWindow"]; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/platform/db-backup": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Freshness of the newest control-plane database backup (admin). * @description What the host's felis-db-backup.timer (or a manual `felis db backup`) last recorded in platform_settings. last is null before the first backup; stale is true then, and whenever the newest daily backup (last.daily_at) is missing or older than max_age_seconds. Read-only: backups run on the host, never through the API. */ get: operations["getDBBackup"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/updates/report": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * The newest recorded version check of every tracked component (admin). * @description What `felis update --record` last stored in platform_settings; the installer's felis-update-check.timer runs it daily on the host, where the installed versions are readable. report is null before the first check; stale is true then, and whenever the check is older than max_age_seconds. Read-only: Felis applies no update on its own. */ get: operations["getUpdateReport"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/fleet": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * The SysAdmin cockpit's fleet-wide server list (admin; cockpit extension, not a spec §7 route). * @description Every MinecraftServer's CRD+status lifecycle view, for the SysAdmin FleetTable. Admin-tier — it reads every owner's server. A path distinct from the internal velocity GET /api/v1/servers because one {method, path} cannot carry both the service and admin tiers. Lifecycle is CRD truth (§1); the owner is the only business field, joined READ-ONLY from Postgres (§6) for display — best-effort, so a Postgres blip degrades to owner-less rows. */ get: operations["fleet"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/backups": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List world backups (admin sees all; a user sees only worlds they formerly owned). * @description One page of the present backups in the caller's scope, newest first. server narrows the page to one server's backups inside that scope; it never widens it. */ get: operations["listBackups"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/backups/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** * Delete one world backup (admin, or the user who owned the world). * @description The backup leaves every list, restore and the backup budget at once; the reaper's next daily run deletes the archive and the off-site copy's next sync removes the bucket's copy. A user gets 404 for a backup outside their scope, as their list never shows it. Refused while a restore on the backup's server may still read it. */ delete: operations["deleteBackup"]; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/restore-backup": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Restore a world from a backup (owner-or-admin plus a former-owner match). * @description By default the restore starts with a safety snapshot: a backup of the data volume as it is now (reason "pre_restore", the newest 3 kept per server), and the restore Job starts only once that backup has succeeded. If the snapshot fails the restore is given up and the world is left as it was. GET /servers/{name}/jobs shows the snapshot as a backup job whose then_restore says what became of the restore. The world stays locked from the request until the restore Job finishes. Pass safety_snapshot false to restore straight away. */ post: operations["restoreBackup"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/backup": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Back up a server's data volume on demand (owner-or-admin; server must be stopped). * @description Snapshots the server's whole data volume (worlds, config, plugins/mods, jars, libraries — not just world folders) into the archive store as a first-class world_backups row (reason "manual"), restorable later like an inactivity backup. A restore replaces the volume with the archive. The world PVC is RWO and held by a running server, so the server must be fully stopped first (409 not_stopped otherwise). The backup runs asynchronously as a Job, so success is 202 (backing_up). */ post: operations["backupNow"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/jobs": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Latest async world operations (backup/restore) for a server (owner-or-admin). * @description Backup and restore run as cluster Jobs, so a 202 that later failed left its only trace in the Job object. This route projects the newest such Jobs, newest first, so failures are observable without kubectl. State is "running" | "succeeded" | "failed". */ get: operations["listServerJobs"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/files": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List a directory in a server's world volume (owner-or-admin; server must be stopped). * @description Lists one directory inside the server's world volume — the repair lever for a server that will not boot because a config file is wrong. The world PVC is RWO and held by a running server, so the server must be fully stopped first (409 not_stopped otherwise). The listing runs as a one-shot Job whose output is read back through pods/log, so the call is synchronous but takes seconds rather than milliseconds. Paths are resolved inside the world root by os.Root, so "..", an absolute path, and a symlink leaving the root are all refused with 400 bad_path. Listings are capped; truncated reports that the cap was hit. */ get: operations["listServerFiles"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/file": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Read a file from a server's world volume (owner-or-admin; server must be stopped). * @description Returns one file's bytes, base64-encoded, from inside the server's world volume. Same stopped-gate and os.Root containment as the directory listing. Reads are capped at 1 MiB; a larger file is 413 rather than a truncated read, because a config editor that silently returned half a file would let a subsequent save destroy the other half. */ get: operations["readServerFile"]; /** * Write a file in a server's world volume (owner-or-admin; server must be stopped). * @description Replaces a file's contents, creating the file if absent but never creating its parent directories. Content is base64 so arbitrary bytes (CRLF endings, a BOM) survive intact. Writes are capped at 256 KiB — the Job spec carries the content, and etcd bounds the object — so a larger body is 413. Same stopped-gate and os.Root containment as the read; a write through a symlink leaving the world root is refused. The replacement is atomic (a synced temporary sibling renamed over the file, keeping its mode), so a failed write leaves the old file whole. With expect_sha256 the write lands only if the file still has that hash; otherwise 409 file_changed. Audited as file.write. */ put: operations["writeServerFile"]; post?: never; /** * Delete a file or folder in a server's world volume (owner-or-admin; server must be stopped). * @description Deletes a file, a symlink (never what it points at) or a folder with everything in it. The world root itself is refused (400 bad_path). Same stopped-gate, world lock and os.Root containment as a write. The panel confirms first; this route does not. Audited as file.delete. */ delete: operations["deleteServerFile"]; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/files/mkdir": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Make a folder in a server's world volume (owner-or-admin; server must be stopped). * @description Makes one folder. Its parent must already exist (404), and nothing may be at the path yet (409 file_exists). Same stopped-gate, world lock and os.Root containment as a write. Audited as file.mkdir. */ post: operations["makeServerFolder"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/files/rename": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Move or rename a file or folder in a server's world volume (owner-or-admin; server must be stopped). * @description Moves the file or folder at path to to. It never replaces: an existing destination is 409 file_exists, and a missing destination folder is 404. server.properties, config/paper-global.yml and config/ cannot be moved under any name they are reached by (400 bad_path), because elsewhere the read path would no longer withhold their secrets. Same stopped-gate, world lock and os.Root containment as a write. Audited as file.rename. */ post: operations["renameServerFile"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/files/upload": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; /** * Upload a file into a server's world volume (owner-or-admin; server must be stopped). * @description Lands the raw request body as the file at path, up to 64 MiB — a plugin jar, a datapack, a world region. Content-Length is required (411 length_required). An existing file is 409 file_exists unless overwrite=true; a folder at the path is 400 bad_path either way. The body is staged on felis-api's disk first and then fetched by the file Job with a one-time token, so the world lock is taken only after the body has arrived and a slow upload holds off no backup. The file lands atomically: a synced temporary sibling is checked against the staged size and SHA-256, then renamed into place, so a failed upload leaves the old file whole. Same stopped-gate and os.Root containment as a write. Audited as file.upload. */ put: operations["uploadServerFile"]; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/schedules": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List a server's scheduled tasks (owner-or-admin). */ get: operations["listServerSchedules"]; put?: never; /** * Add a scheduled task to a server (owner-or-admin). * @description The task belongs to the server's current owner: once the server has another owner felis-api disables it instead of running it, until somebody saves it again. felis-api checks the tasks every 15 seconds; a run it was down for is started late, up to 10 minutes, and dropped as missed after that. A command runs only on a running server, a restart only restarts a running one, and a start goes through the running-server cap and a pending retirement like a wake. Audited as schedule.create; each run as schedule.run by scheduler. */ post: operations["createServerSchedule"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/schedules/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; /** * Change a scheduled task (owner-or-admin). * @description Replaces the task's settings and recomputes its next run. The task passes to the server's current owner. Refused while a run is in progress. Audited as schedule.update. */ put: operations["updateServerSchedule"]; post?: never; /** * Remove a scheduled task (owner-or-admin). * @description Refused while a run is in progress. Audited as schedule.delete. */ delete: operations["deleteServerSchedule"]; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}/schedules/{id}/run": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Run a scheduled task now (owner-or-admin). * @description Starts a run at once, without the players' warning, whether the task is enabled or not; its next scheduled run stays where it was. The answer is the task after the run's first step: a command, stop or start has finished, and a restart or backup goes on in the background (run_state). Audited as schedule.run_now, and the run itself as schedule.run. */ post: operations["runServerSchedule"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/users": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List users (admin only). * @description Returns a page of non-deleted users matching optional query filters, newest first. Every route under /users gates on the admin Zero-Trust path. */ get: operations["listUsers"]; put?: never; /** Create a user (admin only). */ post: operations["createUser"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/users/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Get user detail (admin only). */ get: operations["getUser"]; put?: never; post?: never; /** Soft-delete a user — releases servers, revokes sessions (admin only, cannot delete self). */ delete: operations["deleteUser"]; options?: never; head?: never; /** Edit a user (admin only, cannot patch self). */ patch: operations["patchUser"]; trace?: never; }; "/api/v1/users/{id}/disable": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Disable or re-enable a user (admin only, cannot disable self). * @description Disabling a user additionally revokes every live session so the lockout is immediate. Re-enabling simply clears the flag. */ post: operations["disableUser"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/users/{id}/quotas": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Get a user's quotas (admin only). */ get: operations["getQuotas"]; /** * Set a user's quotas (admin only). * @description Replaces all four caps at once. An absent or null field is unlimited; 0 grants none of that resource, so every claim that needs it is refused. An empty body lifts every cap. A cap below what the user already owns refuses new claims and leaves the servers they have alone. */ put: operations["setQuotas"]; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/users/{id}/sessions": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List a user's live sessions (admin only). */ get: operations["listUserSessions"]; put?: never; post?: never; /** Revoke every live session of a user (admin only). */ delete: operations["revokeUserSessions"]; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/users/{id}/sessions/{hash}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** Revoke a single session of a user (admin only). */ delete: operations["revokeUserSession"]; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/users/{id}/passkeys": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** * Unbind every passkey of a user (owner only) — authenticator remediation. * @description Severs a compromised or planted authenticator that would otherwise outlive a session revoke. A complete remediation pairs this with revoking the user's sessions (DELETE /users/{id}/sessions/{hash}): unbinding the credential alone leaves the live hijacked session, and revoking sessions alone leaves a re-enrollable credential. It is not a lockout — the account re-enters via the email-OTP door or op-login and re-enrolls. Removing zero passkeys is a 200 no-op, not a 404. */ delete: operations["unbindUserPasskeys"]; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/users/{id}/links": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Force-link a Minecraft UUID to a user, bypassing the code-verification flow (admin only). * @description The UUID must not already be bound to a different user (409). Same (user, uuid) pair is idempotent (200). When auth_source is omitted it is derived from the UUID's version nibble exactly as on the mint path (v3 → thirdparty, else mojang), so a force-linked thirdparty account keeps its reclaim-guard protection. */ post: operations["linkAccount"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/users/{id}/links/{mc_uuid}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** Remove a single Minecraft UUID binding from a user (admin only). */ delete: operations["unlinkAccount"]; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/link/start": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Report account-link status and in-game instructions (web side, spec §10). */ post: operations["linkStart"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/link/verify": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Consume an in-game link code and bind the account. */ post: operations["linkVerify"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/email/start": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Mint and deliver an email one-time code for the caller (web onboarding, spec §B2). * @description Generates a one-time code bound to the authenticated principal and the supplied address, persists only its hash, and delivers it out of band. The code is never returned in the response. A re-request supersedes the prior unconsumed code. Once the account has a passkey or a verified email, the session must have reauthed within 5 minutes (403 reauth_required). */ post: operations["emailOtpStart"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/email/verify": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Redeem an email one-time code and mark the caller's email verified (spec §B2). * @description Consumes a previously delivered code for the authenticated principal. On success the user's email is written and email_verified is set true. When the new address replaces a different verified one, every other session of the caller is signed out: sign-in codes now go to the new address, so a session opened through the old one ends; the old address is mailed a notice with the new one masked. A verified code also counts as a reauth for this session. Too many incorrect attempts lock the code (429 otp_locked); 10 wrong codes in 24h, counted across every code, lock the account's email-code door until the window ends (429 otp_account_locked with Retry-After). An unknown, expired, consumed, or mismatched code is a 400. */ post: operations["emailOtpVerify"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/email": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Record the caller's email WITHOUT verifying it (setup bootstrap, spec §B2). * @description Writes the supplied address to the authenticated principal's user row and clears email_verified (already false for a fresh Owner). The setup bootstrap has no SMTP, so the Owner cannot receive an emailed code; a later Settings/SMTP flow proves control of the address via /account/email/verify. Clearing a verified address strips a factor, so once the account has one the session must have reauthed within 5 minutes (403 reauth_required). */ post: operations["setEmail"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/passkey/register/begin": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Begin a passkey (WebAuthn) registration ceremony for the caller (spec §14, Phase 6 bind). * @description Mints a credential-creation challenge bound to the authenticated principal, stashes the server-side ceremony state under a short TTL, and returns the WebAuthn publicKey creation options for navigator.credentials.create(). The challenge is never echoed by the client. Once the account has a passkey or a verified email, the session must have reauthed within 5 minutes (403 reauth_required). 503 when the WebAuthn verifier is not configured on this instance. */ post: operations["passkeyRegisterBegin"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/passkey/register/finish": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Finish a passkey registration ceremony and bind the credential (spec §14, Phase 6 bind). * @description Consumes the caller's live registration challenge (single-use), verifies the authenticator's attestation against the server-stashed ceremony state, and persists the public credential. A missing or expired ceremony is a 400; an attestation that fails verification is a 400; a credential already bound to any account is a 409. The verified email is mailed a notice, and the ceremony counts as a reauth for this session. 503 when the WebAuthn verifier is not configured. */ post: operations["passkeyRegisterFinish"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/passkey/credentials": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List the passkeys the caller has bound (spec §14, Phase 6 bind). * @description Returns the authenticated principal's own bound passkeys, newest first, as display projections (never the public key). Reading the credential list does not need the WebAuthn verifier, so it succeeds even where begin/finish report 503. */ get: operations["passkeyList"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/passkey/credentials/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** * Unbind one of the caller's passkeys (spec §14, Phase 6 bind). * @description Removes a passkey scoped to the authenticated principal, so a caller can only unbind their OWN credential. An unknown or cross-user id is a 404; it never silently no-ops as success. The account's only passkey cannot be removed while its email is unverified (409 last_passkey): it is then the account's only durable way in. Removing a passkey signs out every other session of the caller, so a session opened with that passkey ends with it, and mails the verified email a notice. The session must have reauthed within 5 minutes (403 reauth_required). */ delete: operations["passkeyDelete"]; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/reauth": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Say whether a passkey or email change needs a reauth first, and how to give one. * @description needed is true when the account has a passkey or a verified email and this session has not proven one within the last 5 minutes. until is when the current proof stops counting. factors lists the ways this caller can reauth, best first: passkey (an enrolled passkey), email (a player's verified address), sign_in (an operator signs out and back in through op-login or a passkey). */ get: operations["reauthStatus"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/reauth/passkey/begin": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Begin a passkey assertion that reauths this session. * @description Returns WebAuthn assertion request options over the caller's own passkeys, bound to a fresh reauth-purpose challenge. */ post: operations["reauthPasskeyBegin"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/reauth/passkey/finish": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Finish the passkey assertion and mark this session reauthed for 5 minutes. * @description Verifies the assertion against the reauth challenge with the login door's user-verification and clone checks (a credential or assertion without user verification, or a cloned authenticator, is 400 passkey_login_invalid). */ post: operations["reauthPasskeyFinish"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/reauth/email/start": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Mail a reauth code to the caller's verified address. * @description For players with a verified email. Operators reauth with a passkey or by signing in again (403 staff_reauth). */ post: operations["reauthEmailStart"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/reauth/email/verify": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Redeem the reauth code and mark this session reauthed for 5 minutes. */ post: operations["reauthEmailVerify"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/sessions": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List the caller's own live sessions, marking the one this request came in on. * @description Every device signed in to the caller's account, most recently seen first, with the one this request came in on marked current. */ get: operations["listMySessions"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/sessions/{hash}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** * Sign out one of the caller's sessions. * @description Scoped to the caller: a hash that is not one of the caller's live sessions is a 404 whoever it belongs to. Revoking the session the request came in on is a sign-out; the cookie is cleared and signed_out is true. */ delete: operations["revokeMySession"]; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/sessions/revoke-others": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Sign out every session of the caller except the one making this request. */ post: operations["revokeMyOtherSessions"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/migrate": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Report the caller's active account-migration and where it is in the flow (spec §B3 inherit, web side). * @description Read-only. Returns the live migration whose source is the authenticated principal, if any, so the web onboarding can resume the flow: whether a confirmation step-up is still needed, which factor confirmed it and until when, the named target, and the one-time code's expiry once issued. The step-up counts only for the session that gave it and for 10 minutes, so a confirmation made in another session, one that lapsed, and a code that expired unspent all read as initiated. active:false when the caller has no live migration. */ get: operations["migrateStatus"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/migrate/confirm/otp/start": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Send a fresh email one-time code to confirm control of the migrating source account (spec §B3 step-up). * @description Opens the email-OTP confirmation factor for the caller's initiated migration. This is a FRESH step-up bound to the migrate purpose, never mere session possession. If the account has ANY passkey enrolled, email-OTP is refused with 409 passkey_required — the stronger factor is forced. The code is delivered out of band and never returned; requires a verified email on the account. */ post: operations["migrateConfirmOtpStart"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/migrate/confirm/otp/verify": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Redeem the email one-time code and confirm the migration (spec §B3 step-up). * @description Consumes the fresh migrate-purpose email code for the caller's initiated migration and advances it to confirmed with confirm_factor email_otp. Too many wrong attempts lock the code (429 otp_locked), and 10 wrong codes in 24h lock the account's email-code door (429 otp_account_locked with Retry-After); an unknown, expired, consumed, or mismatched code is a 400 invalid_code. */ post: operations["migrateConfirmOtpVerify"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/migrate/confirm/passkey/begin": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Begin a fresh passkey assertion to confirm control of the migrating source account (spec §B3 step-up). * @description Returns WebAuthn assertion request options for the caller's own enrolled passkeys, bound to a fresh migrate-purpose challenge. This is the forced factor whenever a passkey exists. The finish call proves the assertion and, exactly as the login door does, runs the clone-signal (sign-count) check before confirming. */ post: operations["migrateConfirmPasskeyBegin"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/migrate/confirm/passkey/finish": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Finish the passkey assertion and confirm the migration (spec §B3 step-up). * @description Verifies the WebAuthn assertion against the fresh migrate-purpose challenge and, like the login door, applies the per-credential user-verification check and the authenticator sign-count clone check: either refusal fails closed (400 passkey_login_invalid) and is audited. On success the migration advances to confirmed with confirm_factor passkey. */ post: operations["migrateConfirmPasskeyFinish"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/migrate/issue-code": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Name the target account and mint the one-time migration code (spec §B3 inherit). * @description For a migration confirmed by a step-up in this same session within the last 10 minutes, binds the named target account and mints a single one-time code (only its hash is stored) that the target must redeem while logged in AS that target — an intercepted code is useless to anyone else. The target must exist and be neither disabled nor soft-deleted, and cannot be the source. The source's verified address is sent a notice naming the target and the expiry, and another when the code is redeemed. */ post: operations["migrateIssueCode"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/account/migrate/redeem": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Redeem a migration code as the named target and inherit the source's owned servers (spec §B3 inherit). * @description The authenticated caller — who must be the target named at issue time — spends the one-time code. In a single atomic step the source's owned servers are re-pointed to the caller and the source account is retired (disabled and soft-deleted), which also spends the code so it cannot be replayed. The caller keeps its own in-game identity and credentials; only server ownership moves. */ post: operations["migrateRedeem"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/me/submissions": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List the caller's own modpack submissions with each linked build's outcome (user-directed lane over §16). */ get: operations["mySubmissions"]; put?: never; /** Submit a modpack for admin review (user side; user-directed lane over §16). Starts no build. */ post: operations["createSubmission"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/me/submissions/limits": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * The per-upload build-context cap * @description The effective [registry] context_max_bytes, 1 GiB by default. The panel checks a file against it before upload and sends the file through the chunked upload (/api/v1/me/submissions/{id}/context/upload), so the cap holds behind the Cloudflare edge too, whose proxy refuses a single body over 100 MB. */ get: operations["submissionLimits"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/me/submissions/{id}/context": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Upload the modpack build context for your own pending submission (user side; user-directed lane over §16). * @description The request body IS the raw gzip build context (context.tar.gz) — not JSON, not multipart — streamed to the platform-derived, id-namespaced location Kaniko reads via --context. The submitter is taken from the principal; a submission the caller does not own is reported as 404, so this endpoint cannot upload to or probe another user's submission. Only a pending_review submission accepts a context (409 otherwise), and one withdrawn or deleted while its context streams in answers 404 with the bytes discarded; a wrong-format or oversize body is rejected with 400 (the per-upload cap is [registry] context_max_bytes, 1 GiB by default; GET /api/v1/me/submissions/limits reports it so a client can check a file before sending it). This request carries the whole context, so behind the Cloudflare edge, whose proxy refuses bodies over 100 MB with its own HTML 413 before they reach the API, a larger context goes through the chunked upload at /api/v1/me/submissions/{id}/context/upload instead. An upload that would push the caller past their per-user stored-context budget is refused with 403 before the excess is persisted. Returns 503 when the deployment's context store has no implemented upload transport. */ post: operations["uploadSubmissionContext"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/me/submissions/{id}/context/upload": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Where your chunked context upload stands (the resume point). * @description The chunked form of POST /api/v1/me/submissions/{id}/context, for a context larger than one request carries through the edge. received is how many bytes are staged: the next part starts there. A client reads it before the first part and again after a failed one. Nothing staged reads as 0. Same owner scoping as the single upload (404 for another user's submission, 409 once reviewed). */ get: operations["getContextUpload"]; /** * Append one part of your chunked context upload. * @description The body is the part's raw bytes, at most part_max_bytes (32 MiB). offset is where they start: 0 starts the upload over, and anything else must equal the staged length, or the answer is 409 upload_offset_mismatch and the client reads GET for where to resume. The first part must open with the gzip magic (400). The staged total meets the same context cap (400) and storage budget (403) as a single upload. A part that breaks off is cut back off, so the staged bytes are always a prefix of the file. One request per upload at a time (409 upload_busy). Staged bytes untouched for 24 hours are deleted. The budget check reads blob sizes remembered for up to a minute; when a size has to be read and the uploads store does not answer, the answer is 503 uploads_store_unavailable with Retry-After, and the same part can be sent again. */ put: operations["putContextUploadPart"]; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/me/submissions/{id}/context/upload/complete": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Store your staged chunked upload as the submission's build context. * @description Runs every check of POST /api/v1/me/submissions/{id}/context on the staged bytes (format, cap, budget, room), records the digest the same way, and deletes the staged copy. Holds the same per-user upload cooldown (429) and writes the same submission.upload audit event. Nothing staged is 400. After a failure the staged bytes stay, for a retry. The budget is checked against every blob's size read from the uploads store; a store that does not answer is 503 uploads_store_unavailable with Retry-After. */ post: operations["completeContextUpload"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/me/submissions/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** * Withdraw your own pending submission (user side; user-directed lane over §16). * @description Retracts the caller's own submission while it is still pending review: the row and its uploaded build context are deleted, freeing the pending slot and the per-user storage budget for a fresh submission. A reviewed submission is frozen (409 — its build may already be consuming the context), and a submission the caller does not own reads back as 404, so this endpoint cannot probe or clear another user's uploads. */ delete: operations["withdrawSubmission"]; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/servers/{name}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; delete?: never; options?: never; head?: never; /** Mutate a server spec (admin). Storage is immutable. */ patch: operations["patchServer"]; trace?: never; }; "/api/v1/images/build": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List builds (admin), newest first. * @description One page of the build history across every admin. Rows are read as stored (the reconcile loop advances them; GET /images/build/{id} reconciles one on demand) and leave out the Dockerfile, which GET /images/build/{id} returns. */ get: operations["listBuilds"]; put?: never; /** Submit an image build (admin). A build is build-time RCE against the cluster. */ post: operations["buildImage"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/images/build/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Get one build's status (admin). */ get: operations["getBuild"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/images/build/{id}/logs": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Stream a build's Job log over SSE (admin, spec §16 / §416). */ get: operations["buildLogs"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/images/build/{id}/cancel": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Cancel a running build (admin). */ post: operations["cancelBuild"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/images/build/{id}/scan": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** The scan gate's verdict and findings for a build (admin). */ get: operations["getBuildScan"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/images/build/{id}/scan/report": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Download a build's full Trivy JSON report (admin). */ get: operations["getBuildScanReport"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/images/build/{id}/sbom": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Download a build's CycloneDX SBOM (admin). */ get: operations["getBuildSBOM"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/images": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List whitelisted images (admin). */ get: operations["listImages"]; put?: never; /** Whitelist an externally-built image by reference (admin). */ post: operations["addImage"]; /** Remove an image from the whitelist by reference (admin). */ delete: operations["removeImage"]; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/submissions": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** The admin review queue — every user's modpack submissions (admin; user-directed lane over §16). */ get: operations["listSubmissions"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/submissions/{id}/approve": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Approve a submission and start its Trivy-gated build (admin; user-directed lane over §16). Approval is layered in front of the scan, never instead of it. */ post: operations["approveSubmission"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/submissions/{id}/context": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Download a submission's uploaded build context (admin; user-directed lane over §16). * @description The reviewer's read path to the artifact they are about to approve: the executed Dockerfile lives inside this tarball (Kaniko runs the context's root `Dockerfile`), so without it the human gate would be blind. Streams the stored context.tar.gz verbatim with an attachment disposition — the same bytes the build Pod fetches over the internal face. 404 when the submission is unknown or has no uploaded context; 503 when the deployment's context store has no implemented transport. */ get: operations["downloadSubmissionContext"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/submissions/{id}/reject": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Reject a submission with a required reason (admin; user-directed lane over §16). Starts no build. */ post: operations["rejectSubmission"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/api/v1/submissions/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** * Retire a submission outright — row and uploaded context (admin; user-directed lane over §16). * @description Removes the submission and its uploaded build context, any status — the lane's only lifecycle valve, and the path that reclaims a rejected or consumed upload from the uploads PVC. The reviewer identity is recorded in the audit event, not on the (now deleted) row. Deleting an approved submission whose build is still running fails that build's context fetch; the admin has explicitly chosen to retire the artifact. */ delete: operations["deleteSubmission"]; options?: never; head?: never; patch?: never; trace?: never; }; } export type webhooks = Record; export interface components { schemas: { /** @description Uniform error envelope emitted by every handler (internal/api/errors.go). */ Error: { error: { /** @description Stable machine-readable code (e.g. not_found, conflict, forbidden, bad_request). */ code: string; message: string; /** @description Correlates the response with server logs (withRequestID middleware). */ request_id?: string; }; }; /** @description The SysAdmin-set auto-update maintenance window (internal/api/handlers_updates.go updateWindow). An absolute [start,end) interval during which Felis may apply a Scheduled component's update to itself; both ends null means unset (no apply is ever opened). Keys are always present; their values are null when unset. */ UpdateWindow: { /** * Format: date-time * @description Window start (RFC3339, inclusive), or null when unset. */ start: string | null; /** * Format: date-time * @description Window end (RFC3339, exclusive), or null when unset. */ end: string | null; }; /** @description The newest control-plane database backup the host recorded (internal/api/handlers_dbbackup.go dbBackupView; the record itself is internal/dbbackup Status, written by `felis db backup`). */ DBBackupStatus: { /** @description Null until the first backup has been recorded. */ last: { /** * Format: date-time * @description When the bundle was written. */ at: string; /** @description Bundle file name, felis-db--