openapi: 3.1.1
info:
  title: HTTPS-API för Scacelith-servern
  version: "1.1.0"
  summary: HTTPS-API:et för varje Scacelith-server, både den officiella och communityservrar.
  description: |-
    Varje Scacelith-server, den officiella `caissa.scacelith.com` och varje communityserver, svarar på detta HTTPS-API. Spelet använder det till allt utanför själva partiet: registrering och inloggning (inklusive tvåfaktorsautentisering och Google), kontosidan, partihistoriken, PGN-nedladdningar och animerade GIF:ar av partier, inloggade enheter, nedladdningen av kontots data, raderingen av kontot och anmälningar.

    Spel i realtid (matchning, utmaningar, drag, klockor) går via samma servers WebSocket (`wss://<host>/ws`), som öppnas med en sessionstoken från detta API; det beskrivs i `PROTOCOL.md`, inte här.

    Standardvärdena nedan är de som gäller för en server med orörd konfiguration; en communityserver kan ändra dem (`CONFIG.md` namnger varje inställning).

    ## Bas-URL, port och transport

    - Officiell server: `https://caissa.scacelith.com/api/v1`, TCP-port 443.
    - En communityserver: `https://<SERVER_PUBLIC_HOST>[:<port>]/api/v1`. `API_PORT` är 443 som standard; bakom en NAT eller en proxy är den port som spelarna använder `PUBLIC_API_PORT`.
    - Spelets WebSocket ligger som standard på samma port (`WS_PORT` flyttar den; `GET /info` talar om för klienten var den finns).
    - HTTP/1.1 över TLS. Med `TLS_MODE=proxy` terminerar en omvänd proxy TLS framför servern. `TLS_MODE=off` (vanlig HTTP) är endast avsett för lokal utveckling.
    - HTML-sidorna som öppnas från länkarna i e-postmeddelanden (`/verify-email`, `/reset-password`, `/confirm-email-change`) ligger i serverns rot, utanför `/api/v1`. Endpoints för hälsokontroll svarar både i roten och under `/api/v1`. Google-inloggning har ingen sida på servern: Google skickar tillbaka webbläsaren till själva spelet, på `127.0.0.1`, eller till en sida på den webbplats som startade inloggningen (`GOOGLE_WEB_REDIRECT_URI`).
    - Ett avslutande snedstreck ignoreras (`/api/v1/info/` är `/api/v1/info`), och sökvägsparametrar URL-avkodas (en parameter som inte är giltig URL-kodning ger svaret 400 `invalid_request`).

    ## Anrop

    - **JSON-body.** `POST`-, `PUT`- och `DELETE`-anrop innehåller ett JSON-objekt med `Content-Type: application/json`. En annan `charset` än UTF-8 avvisas, liksom alla andra innehållstyper (415 `unsupported_media_type`). En tom body räknas som `{}`, vilket är vad endpoints utan parametrar tar emot (`POST /auth/logout`, `POST /auth/logout-all`, `DELETE /auth/sessions/{id}`).
    - **Storlek och tid.** En body är begränsad till `HTTP_BODY_LIMIT` byte (16 384 som standard; `POST /gif` har en egen gräns på 135 168 byte): 413 `payload_too_large`. Den måste komma fram inom 10 sekunder: 408 `request_timeout`. Efter vilket som helst av de två felen stänger servern anslutningen.
    - **Strikta scheman.** Ett fält som endpointen inte känner till avvisas, ett fält som inte är markerat som valfritt är obligatoriskt, och typer och längder kontrolleras. Vart och ett av dessa fel ger svaret 400 `invalid_request`, där `field` anger fältet (med punkter för ett nästlat fält, till exempel `pow.nonce`). Strängar får inte innehålla styrtecken. Längder räknas i UTF-16-kodenheter, så ett tecken utanför Basic Multilingual Plane (en emoji) räknas dubbelt. En body som inte är JSON ger svaret 400 `invalid_json`. `POST /gif` och `POST /reports` kontrollerar själva sin body (se respektive endpoint).
    - **Frågesträngar.** Den första förekomsten av en parameter gäller, okända parametrar ignoreras, och ett `+` avkodas till ett mellanslag: de endpoints som tar emot en betänketidskategori accepterar både `3%2B2` och `3+2`.
    - **Metoder.** `HEAD` är `GET` utan body. `OPTIONS` på en befintlig sökväg ger svaret 204 med en `Allow`-header (och headers för ett CORS-preflight-anrop, se nedan). Alla andra metoder som sökvägen inte har ger svaret 405 `method_not_allowed` med `Allow`.
    - **Anropsmålet.** Det får inte överstiga 4096 tecken (414 `uri_too_long`). Ett anrop som inte alls går att tolka, eller vars huvud är för stort eller kommer fram för långsamt, får ett tomt svar 400, 431 eller 408, och anslutningen stängs.
    - **CORS: en tillåtelselista, avstängd som standard.** API:et betjänar spelet, som inte skickar någon `Origin`. När `CORS_ORIGINS` är tom (standard) skickas aldrig någon `Access-Control-*`-header, så ingen webbsida kan läsa ett svar. `CORS_ORIGINS` listar de webbursprung vars sidor får anropa API:et från en webbläsare, vart och ett exakt så som webbläsare skickar det i `Origin`: `https://host` eller `https://host:port`, med gemener, utan sökväg, avslutande snedstreck eller standardport; `http://` endast för `localhost`, `127.0.0.1` och `[::1]` (lokal utveckling); inga jokertecken. Den officiella servern sätter `CORS_ORIGINS=https://scacelith.com,https://www.scacelith.com` för sin webbplats. Då gäller:
      - Varje svar för en sökväg under `/api/v1` utom endpoints för hälsokontroll, både lyckade svar och fel (inklusive 429 från lagret per adress), har `Vary: Origin`. När anropets `Origin` byte för byte är ett av de listade ursprungen har svaret dessutom `Access-Control-Allow-Origin: <that origin>` och, utom i svaret på ett preflight-anrop, `Access-Control-Expose-Headers: Retry-After, Content-Disposition`, så att sidan kan läsa felkoderna, `Retry-After` och filnamnen i PGN- och GIF-nedladdningarna.
      - Ett preflight-anrop (`OPTIONS` med `Access-Control-Request-Method`) från ett listat ursprung till en befintlig sökväg ger svaret 204, utan body, med `Allow`, `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods` (metoderna i `Allow`), `Access-Control-Allow-Headers: Authorization, Content-Type`, `Access-Control-Max-Age: 600` och `Vary: Origin`. Det kräver ingen token och räknas varken mot någon kontobudget eller mot någon gräns per endpoint; som alla anrop tar det en token från lagret per adress. En webbläsare återanvänder resultatet av ett preflight-anrop i 10 minuter, men bara för exakt samma URL, inklusive frågesträngen. Ett anrop utan `Authorization` och utan JSON-body, till exempel `GET /info` eller `GET /leaderboard`, behöver inget preflight-anrop.
      - Alla andra svar på ett preflight-anrop innebär ett misslyckat preflight, även med CORS-headers: en 404 för en sökväg som inte finns, eller 429 från lagret per adress. Webbläsaren skickar då inte anropet, och sidan ser ett nätverksfel, utan svaret och dess `Retry-After`.
      - `Access-Control-Allow-Credentials` skickas aldrig, inte heller `Access-Control-Allow-Origin: *`: en sida skickar sin sessionstoken i `Authorization: Bearer`, precis som spelet, och API:et sätter ingen cookie. HTML-sidorna, endpoints för hälsokontroll och de tomma svaren på oläsbara anrop har ingen CORS-header. WebSocket använder inte CORS: `WS_ALLOWED_ORIGINS` listar de sidor som får öppna den.
      - En sida från ett ursprung som inte är listat kan inte läsa något, och dess skrivande anrop mellan webbplatser skickas aldrig: JSON-body och `Authorization` kräver ett preflight-anrop, vars svar saknar `Access-Control-Allow-Origin` för det ursprunget. `Cross-Origin-Resource-Policy: same-origin` finns kvar på alla svar: den gäller bara `no-cors`-anrop (en annan webbplats som bäddar in ett svar), aldrig `cors`-anropen från en listad sida.

    ## Svar och fel

    - Svaren är JSON i UTF-8, utom PGN-nedladdningen (`application/x-chess-pgn`), de animerade GIF:arna (`image/gif`) och HTML-sidorna. Tider anges i millisekunder sedan 1970-01-01 UTC. Id:n är heltal. Parti-id:n har upp till 16 siffror men håller sig under 2^53, så ett JSON-tal (en double) rymmer dem exakt.
    - Varje svar från API:et har `Cache-Control: no-store`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`, `X-Frame-Options: DENY`, `Cross-Origin-Resource-Policy: same-origin` och `Content-Security-Policy: default-src 'none'; frame-ancestors 'none'` (HTML-sidorna har en egen policy). Med `TLS_MODE=native` har det även `Strict-Transport-Security: max-age=31536000`. De svar som lyssnaren skriver själv, före API:et (`GET` och `HEAD` för endpoints för hälsokontroll, 429 från lagret per adress och en 500 `internal_error` när API:et fallerar helt), har samma headers utom `Cross-Origin-Resource-Policy`, och deras `Content-Type` är `application/json` utan teckenkodning.
    - Ett svar måste läsas inom 60 sekunder från det att servern har det klart, vilket bara spelar roll för de stora svaren (en GIF, dataexporten, en lång PGN). Den tid servern behöver för att förbereda ett svar räknas varken in i detta eller i de 30 sekunder utan någon byte in eller ut efter vilka en anslutning stängs.

    Fel har en enda form (schemat `Error`):

    ```json
    { "error": "snake_case_code", "message": "An English sentence.", "retryAfter": 30 }
    ```

    `message` är avsett för loggar och som reserv; en klient väljer vad som ska visas utifrån `error`. `retryAfter` (sekunder) finns bara vid avslag som upphör med tiden, och svaret har då också en `Retry-After`-header med samma värde; det enda undantaget är 503 `busy` vid läsning av historik, partier, spelare och topplistan, som bara har fältet. Vissa fel lägger till fält: `field` (ogiltig indata), `reason` (`weak_password`, `pow_required`), `pow` (`pow_required`), `until` (`banned`), `line` och `column` (`invalid_pgn`).

    Fel som alla endpoints kan ge:

    | Status | `error` | När |
    |---|---|---|
    | 400 | `invalid_request` | Bodyn bryter mot endpointens schema (`field` anger var), anropsmålet eller `Content-Length` är felformaterat, en sökvägsparameter är inte giltig URL-kodning, eller bodyn kapades. |
    | 400 | `invalid_json` | Bodyn är inte JSON. |
    | 401 | `unauthorized` | Ingen `Authorization`-header på en endpoint som kräver en session. |
    | 401 | `invalid_token` | Tokenen är felformaterad, har gått ut, har återkallats eller tillhör ett raderat konto; även på endpoints där sessionen är valfri. |
    | 404 | `not_found` | Ingen sådan endpoint. Vissa endpoints använder den också: inget sådant parti, ingen sådan spelare eller session. |
    | 405 | `method_not_allowed` | Sökvägen finns för andra metoder (se `Allow`). |
    | 408 | `request_timeout` | Bodyn kom inte fram inom 10 sekunder. |
    | 413 | `payload_too_large` | Bodyn överskrider `HTTP_BODY_LIMIT` (135 168 byte för `POST /gif`). |
    | 414 | `uri_too_long` | Anropsmålet överskrider 4096 tecken. |
    | 415 | `unsupported_media_type` | Bodyn är inte `application/json`, eller dess teckenkodning är inte UTF-8. |
    | 429 | `rate_limited` | En anropsgräns (se nedan): `retryAfter` plus en `Retry-After`-header. |
    | 500 | `internal_error` | Ett oväntat fel. Servern loggar det. |
    | 503 | `timeout` | Servern svarade inte inom 30 sekunder (60 sekunder för exporten, 45 sekunder för GIF:arna med standardinställningarna). |
    | 503 | `server_busy` | Ett sessionsuppslag eller en kontoändring fann databasen låst (`retryAfter` 1), eller så är kön för lösenordshashning full (se nedan). Även, med en `retryAfter` på 2 till 5, när servern ligger långt efter: för en `POST`, `PUT`, `PATCH` eller `DELETE` har databasen för många skrivningar i kö; för alla anrop gäller att för många tidigare anrop fortfarande körs efter sin tidsgräns, eller att sessionstokenen inte kunde kontrolleras inom 5 sekunder. Anropet kördes inte, och dess anropsgränser tog ingenting. |

    Endpoints för läsning (partihistorik, partier, spelare, topplista) och exporten svarar 503 `busy` med `retryAfter: 1` när databasen förblev låst.

    Endpoints som kontrollerar eller hashar ett lösenord kan också svara med något av dessa, båda med en slumpmässig `retryAfter` på 5 till 15 sekunder:

    - 503 `server_busy`: serverns kö för lösenordshashning (`PASSWORD_HASH_QUEUE_MAX`) är full, eller väntetiden tog slut.
    - 429 `rate_limited`: kön är minst halvfull och den här klienten (en IPv4-adress eller ett IPv6-/48-nät) har redan `PASSWORD_HASH_WAITERS_PER_SOURCE` hashningar som väntar. Denna 429 lämnar tillbaka de token från anropsgränserna som anropet tog.

    Ingenting ändrades och inget misslyckat försök räknades (utom på `POST /auth/sso/google/link`, där försöket på biljetten och misslyckandet för kontot räknades före hashningen), och en återställningslänk förblir giltig.

    HTML-sidorna svarar med sina fel som HTML-sidor med samma statuskoder.

    ## Autentisering

    De endpoints som kräver en session tar emot en bearer-token i headern `Authorization` (schemat `bearerAuth`):

    ```
    Authorization: Bearer sct_L_8GDd7uzfQ3QQWtqrsWXDTsFWzRwIvJcwIGHhjWPS8
    ```

    - **Hämta en token.** En token (`sct_` följt av 43 base64url-tecken) kommer från `POST /auth/login`, och sedan `POST /auth/login/mfa` när tvåfaktorsautentisering är på, från `POST /auth/sso/google/finish` och `POST /auth/sso/google/link` (Google-inloggning) och från `POST /auth/sso/complete`. Var och en av dem svarar `{ token, expiresAt, user }`. Servern lagrar bara en SHA-256 av tokenen.
    - **Session krävs.** En saknad header ger svaret 401 `unauthorized` med `WWW-Authenticate: Bearer realm="scacelith"`. En ogiltig token ger svaret 401 `invalid_token` med `WWW-Authenticate: Bearer realm="scacelith", error="invalid_token"`. En klient bör glömma en token som får `invalid_token` och logga in igen.
    - **Valfri session.** På `GET /games/{id}`, `GET /games/{id}/pgn`, `GET /players/{username}` och `GET /players/{username}/games` är sessionen valfri. Utan headern svarar de med den offentliga vyn; om en header skickas måste dess token vara giltig.
    - **Upptagen lagring.** När databasen förblir låst medan en token kontrolleras svarar en endpoint som kräver eller accepterar en session 503 `server_busy` med `retryAfter: 1` (och `Retry-After`); när kontrollen tar mer än 5 sekunder svarar den 503 `server_busy` med en `retryAfter` på 2 till 5. I båda fallen har tokenen inte bedömts: behåll den och skicka anropet igen.
    - **Livslängd.** En session upphör vid det som inträffar först av `SESSION_MAX_DAYS` (90) efter inloggningen (detta är `expiresAt`) och `SESSION_IDLE_DAYS` (30) utan användning. Varje användning skjuter upp inaktivitetsgränsen; servern skriver det nya värdet högst var 5:e minut, i bakgrunden. Medan databasen har många skrivningar i kö skjuter servern upp den skrivningen till ett senare anrop, utom under den sista timmen före inaktivitetsgränsen, så att en session som används inte går ut. Ett konto behåller högst `MAX_SESSIONS_PER_USER` (10) sessioner: en ny inloggning återkallar de äldsta utöver det antalet.
    - **Återkallelse.** Utloggning (`POST /auth/logout`, `POST /auth/logout-all`, `DELETE /auth/sessions/{id}`) återkallar sessioner; det gör även ett lösenordsbyte (de andra sessionerna), en återställning av lösenordet och radering av kontot (alla sessioner), samt en administratör. En återkallelse via API:et träder i kraft direkt, och den WebSocket som öppnades med en återkallad session stängs. Ett lösenordsbyte eller en återställning av lösenordet och radering av kontot återkallar i samma transaktion som själva ändringen: när någon av dem misslyckas har ingen session återkallats och inget annat heller ändrats. Servern cachar sessionsuppslag i 30 sekunder, så en återkallelse via administratörskommandot, som är en separat process, träder i kraft inom 30 sekunder.
    - **Omfattning.** En token hör till en enda server och öppnar även dess WebSocket. Skicka den aldrig till en annan server.

    ## Anropsgränser och annan strypning

    Gränserna gäller ett av två omfång. **Klient:** en IPv4-adress eller ett IPv6-/64-nät (i proxyläge kommer adressen från `X-Forwarded-For` som skickats av en `TRUSTED_PROXIES`-adress); vissa gränser räknar också varje IPv6-/48-nät som helhet, utöver vart och ett av dess /64-nät. **Spelare:** det inloggade kontot, oavsett adress; en gräns som räknas per spelare på en endpoint där sessionen är valfri räknas per klient för ett anrop utan token.

    Varje gräns är en token bucket som rymmer `limit` anrop och fylls på kontinuerligt med `limit / window`; `retryAfter` är tiden till nästa token. Gränser markerade *delad* räknas dessutom över ett glidande fönster av samma längd, så att inget fönster rymmer mycket mer än `limit` anrop. Varje gräns räknas för hela servern. När en av en endpoints gränser avvisar ett anrop lämnas de token som dess övriga gränser tog för anropet tillbaka. Ett avslag ger svaret 429 `rate_limited` med `retryAfter` och `Retry-After`.

    - **Lagret per adress.** Varje anrop (alla sökvägar och metoder, inklusive endpoints för hälsokontroll och WebSocket-uppgraderingar) tar först en token ur klientens budget: `HTTP_RATE_PER_IP` (600) per minut med en burst på en halv minut, och `HTTP_RATE_PER_PREFIX` (4 x `HTTP_RATE_PER_IP`) för ett IPv6-/48-nät. En klient får också ha högst `IP_MAX_INFLIGHT` (32 x `WORKERS`) pågående anrop (`retryAfter` 1 därutöver). En klient som fortsätter trots avslagen blockeras: `ABUSE_BLOCK_REFUSALS_PER_MIN` (600) avslag inom en minut blockerar den i 1 minut, sedan 4, 16 och 60 minuter vid varje ny blockering inom 6 timmar. Ett avslag från gränserna `auth`, `auth_*` och `reauth` räknas som 5; gränser som räknas per spelare räknas aldrig. Adresser i `ABUSE_EXEMPT` hoppar över detta lager, men inte kontobudgeten eller gränserna per endpoint.
    - **Kontobudget.** Varje anrop som har en giltig sessionstoken räknas också mot sitt konto: `USER_RATE_PER_MIN` (120) per minut för alla endpoints tillsammans, oavsett adress, med en burst på en halv minut.
    - **Gränser per endpoint:**

    | Gräns | Standard | Räknas per | Endpoints |
    |---|---|---|---|
    | `auth` | `AUTH_RATE_PER_IP` (20) / 10 min, delad | klient, och `AUTH_RATE_PER_PREFIX` (5 x `AUTH_RATE_PER_IP`) per IPv6-/48-nät | `POST /auth/register`, `/auth/login`, `/auth/login/mfa`, `/auth/verify-email/resend`, `/auth/password/forgot`, `/auth/password/reset`, `/auth/sso/google/link`, `/auth/sso/complete`; `POST /verify-email`, `/reset-password`, `/confirm-email-change` |
    | `auth_register` | `AUTH_REGISTER_PER_HOUR` (10) / timme, delad | klient, 3 gånger så många per IPv6-/48-nät | `POST /auth/register` |
    | `auth_mail` | `AUTH_MAIL_PER_HOUR` (10) / timme, delad | klient, 3 gånger så många per IPv6-/48-nät | `POST /auth/verify-email/resend` |
    | `auth_forgot` | `AUTH_FORGOT_PER_HOUR` (3) / timme, delad | klient, 3 gånger så många per IPv6-/48-nät | `POST /auth/password/forgot` |
    | `auth_forgot_day` | `AUTH_FORGOT_PER_DAY` (10) / 24 timmar, delad | klient, 3 gånger så många per IPv6-/48-nät | `POST /auth/password/forgot` |
    | `auth_reset` | `AUTH_RESET_PER_HOUR` (10) / timme, delad | klient, 3 gånger så många per IPv6-/48-nät | `POST /auth/password/reset`, `POST /reset-password` |
    | `reauth` | samma tal som `auth`, en egen bucket, delad | klient och IPv6-/48-nät | `POST /account/password`, `/account/mfa/totp/setup`, `/account/mfa/totp/enable`, `/account/mfa/totp/disable`, `/account/mfa/recovery-codes`, `/account/email`, `/account/export`, `/account/delete` |
    | `reauth_user` | `AUTH_REAUTH_PER_USER` (10) / 10 min, delad | spelare | samma endpoints som `reauth` |
    | `account` | 60 / min | spelare | `GET /account/me`, `PUT /account/preferences` |
    | `account_games` | 60 / min | spelare | `GET /account/games` |
    | `account_export` | 5 / timme, delad | spelare | `POST /account/export` (kontrolleras före `reauth`; varje försök räknas) |
    | `sessions` | 60 / min | spelare | `POST /auth/logout`, `/auth/logout-all`, `GET /auth/sessions`, `DELETE /auth/sessions/{id}` |
    | `public_read` | 60 / min | spelare (klient utan token) | `GET /players/{username}`, `/players/{username}/games`, `/games/{id}`, `/games/{id}/pgn` (en bucket för alla fyra) |
    | `gif` | 30 / min | spelare | `GET /games/{id}/gif`, `POST /gif` (en bucket för båda) |
    | `gif_user_min`, `gif_user_hour` | `GIF_USER_RENDERS_PER_MIN` (4) / min och `GIF_USER_RENDERS_PER_HOUR` (30) / timme, delad | spelare | samma två, bara när GIF:en måste skapas (inte från cachen) |
    | `gif_ip_min`, `gif_ip_hour` | `GIF_IP_RENDERS_PER_MIN` (12) / min och `GIF_IP_RENDERS_PER_HOUR` (120) / timme, delad | klient (alla dess konton tillsammans), 3 gånger så många per IPv6-/48-nät | samma, som ovan |
    | `reports` | 30 / timme | spelare | `POST /reports` |
    | `sso_start` | 30 / 10 min, delad | klient, och 90 per IPv6-/48-nät | `POST /auth/sso/google/start`, `POST /auth/sso/google/web/start` (tillsammans) |
    | `sso_finish` | 30 / min | klient | `POST /auth/sso/google/finish` |
    | `page` | 60 / min | klient | `GET /verify-email`, `/reset-password`, `/confirm-email-change` |

    `GET /info` och `GET /leaderboard` har ingen egen gräns: bara lagret per adress. En endpoint med flera gränser kontrollerar dem i den ordning som anges i dess beskrivning.

    Servern hanterar ett anrop i denna ordning: lagret per adress, sedan endpoints för hälsokontroll; matchning av endpoint; kontrollerna av att servern inte ligger långt efter (503 `server_busy`): för många tidigare anrop som fortfarande körs efter sin tidsgräns och, för en `POST`, `PUT`, `PATCH` eller `DELETE`, databasens kö av skrivningar; autentisering, inom 5 sekunder (503 `server_busy` därutöver); kontobudgeten, när anropet har en session; endpointens gränser; bodyn; själva endpointen (GIF-endpoints tar sina renderingsgränser bara när de har en GIF att skapa). Ett anrop som avvisas på grund av sin token förbrukar alltså inga av endpointens token, medan ett anrop med ogiltig body förbrukar dem.

    Annan strypning, som endpoints själva svarar med:

    - **Misslyckade inloggningar på ett inloggningsnamn** (användarnamn eller e-postadress): från och med `AUTH_FAILURES_PER_ACCOUNT` (5) misslyckanden måste varje försök vänta dubbelt så länge som det föregående (2 s, 4 s och så vidare, upp till 15 minuter): 429 `too_many_attempts` med `retryAfter`. Räknaren glömmer dem efter en timme utan misslyckanden.
    - **Misslyckade andra faktorer vid inloggning:** samma regel från och med kontots 5:e felaktiga kod. Ett inloggningssteg accepterar högst 5 felaktiga koder.
    - **Koder för andra faktorn på ett konto:** högst `AUTH_MFA_PER_ACCOUNT` (10) koder (från autentiseringsappen eller återställningskoder, rätt eller fel) per 15 minuter, från vilken adress som helst, vid inloggning och vid förnyad autentisering; därutöver 429 `too_many_attempts` innan koden kontrolleras, så att en återställningskod inte förbrukas.
    - **Misslyckade förnyade autentiseringar för ett konto** (fel lösenord eller kod): samma regel (429 `too_many_attempts`), gemensam för alla endpoints som kräver förnyad autentisering.
    - **E-post:** ett bekräftelse- eller återställningsmeddelande per adress var 5:e minut (svaret förblir detsamma); ett meddelande om att ”någon försökte använda din adress” per adress och timme.
    - **Anmälningar:** `REPORTS_PER_DAY` (5) per spelare under 24 timmar: 429 `report_limit`.

    ## Proof of work

    `POST /auth/register` kräver alltid ett proof of work när `POW_REGISTER_BITS` är större än 0 (18 som standard; `GET /info` anger det som `pow.register`). `POST /auth/login` och `POST /auth/sso/google/link` (samma sorts challenge för båda) kräver ett bara under 5 minuter efter att servern har sett en våg av misslyckade inloggningar (`POW_LOGIN_TRIGGER_PER_MIN`, sedan `POW_LOGIN_BITS`); en klient får veta det av svaret.

    1. Anropet utan bevis (eller med ett avvisat bevis) ger svaret 428 `pow_required` med `reason` och en `pow`-challenge `{ challenge, bits, expiresAt }`.
    2. Hitta en nonce: en decimal sträng på högst 20 siffror sådan att `SHA-256(challenge + ":" + nonce)` börjar med `bits` nollbitar (den mest signifikanta biten i den första byten först). 18 bitar kräver i genomsnitt cirka 260 000 hashningar.
    3. Skicka samma anrop igen med `"pow": { "challenge": "...", "nonce": "123456" }` i bodyn.

    En challenge är giltig i 2 minuter och bara en gång, för en endpoint och ett klientnätverk (en IPv4-adress eller ett IPv6-/64-nät). Den är signerad, så servern sparar ingenting om den förrän den kommer tillbaka. `reason` anger varför ett bevis avvisades: `required`, `malformed`, `signature`, `endpoint`, `network`, `expired`, `bits`, `work` eller `replayed`.

    ## Förnyad autentisering

    Kontoändringar kräver lösenordet igen och, när tvåfaktorsautentisering är på, en andra faktor:

    - bara lösenordet: `POST /account/password` och `POST /account/mfa/totp/setup`;
    - lösenordet och en kod från autentiseringsappen (en återställningskod avvisas): `POST /account/mfa/recovery-codes`;
    - lösenordet och en kod från autentiseringsappen eller en återställningskod: `POST /account/mfa/totp/disable`, `POST /account/email`, `POST /account/export` och `POST /account/delete`.

    I bodyn innehåller `code` en 6-siffrig kod från autentiseringsappen och `recoveryCode` en återställningskod (`xxxx-xxxx-xx`; versaler eller gemener, mellanslag och bindestreck spelar ingen roll). En återställningskod kan också skickas i `code` där återställningskoder accepteras. Varje kod fungerar en gång: en använd kod från autentiseringsappen avvisas fram till nästa tidssteg på 30 sekunder, och en återställningskod är förbrukad när den har använts.

    | Status | `error` | När |
    |---|---|---|
    | 403 | `invalid_password` | Fel lösenord, eller så återställdes eller byttes lösenordet medan anropet kördes (ingenting ändrades). |
    | 403 | `mfa_code_required` | Tvåfaktorsautentisering är på och varken `code` eller `recoveryCode` skickades. |
    | 403 | `invalid_code` | Fel kod eller redan använd kod. |
    | 400 | `password_not_set` | Ett konto med enbart Google-inloggning har inget lösenord än (”Glömt lösenordet?” sätter ett). |
    | 429 | `too_many_attempts` | För många misslyckanden, eller för många prövade koder, på det här kontot. |
    | 503 / 429 | `server_busy` / `rate_limited` | Kön för lösenordshashning är upptagen. |

    Misslyckade lösenord och koder räknas i kontots räknare för misslyckanden och registreras som säkerhetshändelser.

    ## Port för mätvärden

    Utöver API-porten svarar servern med vanlig HTTP på porten för mätvärden (`METRICS_PORT`, 9464, bunden till `METRICS_BIND`, 127.0.0.1 som standard; håll den privat). Den ingår inte i detta API: `GET /healthz` svarar `ok`; `GET /readyz` svarar `ready` när uppstarten är klar (varje shard har spelat upp sin journal igen och lyssnarna är bundna) och fram till att nedstängningen börjar, annars 503 `not ready`; `GET /metrics` levererar Prometheus-mätvärdena och kräver, när `METRICS_TOKEN` är satt, `Authorization: Bearer <token>` med exakt den token.
  contact:
    name: Scacelith
    url: https://github.com/DarkCenobyte/scacelith-chess-server
  license:
    name: GPL-3.0-or-later
    identifier: GPL-3.0-or-later
externalDocs:
  description: Källkoden till Scacelith-servern och dess referensdokumentation (API.md, PROTOCOL.md, CONFIG.md).
  url: https://github.com/DarkCenobyte/scacelith-chess-server
servers:
  - url: https://caissa.scacelith.com/api/v1
    description: Den officiella servern.
  - url: https://{host}:{port}/api/v1
    description: Valfri Scacelith-server, till exempel en communityserver.
    variables:
      host:
        default: caissa.scacelith.com
        description: Serverns offentliga värdnamn (`SERVER_PUBLIC_HOST`).
      port:
        default: "443"
        description: Den offentliga API-porten (`PUBLIC_API_PORT`, annars `API_PORT`).
tags:
  - name: server-info
    x-displayName: Serverinformation
    description: Det en klient behöver veta innan den loggar in eller ansluter.
  - name: health
    x-displayName: Hälsokontroll
    description: |-
      Om servern lever och är redo (liveness och readiness), för övervakning. De svarar på API-porten före autentiseringen och alla gränser per endpoint, men som alla anrop tar de en token från lagret per adress; en övervakningsvärd kan listas i `ABUSE_EXEMPT`. Båda sökvägarna fungerar för varje endpoint: i serverns rot och under `/api/v1`.

      Porten för mätvärden har egna endpoints för hälsokontroll, som beskrivs i inledningen.
  - name: auth
    x-displayName: Registrering och inloggning
    description: |-
      Skapa ett konto, logga in med lösenord (och en andra faktor), e-postmeddelanden för bekräftelse och för återställning av lösenordet.

      `POST /auth/login`, `POST /auth/login/mfa`, `POST /auth/sso/google/finish`, `POST /auth/sso/google/link` och `POST /auth/sso/complete` svarar med en session `{ token, expiresAt, user }` (eller, för de första av dem, ett andra steg).
  - name: google-sign-in
    x-displayName: Google-inloggning
    description: |-
      Erbjuds när `GET /info` anger `sso.google: true`; annars svarar alla endpoints nedan 404 `sso_disabled`. En webbplats loggar in på samma sätt med en egen klient när `sso.googleWeb` är true (nedan, **På en webbplats**). Spelet loggar in via systemets webbläsare med flödet för installerade appar i RFC 8252 (en klient av typen ”Desktop app”): Google skickar tillbaka webbläsaren till en lyssnare i spelet på `127.0.0.1`, aldrig till den här servern, och spelet ser aldrig några inloggningsuppgifter för Google.

      1. Klienten lyssnar på `127.0.0.1:0` (systemet väljer porten) och skapar ett PKCE-par: en `codeVerifier` på 43 till 128 tecken `[A-Za-z0-9._~-]` och `codeChallenge = BASE64URL(SHA-256(codeVerifier))`, som har 43 tecken och ingen utfyllnad.
      2. `POST /auth/sso/google/start` med challengen och porten returnerar Google-URL:en och försökets `state`. Klienten kontrollerar URL:en (se nedan) och öppnar den i webbläsaren.
      3. Google skickar webbläsaren till `http://127.0.0.1:<port>/oauth2/google/<tag>?code=...&state=...`. Klienten accepterar bara den `state` som fanns i startsvaret, och skickar ingenting efter en omdirigering med `error=`.
      4. `POST /auth/sso/google/finish` med försökets id, `codeVerifier`, `state` och `code` (och `iss` när Google skickade det).
      5. Svaret är en session, ett steg för tvåfaktorsautentisering (fortsätt med `POST /auth/login/mfa`), `needsUsername` för en ny spelare (fortsätt med `POST /auth/sso/complete`) eller `needsPassword` när ett konto med lösenord använder adressen (fortsätt med `POST /auth/sso/google/link`).

      **Ursprungstaggen.** Omdirigerings-URI:n innehåller en tagg för den server som spelaren lade till, så att spelet avvisar en URL som en annan server har fått för sina egna spelare. Ursprunget är värden med gemener (en IPv6-literal inom hakparenteser), `:` och API-porten i decimal form, alltid utskriven, även 443: servern använder `SERVER_PUBLIC_HOST` och sin offentliga API-port, spelet den adress som det är anslutet till. Taggen är de första 22 tecknen av base64url (utan utfyllnad) av SHA-256(UTF-8 `"scacelith-sso-origin-v1\n"` + ursprung). Omdirigerings-URI:n är `"http://127.0.0.1:" + port + "/oauth2/google/" + tag`: alltid IPv4-literalen och porten för spelets lyssnare (1024 till 65535). Servern tar aldrig emot en URI, en värd eller en sökväg från klienten.

      | Ursprung | Tagg |
      |---|---|
      | `play.scacelith.example:443` | `IhcScoV7eDOzTEcSnqPUPt` |
      | `localhost:8443` | `TFGx7zQ_8QlGZW5zpqznCr` |
      | `[::1]:8443` | `XToJm0DG5PjciEVmZa9Cho` |
      | `127.0.0.1:50443` | `3r653wM5ZjYsHcAJljmCwY` |

      Google-inloggning fungerar alltså bara för spelare som har lagt till servern med exakt `SERVER_PUBLIC_HOST` och dess offentliga API-port.

      **Vad spelet kontrollerar innan det öppnar webbläsaren.** `authUrl` börjar med exakt `https://accounts.google.com/o/oauth2/v2/auth?` och är utskrivbar ASCII på under 4096 tecken, och dess frågesträng innehåller exakt en `response_type=code`, exakt en `redirect_uri` som är lika med den URI som spelet beräknar utifrån sin port och sin ursprungstagg, exakt en `state` som är lika med svarets `state`, `code_challenge_method=S256` och en `code_challenge` på 43 tecken. Annars stoppar spelet sin lyssnare och öppnar ingenting. Det skickar `finish` och `link` bara till den server som svarade på `start`.

      **På en webbplats.** En webbplats (den officiella: <https://scacelith.com/>) loggar in med en klient av typen ”Web application” i samma Google-projekt (`GOOGLE_WEB_CLIENT_ID`). Google skickar tillbaka webbläsaren till en sida på webbplatsen som operatören har konfigurerat (`GOOGLE_WEB_REDIRECT_URI`, listad under klientens ”Authorized redirect URIs”), och den sidan skickar koden till `finish`; servern tar inte heller här emot någon omdirigerings-URI från klienten.

      1. Sidan skapar ett PKCE-par som ovan, behåller verifiern i fliken (`sessionStorage`) och skickar `POST /auth/sso/google/web/start` med challengen.
      2. Den kontrollerar `authUrl` som spelet gör, förutom att dess `redirect_uri` måste vara en sida i sidans eget ursprung, och navigerar sedan dit. Inget Google-skript läses in.
      3. Google skickar webbläsaren till `GOOGLE_WEB_REDIRECT_URI?code=...&state=...`. Sidan accepterar bara den `state` som fanns i startsvaret och skickar ingenting efter en omdirigering med `error=`.
      4. `finish`, `link` och `complete` följer som för spelet. Försöket kommer ihåg att en webbplats startade det: koden löses in med webbplatsens klient och sida, och ID-tokenen måste ange den klienten (en kod eller ID-token från spelets klient misslyckas med 502 `sso_failed`, och omvänt).

      Ett Google-konto når samma Scacelith-konto från spelet och från webbplatsen: kopplingen innehåller Googles subject (`sub`), som identifierar Google-kontot oavsett klient. Webbplatsens ursprung måste finnas i `CORS_ORIGINS` (annars varnar `check-config`).

      **Vilket konto Google-inloggningen når:** det konto som redan är kopplat till det Google-kontot; annars ett aktivt konto med den adress som Google har bekräftat (med lösenord svarar `finish` med `needsPassword` och kopplingen sparas först när kontots lösenord, och sedan dess andra faktor om den är på, har godkänts; utan lösenord 409 `sso_account_exists`); annars ett nytt konto (`needsUsername`). Ett Google-konto kopplas aldrig till ett befintligt konto enbart på grund av adressen. Kontots adress får ett e-postmeddelande när Google-inloggning skapar ett konto och när den läggs till på ett befintligt konto.
  - name: sessions
    x-displayName: Sessioner
    description: Kontots inloggade enheter, och utloggning.
  - name: account
    x-displayName: Konto
    description: Kontovyn, dess inställningar och dess lösenord.
  - name: two-step-verification
    x-displayName: Tvåfaktorsautentisering
    description: Autentiseringsappar (TOTP, RFC 6238), med SHA-1, 6 siffror, tidssteg på 30 sekunder och ett stegs tolerans åt vardera hållet, samt återställningskoder för engångsbruk.
  - name: email-change
    x-displayName: Byte av e-postadress
    description: Byte av kontots adress, bekräftat med en länk som skickas till den nya adressen.
  - name: data-export
    x-displayName: Dataexport
    description: Allt som servern sparar om kontot, i en enda JSON-fil.
  - name: account-deletion
    x-displayName: Radering av konto
    description: Permanent radering av kontot.
  - name: game-history
    x-displayName: Partihistorik
    description: Den inloggade spelarens egna partier, filtrerade och sidindelade.
  - name: games
    x-displayName: Partier och PGN
    description: |-
      Partiposter med sina drag och klockor, och deras PGN-filer.

      **Partikoder.** `status`: 1 `WhiteWins`, 2 `BlackWins`, 3 `Draw`, 4 `Aborted` (bara avslutade partier sparas). `result`: `1-0`, `0-1`, `1/2-1/2` eller `*` (avbrutet). `reason`, med sitt namn (`termination`) och de ord som avslutar PGN-filens dragtext (på engelska i filen, här följda av en svensk översättning); koderna 7, 14 och 21 betyder remi (den spelare vars tid tog slut, som gav upp eller som lämnade partiet mötte en motståndare som inte kunde sätta matt: FIDE 6.9 och 5.1.2; `DESIGN.md` 6.3 anger regeln), och servern avslutar aldrig ett parti med koderna 4 och 13 (de hör till den gemensamma listan över orsaker):

      | Kod | `termination` | Ord i PGN-filen |
      |---|---|---|
      | 1 | `Checkmate` | Checkmate (Schackmatt) |
      | 2 | `Resignation` | Resignation (Uppgivet parti) |
      | 3 | `Timeout` | Loss on time (Förlust på tid) |
      | 4 | `IllegalMoves` | Second illegal move (forfeit) (Andra otillåtna draget, förlust) |
      | 5 | `Stalemate` | Stalemate (Patt) |
      | 6 | `InsufficientMaterial` | Dead position (insufficient material) (Död ställning, otillräckligt material) |
      | 7 | `TimeoutVsInsufficient` | Flag fall, but the opponent cannot checkmate (Flaggan föll, men motståndaren kan inte sätta matt) |
      | 8 | `FivefoldRepetition` | Fivefold repetition (Femfaldig upprepning) |
      | 9 | `SeventyFiveMoves` | 75-move rule (75-dragsregeln) |
      | 10 | `ThreefoldClaim` | Threefold repetition (claimed) (Trefaldig upprepning, på begäran) |
      | 11 | `FiftyMoveClaim` | 50-move rule (claimed) (50-dragsregeln, på begäran) |
      | 12 | `Agreement` | Draw by agreement (Remi efter överenskommelse) |
      | 13 | `IllegalMovesVsInsufficient` | Second illegal move, but the opponent cannot checkmate (Andra otillåtna draget, men motståndaren kan inte sätta matt) |
      | 14 | `ResignationVsInsufficient` | Resignation, but the opponent cannot checkmate (Uppgivet parti, men motståndaren kan inte sätta matt) |
      | 20 | `Abandonment` | Abandoned (disconnected for too long) (Lämnade partiet, frånkopplad för länge) |
      | 21 | `AbandonmentVsInsufficient` | Abandoned, but the opponent cannot checkmate (Lämnade partiet, men motståndaren kan inte sätta matt) |
      | 22 | `Aborted` | Game aborted (Partiet avbrutet) |
      | 23 | `NoShow` | Aborted: first move not played in time (Avbrutet: första draget spelades inte i tid) |
      | 24 | `Forfeit` | Forfeit (fair play violation) (Tilldömd förlust, brott mot rent spel) |
      | 25 | `ServerAborted` | Aborted by the server (Avbrutet av servern) |
      | 26 | `BothDisconnected` | Aborted: both players disconnected (Avbrutet: båda spelarna tappade anslutningen) |

      Kod 14 är ny i protokollets minor-version 1: en klient som skrevs före den bör visa en okänd kod som ett vanligt partislut med dess `result`.
  - name: gifs
    x-displayName: Animerade GIF:ar
    description: |-
      Ett parti som animerad GIF, att spara eller dela: brädet sett ovanifrån, en bildruta per ställning från början till slutställningen, spelarnas namn och ratingtal ovanför brädet, det senaste draget under det, och i den sista bildrutan resultatet och hur partiet slutade.

      **Kostnad, cache och kvoter.** En GIF skapas i en egen renderingstråd, aldrig i de trådar som kör partierna, och med lägsta CPU-prioritet: `GIF_THREADS` trådar (`WORKERS` som standard), som startas med den första GIF:en och stoppas efter en minut utan någon. Upp till `GIF_QUEUE_MAX` (4 x `WORKERS`) GIF:ar väntar på en tråd, högst `GIF_QUEUE_TIMEOUT_MS` (10 s) var; en rendering får pågå i `GIF_RENDER_TIMEOUT_MS` (30 s). Servern sparar de GIF:ar den har skapat i en cache på `GIF_CACHE_MB` MB (32 x `WORKERS`), där den som använts minst nyligen försvinner först; en GIF från cachen, eller en som håller på att skapas för ett annat anrop, kostar ingen rendering. Cachens nyckel omfattar allt som ändrar bilden, även namnen.

      Varje anrop räknas i `gif` (30 per minut per spelare för båda endpoints). En GIF som måste skapas räknas också i renderingsgränserna: per spelare `GIF_USER_RENDERS_PER_MIN` (4) per minut och `GIF_USER_RENDERS_PER_HOUR` (30) per timme; per klient, alla dess konton tillsammans, `GIF_IP_RENDERS_PER_MIN` (12) per minut och `GIF_IP_RENDERS_PER_HOUR` (120) per timme, och 3 gånger så många per IPv6-/48-nät. En klient bör behålla filen den har laddat ned i stället för att begära den igen, och vänta `retryAfter` efter en 429 eller en 503.

      Storlekar: `small` (rutor på 32 px: 284 x 350 pixlar), `medium` (48 px: 424 x 515), `large` (72 px: 628 x 762); utan koordinater 268 x 342, 400 x 503 och 600 x 748. Utgångsställningen visas i minst 1 s (eller fördröjningen, om den är längre), slutställningen i 3 s, och GIF:en spelas i en slinga; efter den första bildrutan lagras bara den del av bilden som ändras. Ett parti på 40 drag tar cirka 135, 205 och 325 KiB (small, medium, large), ett parti på 150 drag 0,5, 0,8 och 1,2 MiB.
  - name: players
    x-displayName: Spelare
    description: Offentliga profiler och senaste partier. Bara offentliga data, aldrig en e-postadress, en session, en sanktion eller en integritetsnivå. Ett raderat konto har ingen profil.
  - name: leaderboard
    x-displayName: Topplista
    description: De bästa spelarna i varje officiell kategori.
  - name: reports
    x-displayName: Anmälningar
    description: Anmälan av motståndaren i ett nyligen spelat parti till moderatorerna.
  - name: pages
    x-displayName: HTML-sidor
    description: |-
      Sidor för en webbläsare, som öppnas från länkarna i e-postmeddelanden. Deras länkar pekar på `https://<SERVER_PUBLIC_HOST>` (med `:<PUBLIC_API_PORT>` när den inte är 443), utanför `/api/v1`.

      - Sidorna kör inget JavaScript och läser inte in några externa resurser. De levereras med `Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'`.
      - En `GET` visar bara en knapp eller ett formulär, så att en e-postskanner som öppnar länken inte förbrukar den. Ändringen sker vid `POST`: ett formulär som skickas som `application/x-www-form-urlencoded` (en JSON-body accepteras också), med länkens token i ett dolt fält. Ett fält som anges två gånger avvisas.
      - Fel (anropsgränser, ogiltiga fält) är också HTML-sidor, med rubriken ”Request refused” (anropet avvisades) eller, från 500 och uppåt, ”Server error” (serverfel). Bara de fel som upptäcks innan sidan har matchats (ett anropsmål som är för långt eller felformaterat, lagret per adress) får svar i JSON.
x-tagGroups:
  - name: server
    x-displayName: Server
    tags:
      - server-info
      - health
  - name: accounts
    x-displayName: Konton
    tags:
      - auth
      - google-sign-in
      - sessions
      - account
      - two-step-verification
      - email-change
      - data-export
      - account-deletion
  - name: games
    x-displayName: Partier
    tags:
      - game-history
      - games
      - gifs
  - name: community
    x-displayName: Spelare och anmälningar
    tags:
      - players
      - leaderboard
      - reports
  - name: browser-pages
    x-displayName: Webbläsarsidor
    tags:
      - pages
paths:
  /info:
    get:
      operationId: getServerInfo
      tags:
        - server-info
      summary: Hämta serverns namn, versioner, portar och registreringsregler
      description: |-
        Det en klient behöver innan den loggar in eller ansluter: serverns namn och id, WebSocket-protokollets versioner och var WebSocket finns, registreringsreglerna, de officiella betänketiderna och de gränser som en klient kan kontrollera innan den skickar ett formulär.

        **Gränser:** bara lagret per adress.
      security: []
      responses:
        '200':
          description: Serverns beskrivning.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ServerInfo'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: lagret per adress.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /auth/register:
    post:
      operationId: registerAccount
      tags:
        - auth
      summary: Skapa ett konto
      description: |-
        Skapar ett konto: när dess e-postadress har bekräftats med länken som skickats dit, eller direkt utan e-postbekräftelse.

        **Med e-postbekräftelse** (`REQUIRE_EMAIL_VERIFICATION`, standard): 202 `verification_sent`. Inget konto finns ännu: registreringen väntar i 24 timmar och reserverar under tiden sitt användarnamn. En länk som är giltig i dessa 24 timmar skickas till adressen, högst en per adress var 5:e minut (`POST /auth/verify-email/resend` skickar en ny); kontot skapas, med adressen bekräftad, när länken används (knappen på sidan `/verify-email`), och spelaren kan sedan logga in. Fram till dess ger en inloggning med det användarnamnet svaret `invalid_credentials`, som för ett okänt konto, och den offentliga profilen finns inte. En ny registrering med samma adress ersätter den väntande. Svaret är detsamma när ett annat konto redan använder adressen: då skickas ingen länk, utan kontots ägare får i stället ett meddelande (högst ett per timme), och användarnamnet reserveras på samma sätt, så att ingenting avslöjar om adressen har ett konto. En registrering vars länk inte användes tas bort efter 24 timmar, och dess användarnamn blir ledigt igen.

        **Utan e-postbekräftelse** (`REQUIRE_EMAIL_VERIFICATION=false`): 201 `ready`, kontot skapas direkt och kan logga in.

        **Regler.** `username`: `USERNAME_MIN` till `USERNAME_MAX` tecken (3 till 20), bokstäver, siffror, `_` och `-`, med en bokstav eller siffra först (`GET /info` anger dem som `limits`); reserverade namn (`admin`, `moderator`, `deleted`...) och vissa prefix avvisas; unikt oavsett versaler och gemener. `email`: trimmas och lagras med gemener, ren ASCII med en domän som innehåller punkt. `password`: minst `PASSWORD_MIN_LENGTH` (10) tecken och högst 256 byte UTF-8; det får inte innehålla användarnamnet eller e-postadressens lokala del och får inte vara ett vanligt lösenord.

        **Fel**, kontrollerade i denna ordning: 403 `registration_closed`; 400 `invalid_username`; 400 `invalid_email`; 400 `weak_password`; 409 `username_taken`; 428 `pow_required`; felen från kön för lösenordshashning; 409 `email_taken` (bara utan e-postbekräftelse).

        **Gränser:** `auth`, sedan `auth_register` (10 registreringar per timme per klient). **Proof of work:** alltid, när `POW_REGISTER_BITS` är större än 0.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
            examples:
              first:
                summary: Första försöket, utan proof of work
                value:
                  username: alice
                  email: alice@example.org
                  password: correct horse battery
              withPow:
                summary: Skickat igen med proof of work
                value:
                  username: alice
                  email: alice@example.org
                  password: correct horse battery
                  pow:
                    challenge: eyJ2IjoxLCJlIjoicmVnaXN0ZXIiLCJiIjoxOCwieCI6MTc5MDg4Mjk5MTIwMH0.6Ku_D7kw3S69OYxj-9KxsXszRCwz6ECnOQ-B6-v2erM
                    nonce: '123456'
      responses:
        '201':
          description: Kontot är skapat och kan logga in (ingen e-postbekräftelse på den här servern).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    const: ready
              example:
                status: ready
        '202':
          description: Registreringen väntar på sin bekräftelselänk (eller så har adressen redan ett konto; svaret avslöjar inte vilket).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerificationSentStatus'
              example:
                status: verification_sent
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_request` (med `field`), `invalid_json`, `invalid_username`, `invalid_email` eller `weak_password` med `reason`: `too_short`, `too_long`, `contains_username`, `contains_email` eller `too_common`.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`registration_closed`: registreringen är stängd på den här servern (`GET /info` anger `registration: closed`).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`username_taken`: ett konto har användarnamnet, eller så reserveras det av en väntande registrering med en annan adress. `email_taken`: ett annat konto använder adressen (bara utan e-postbekräftelse).'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '428':
          $ref: '#/components/responses/PowRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: gränsen `auth` eller `auth_register`, eller kön för lösenordshashning.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (kön för lösenordshashning, eller databasen förblev låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /auth/login:
    post:
      operationId: logIn
      tags:
        - auth
      summary: Logga in med lösenord
      description: |-
        Loggar in med ett användarnamn eller en e-postadress och ett lösenord. Det finns två möjliga svar, båda med status 200: en session, eller, när tvåfaktorsautentisering är på, ett andra steg som ska slutföras inom 5 minuter med `POST /auth/login/mfa`.

        Okänt konto, fel lösenord och konto utan lösenord får samma svar, efter samma tid: 401 `invalid_credentials`. Kontrollerna av kontot (`banned`, `email_unverified`) kommer först efter ett korrekt lösenord. Från och med `AUTH_FAILURES_PER_ACCOUNT` (5) misslyckanden på ett inloggningsnamn måste varje försök vänta (429 `too_many_attempts`).

        **Gräns:** `auth`. **Proof of work:** bara under en våg av misslyckade inloggningar (428 `pow_required`).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
            example:
              login: alice
              password: correct horse battery
              clientLabel: Scacelith 1.4 (Windows)
      responses:
        '200':
          description: En session, eller det andra steget i en inloggning med tvåfaktorsautentisering.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginAnswer'
              examples:
                session:
                  summary: Inloggad
                  value:
                    token: sct_L_8GDd7uzfQ3QQWtqrsWXDTsFWzRwIvJcwIGHhjWPS8
                    expiresAt: 1798658839708
                    user:
                      id: 1
                      username: alice
                      email: alice@example.org
                      emailVerified: true
                      mfaEnabled: false
                      googleLinked: false
                      hasPassword: true
                      acceptChallenges: all
                      createdAt: 1790882839743
                      lastLoginAt: 1790882839708
                      pendingEmail: null
                mfaRequired:
                  summary: Tvåfaktorsautentisering är på
                  value:
                    mfaRequired: true
                    mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                    expiresIn: 300
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_credentials`: okänt konto, fel lösenord eller ett konto utan lösenord.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: 'Bara efter ett korrekt lösenord: `banned`, med `until` (Unix-tid i ms, `null` för en permanent avstängning), eller `email_unverified` (ett äldre konto vars adress inte är bekräftad än).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '428':
          $ref: '#/components/responses/PowRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (väntetiden efter misslyckade försök för det här inloggningsnamnet), eller `rate_limited` (gränsen `auth`, kön för lösenordshashning).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (kön för lösenordshashning, eller databasen förblev låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /auth/login/mfa:
    post:
      operationId: logInWithSecondFactor
      tags:
        - auth
      summary: Slutför en inloggning med en andra faktor
      description: |-
        Det andra steget i en inloggning med tvåfaktorsautentisering, efter att `POST /auth/login` eller en Google-inloggning (`POST /auth/sso/google/finish` eller `POST /auth/sso/google/link`) har svarat `mfaRequired`. Skicka `code` (en 6-siffrig kod från autentiseringsappen, eller en återställningskod) eller `recoveryCode`. En återställningskod som används här är förbrukad.

        Ett steg accepterar högst 5 felaktiga koder; steget upphör också när lösenordet återställs eller byts. Efter lösenordssteget i en Google-koppling sparas kopplingen bara när koden godkänns här.

        **Gränser:** `auth`, och högst `AUTH_MFA_PER_ACCOUNT` (10) koder per 15 minuter för kontot, från vilken adress som helst; därutöver kontrolleras inte koden, så att en återställningskod inte förbrukas.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MfaLoginRequest'
            examples:
              authenticator:
                summary: En kod från autentiseringsappen
                value:
                  mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                  code: '123456'
              recovery:
                summary: En återställningskod
                value:
                  mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                  recoveryCode: j7v5-3ezx-zn
      responses:
        '200':
          description: Inloggad.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionAnswer'
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_request`: varken `code` eller `recoveryCode` skickades, eller så bryter bodyn mot schemat; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_mfa_token`: steget har gått ut, har använts eller upphörde efter 5 felaktiga koder, eller så har lösenordet återställts eller bytts sedan det första steget (logga in igen). `invalid_code`: en felaktig eller redan använd kod.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`banned` (med `until`), `email_unverified`.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_already_linked`: (bara efter lösenordssteget i en Google-koppling) Google-kontot har under tiden kopplats till ett annat konto.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: (bara efter lösenordssteget i en Google-koppling) kontot har ändrats under tiden; starta inloggningen igen.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts`: kontots väntetid efter misslyckade försök, eller så är kontots `AUTH_MFA_PER_ACCOUNT` koder för de senaste 15 minuterna förbrukade. `rate_limited`: gränsen `auth`.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` med `retryAfter: 1`: databasen förblev låst (efter ett steg i en Google-koppling har ingenting kopplats: starta inloggningen igen). `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /auth/logout:
    post:
      operationId: logOut
      tags:
        - sessions
      summary: Logga ut den här sessionen
      description: |-
        Återkallar sessionen för den token som används. Den WebSocket som öppnades med den stängs. Ingen body (eller `{}`).

        **Gräns:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Utloggad.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoggedOutStatus'
              example:
                status: logged_out
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_request`: en annan body än `{}`; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: gränsen `sessions` eller kontobudgeten.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databasen förblev låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /auth/logout-all:
    post:
      operationId: logOutEverywhere
      tags:
        - sessions
      summary: Logga ut alla sessioner
      description: |-
        Återkallar alla kontots sessioner, även den här. De WebSocket-anslutningar som öppnades med dem stängs. Ingen body (eller `{}`).

        **Gräns:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Alla sessioner är utloggade.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoggedOutStatus'
              example:
                status: logged_out
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_request`: en annan body än `{}`; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: gränsen `sessions` eller kontobudgeten.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databasen förblev låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /auth/verify-email/resend:
    post:
      operationId: resendVerificationEmail
      tags:
        - auth
      summary: Skicka bekräftelselänken igen
      description: |-
        Skickar länken för e-postbekräftelse igen. Svaret är 202 `accepted` oavsett adress, så att det aldrig avslöjar om ett konto eller en registrering använder den.

        Ett anrop har effekt högst en gång var 5:e minut per adress. En väntande registrering med den adressen får sina 24 timmar på nytt, oavsett om ett annat konto använder adressen eller inte, så att dess användarnamn förblir reserverat lika länge i båda fallen. En länk skickas bara för den registreringen när adressen inte har något konto (en ny länk, giltig i 24 timmar, ersätter den föregående), eller för ett aktivt, obekräftat konto med den adressen.

        **Gränser:** `auth`, sedan `auth_mail` (10 per timme per klient).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmailRequest'
            example:
              email: alice@example.org
      responses:
        '202':
          description: Accepterat (oavsett adress).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AcceptedStatus'
              example:
                status: accepted
        '400':
          $ref: '#/components/responses/BadRequest'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: gränsen `auth` eller `auth_mail` (oavsett adress).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` med `retryAfter: 1`: databasen förblev låst medan kontot slogs upp eller länken för ett obekräftat konto sparades (förnyelsen av en väntande registrering görs i mån av möjlighet och får aldrig anropet att misslyckas); adressens 5 minuter börjar ändå löpa, så ett nytt anrop inom dem gör ingenting. `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /auth/password/forgot:
    post:
      operationId: requestPasswordReset
      tags:
        - auth
      summary: Skicka en länk för att återställa lösenordet
      description: |-
        Skickar en återställningslänk för lösenordet, giltig i en timme, som öppnar sidan `/reset-password`. Svaret är 202 `accepted` oavsett adress. En länk skickas bara till ett aktivt konto, högst en gång var 5:e minut per adress. Ett konto med enbart Google-inloggning sätter sitt första lösenord på det här sättet.

        Återställning av glömda lösenord har API:ets strängaste gränser, alla räknade för hela servern: 3 anrop per timme (`AUTH_FORGOT_PER_HOUR`) och 10 per 24 timmar (`AUTH_FORGOT_PER_DAY`) per klient (en IPv4-adress eller ett IPv6-/64-nät), 3 gånger så många per IPv6-/48-nät, utöver gränsen `auth` (20 per 10 minuter) och utöver det enda e-postmeddelandet per adress var 5:e minut. Varken 202 eller 429 avslöjar om ett konto använder adressen. Att sätta det nya lösenordet har en egen gräns (`auth_reset`).

        **Gränser:** `auth`, `auth_forgot`, sedan `auth_forgot_day`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmailRequest'
            example:
              email: alice@example.org
      responses:
        '202':
          description: Accepterat (oavsett adress).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AcceptedStatus'
              example:
                status: accepted
        '400':
          $ref: '#/components/responses/BadRequest'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: gränsen `auth`, `auth_forgot` eller `auth_forgot_day` (oavsett adress).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` med `retryAfter: 1`: databasen förblev låst medan kontot slogs upp eller länken sparades; adressens 5 minuter börjar ändå löpa, så ett nytt anrop inom dem skickar ingen länk. `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /auth/password/reset:
    post:
      operationId: resetPassword
      tags:
        - auth
      summary: Sätt ett nytt lösenord med token från en återställningslänk
      description: |-
        Sätter ett nytt lösenord med token från en återställningslänk (sidan `/reset-password` gör samma sak). Det nya lösenordet följer reglerna för registrering.

        Återställningen har följande effekter, i en enda transaktion (alla eller ingen): den återkallar alla sessioner och avbryter ett väntande byte av e-postadress; kontots övriga återställningslänkar slutar fungera; adressen räknas som bekräftad (länken bevisade den); ägaren får ett e-postmeddelande. Tvåfaktorsautentiseringen påverkas inte. Efter ett fel från kön för lösenordshashning, en 503 eller en 500 har ingenting ändrats: det gamla lösenordet och sessionerna finns kvar, och länken förblir giltig.

        **Gränser:** `auth`, sedan `auth_reset` (10 per timme per klient, 30 per IPv6-/48-nät, delad med sidan: varje försök hashar ett lösenord).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasswordResetRequest'
            example:
              token: A0ATfmXCDKFC8UkSxLJCD85K7au5nMIrZ2P3K-1VWQg
              newPassword: a much better passphrase
      responses:
        '200':
          description: Lösenordet är bytt och alla sessioner är utloggade.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    const: password_reset
              example:
                status: password_reset
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_token`: länken är ogiltig, använd eller har gått ut, eller skickades till en adress som kontot inte längre har. `weak_password` (med `reason`). `invalid_request`, `invalid_json`.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: gränsen `auth` eller `auth_reset`, eller kön för lösenordshashning.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: kön för lösenordshashning, eller databasen förblev låst (`retryAfter: 1`, ingenting ändrades). `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /auth/sso/google/start:
    post:
      operationId: startGoogleSignIn
      tags:
        - google-sign-in
      summary: Starta en Google-inloggning
      description: |-
        Startar ett försök till Google-inloggning för PKCE-challengen och porten för spelets lyssnare på `127.0.0.1`. Svaret ger Google-URL:en som ska öppnas i systemets webbläsare och försökets `state`; försöket är giltigt i 10 minuter.

        `authUrl` innehåller `client_id`, `redirect_uri` (byggd av `redirectPort` och serverns ursprungstagg), `response_type=code`, `scope=openid email profile`, `state`, `nonce`, `code_challenge` (S256 av serverns egen verifier för Google) med `code_challenge_method=S256`, och `prompt=select_account`. Spelet kontrollerar den innan det öppnar den (se avsnittets beskrivning).

        **Gräns:** `sso_start`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SsoStartRequest'
            example:
              codeChallenge: 6e7diXEYxG7OTYw7STfNOltEeLxAilPthC_txzaE0xA
              redirectPort: 51234
      responses:
        '200':
          description: Försöket.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SsoStartAnswer'
              example:
                attemptId: sso_ogvVEL75n7q90NIFsh-4zXVt-izc3WRbghsd6HDOtNQ
                authUrl: https://accounts.google.com/o/oauth2/v2/auth?client_id=1234567890-abc.apps.googleusercontent.com&redirect_uri=http%3A%2F%2F127.0.0.1%3A51234%2Foauth2%2Fgoogle%2FIhcScoV7eDOzTEcSnqPUPt&response_type=code&scope=openid%20email%20profile&state=yBjjEhGgqHdeqhUAzyx9jE2IhQ56bec53iRA7RvKRzQ&nonce=gYIKKhwnz1AqWfFxOC3R-ZvCACw06q74fEMtGEOJQWU&code_challenge=ZfEI6iZjePTHbnbevupaD9O7GjBCzcL-GJu2XGOdFGc&code_challenge_method=S256&prompt=select_account
                state: yBjjEhGgqHdeqhUAzyx9jE2IhQ56bec53iRA7RvKRzQ
                expiresIn: 600
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: Google-inloggning erbjuds inte på den här servern.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: gränsen `sso_start`.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databasen förblev låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /auth/sso/google/web/start:
    post:
      operationId: startGoogleWebSignIn
      tags:
        - google-sign-in
      summary: Starta en Google-inloggning på en webbplats
      description: |-
        Startar ett försök till Google-inloggning från en webbplats, för PKCE-challengen från dess sida. Svaret är detsamma som för spelet: Google-URL:en att navigera till och försökets `state`; försöket är giltigt i 10 minuter. Erbjuds när `GET /info` anger `sso.googleWeb: true`.

        `authUrl` innehåller webbplatsens `client_id` och `redirect_uri=GOOGLE_WEB_REDIRECT_URI`, med samma övriga parametrar som för spelet. Sidan kontrollerar den innan den navigerar dit (se **På en webbplats** i avsnittets beskrivning).

        **Gräns:** `sso_start`, delad med spelets start.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SsoWebStartRequest'
            example:
              codeChallenge: 6e7diXEYxG7OTYw7STfNOltEeLxAilPthC_txzaE0xA
      responses:
        '200':
          description: Försöket.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SsoStartAnswer'
              example:
                attemptId: sso_ogvVEL75n7q90NIFsh-4zXVt-izc3WRbghsd6HDOtNQ
                authUrl: https://accounts.google.com/o/oauth2/v2/auth?client_id=1234567890-web.apps.googleusercontent.com&redirect_uri=https%3A%2F%2Fscacelith.com%2Faccount%2Fgoogle%2F&response_type=code&scope=openid%20email%20profile&state=yBjjEhGgqHdeqhUAzyx9jE2IhQ56bec53iRA7RvKRzQ&nonce=gYIKKhwnz1AqWfFxOC3R-ZvCACw06q74fEMtGEOJQWU&code_challenge=ZfEI6iZjePTHbnbevupaD9O7GjBCzcL-GJu2XGOdFGc&code_challenge_method=S256&prompt=select_account
                state: yBjjEhGgqHdeqhUAzyx9jE2IhQ56bec53iRA7RvKRzQ
                expiresIn: 600
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_request`: en saknad eller felformaterad `codeChallenge`, eller något annat fält (även `redirectPort`).'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: Google-inloggning erbjuds inte på den här servern, eller inte till webbplatser.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: gränsen `sso_start` (delad med spelets start).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databasen förblev låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /auth/sso/google/finish:
    post:
      operationId: finishGoogleSignIn
      tags:
        - google-sign-in
      summary: Lämna Googles svar till servern
      description: |-
        Lämnar den `code` och `state` som Google skickade till spelets lyssnare eller till webbplatsens sida vidare till servern, tillsammans med försökets id och PKCE-verifiern. Ett försöks-id är oanvändbart utan verifiern, och koden är oanvändbar utan serverns egen PKCE-verifier och klienthemlighet.

        Servern kontrollerar försöket (okänt, använt eller utgånget: 410), sedan verifiern (en felaktig verifier lämnar försöket användbart), förbrukar sedan försöket, kontrollerar `state` och `iss`, löser in koden hos Google och verifierar ID-tokenen. Svaret (200) är något av följande:

        - `{ token, expiresAt, user }`: inloggad på det kopplade kontot;
        - `{ mfaRequired, mfaToken, expiresIn }`: fortsätt med `POST /auth/login/mfa`;
        - `{ needsUsername, ssoTicket, suggestedUsername }`: ett nytt konto; fortsätt med `POST /auth/sso/complete` inom 10 minuter. `suggestedUsername` kommer från namnet i Google eller från adressen, och är `""` när inget passar;
        - `{ needsPassword, linkTicket, username, expiresIn }`: kontot med den adressen har ett lösenord; fortsätt med `POST /auth/sso/google/link`. Ingenting är kopplat än.

        **Gräns:** `sso_finish`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SsoFinishRequest'
            example:
              attemptId: sso_ogvVEL75n7q90NIFsh-4zXVt-izc3WRbghsd6HDOtNQ
              codeVerifier: Sb5SaoX0ByHGjJ0XQkEqaaPaJYx2BId4ncTT8tLM6W8
              state: yBjjEhGgqHdeqhUAzyx9jE2IhQ56bec53iRA7RvKRzQ
              code: 4/0AVMBsJhR2x7cKq9vT1pLmN3oW8yZ5aB6dE7fG8hJ
              iss: https://accounts.google.com
              clientLabel: Scacelith 1.4 (Windows)
      responses:
        '200':
          description: En session, ett andra steg eller nästa steg i en första Google-inloggning.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SsoFinishAnswer'
              examples:
                session:
                  summary: Inloggad på det kopplade kontot
                  value:
                    token: sct_L_8GDd7uzfQ3QQWtqrsWXDTsFWzRwIvJcwIGHhjWPS8
                    expiresAt: 1798658839708
                    user:
                      id: 1
                      username: alice
                      email: alice@example.org
                      emailVerified: true
                      mfaEnabled: false
                      googleLinked: true
                      hasPassword: false
                      acceptChallenges: all
                      createdAt: 1790882839743
                      lastLoginAt: 1790882839708
                      pendingEmail: null
                mfaRequired:
                  summary: Tvåfaktorsautentisering är på
                  value:
                    mfaRequired: true
                    mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                    expiresIn: 300
                needsUsername:
                  summary: En ny spelare
                  value:
                    needsUsername: true
                    ssoTicket: sso_gYIKKhwnz1AqWfFxOC3R-ZvCACw06q74fEMtGEOJQWU
                    suggestedUsername: alice
                needsPassword:
                  summary: Ett konto med lösenord använder adressen
                  value:
                    needsPassword: true
                    linkTicket: sso_ZfEI6iZjePTHbnbevupaD9O7GjBCzcL-GJu2XGOdFGc
                    username: alice
                    expiresIn: 600
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_verifier`: verifiern stämmer inte med försökets challenge (försöket förblir användbart). `sso_email_unverified`: Google har inte bekräftat adressen. `registration_closed`. `account_disabled`. Bara för ett kopplat konto: `banned` (med `until`), `email_unverified`.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: Google-inloggning erbjuds inte på den här servern.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_account_exists`: ett aktivt konto utan lösenord använder adressen (kontot namnges inte).'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: ett okänt, använt eller utgånget försök.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: gränsen `sso_finish`.'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/BadGateway'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databasen förblev låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /auth/sso/google/link:
    post:
      operationId: linkGoogleAccount
      tags:
        - google-sign-in
      summary: Koppla Google till ett befintligt konto med dess lösenord
      description: |-
        Efter att `finish` har svarat `needsPassword`: kopplar Google till det befintliga kontot med det kontots lösenord, inskrivet i spelet. Svaret (200) är en session (kopplingen sparas och spelaren loggas in), eller, när tvåfaktorsautentisering är på, `{ mfaRequired, mfaToken, expiresIn }`: fortsätt med `POST /auth/login/mfa`, och kopplingen sparas bara när en kod godkänns där.

        Ett fel lösenord lämnar biljetten användbar, för totalt 5 försök. Ett försök dras precis innan lösenordet kontrolleras: en 429 `too_many_attempts` eller en 428 `pow_required` drar inget, medan ett avslag från kön för lösenordshashning (503 `server_busy`, 429 `rate_limited`) redan har dragit ett och räknas som ett misslyckande för kontot. Väntetiden efter misslyckade försök använder samma räknare som `POST /auth/login`.

        **Gräns:** `auth` (med sin räkning per IPv6-/48-nät). **Proof of work:** som för `POST /auth/login`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SsoLinkRequest'
            example:
              linkTicket: sso_ZfEI6iZjePTHbnbevupaD9O7GjBCzcL-GJu2XGOdFGc
              password: correct horse battery
              clientLabel: Scacelith 1.4 (Windows)
      responses:
        '200':
          description: En session, eller inloggningens andra steg.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginAnswer'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_credentials`: fel lösenord; biljetten finns kvar, för totalt 5 försök.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`banned` (med `until`), endast efter ett korrekt lösenord.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: Google-inloggning erbjuds inte på den här servern.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_already_linked`: Google-kontot har under tiden kopplats till ett annat konto.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: en okänd, använd eller utgången biljett, det femte felaktiga lösenordet, ett konto vars status eller adress har ändrats sedan `finish`, eller ett konto vars status, adress, lösenord eller tvåfaktorsautentisering ändrades medan kopplingen sparades. Börja om inloggningen.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '428':
          $ref: '#/components/responses/PowRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (kontots väntetid efter misslyckade försök), eller `rate_limited` (gränsen `auth`, kön för lösenordshashning).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: kön för lösenordshashning, eller databasen förblev låst (`retryAfter: 1`, inget kopplades). `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /auth/sso/complete:
    post:
      operationId: completeGoogleSignUp
      tags:
        - google-sign-in
      summary: Skapa kontot vid en första inloggning med Google
      description: |-
        Efter att `finish` har svarat `needsUsername`: skapar kontot med det valda användarnamnet (registreringens regler gäller) och loggar in på det. Kontot har inget lösenord: `hasPassword` är `false`, och ”Glömt lösenordet?” (`POST /auth/password/forgot`) ger det ett.

        **Gräns:** `auth` (med dess räkning per IPv6-/48-nät).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SsoCompleteRequest'
            example:
              ssoTicket: sso_gYIKKhwnz1AqWfFxOC3R-ZvCACw06q74fEMtGEOJQWU
              username: alice
              clientLabel: Scacelith 1.4 (Windows)
      responses:
        '200':
          description: Kontot är skapat och inloggat.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionAnswer'
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_username`, `invalid_request`, `invalid_json`.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`registration_closed`.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: Google-inloggning erbjuds inte på den här servern.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`username_taken` (ett konto har användarnamnet, eller en väntande registrering med en annan adress har reserverat det), `sso_already_linked`, `email_taken`.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: en okänd, använd eller utgången biljett.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: gränsen `auth`.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databasen förblev låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /auth/sessions:
    get:
      operationId: listSessions
      tags:
        - sessions
      summary: Lista de inloggade enheterna
      description: |-
        Kontots aktiva sessioner (inloggade enheter), den senast använda först. `current` markerar sessionen som gör anropet. `lastSeenAt` uppdateras högst var 5:e minut (senare medan servern ligger långt efter); `expiresAt` är det absoluta slutet, och gränsen för inaktivitet kan avsluta sessionen tidigare.

        **Gräns:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: De aktiva sessionerna.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionList'
              example:
                sessions:
                  - id: 3
                    createdAt: 1790882839708
                    lastSeenAt: 1790882839708
                    expiresAt: 1798658839708
                    clientLabel: Laptop
                    current: false
                  - id: 1
                    createdAt: 1790882839708
                    lastSeenAt: 1790882839708
                    expiresAt: 1798658839708
                    clientLabel: Scacelith 1.4 (Windows)
                    current: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: gränsen `sessions` eller kontobudgeten.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databasen förblev låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /auth/sessions/{id}:
    delete:
      operationId: revokeSession
      tags:
        - sessions
      summary: Logga ut en enhet
      description: |-
        Loggar ut en av kontots sessioner; även den aktuella kan loggas ut. Den WebSocket som öppnades med sessionen stängs. Ingen body (eller `{}`).

        **Gräns:** `sessions`.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/SessionId'
      responses:
        '200':
          description: Sessionen är utloggad.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    const: revoked
              example:
                status: revoked
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_request`: en annan body än `{}`, eller ett `id` som inte är giltig URL-kodning; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: ingen aktiv session med detta id på det här kontot.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: gränsen `sessions` eller kontobudgeten.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databasen förblev låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /account/me:
    get:
      operationId: getAccount
      tags:
        - account
      summary: Hämta kontot, dess ratingtal och aktiva sanktioner
      description: |-
        Kontot som dess spelare ser det: kontovyn (även `user` i varje svar på en inloggning), en ratingpost per kategori där spelaren har spelat partier med rating, de aktiva sanktionerna och avstängningen. Fuskskyddets integritetsnivå visas aldrig.

        **Gräns:** `account`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Kontot.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountMe'
              example:
                user:
                  id: 1
                  username: alice
                  email: alice@example.org
                  emailVerified: true
                  mfaEnabled: true
                  googleLinked: false
                  hasPassword: true
                  acceptChallenges: all
                  createdAt: 1790882871478
                  lastLoginAt: 1790882902200
                  pendingEmail: alice.new@example.org
                ratings:
                  - category: '3+2'
                    rating: 1510
                    games: 2
                    wins: 1
                    draws: 1
                    losses: 0
                    peak: 1510
                    provisional: true
                sanctions: []
                ban: null
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`unauthorized`, eller `invalid_token` (även när kontot har raderats).'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: gränsen `account` eller kontobudgeten.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databasen förblev låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /account/preferences:
    put:
      operationId: updatePreferences
      tags:
        - account
      summary: Ta emot eller avvisa direkta utmaningar
      description: |-
        Anger om andra spelare får utmana den här spelaren via namn. Med `none` avvisas direkta utmaningar: utmanaren får veta att spelaren inte är tillgänglig. Ingen förnyad autentisering.

        **Gräns:** `account`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Preferences'
            example:
              acceptChallenges: none
      responses:
        '200':
          description: De inställningar som nu gäller.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - preferences
                properties:
                  preferences:
                    $ref: '#/components/schemas/Preferences'
              example:
                preferences:
                  acceptChallenges: none
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: gränsen `account` eller kontobudgeten.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databasen förblev låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /account/password:
    post:
      operationId: changePassword
      tags:
        - account
      summary: Byt lösenord
      description: |-
        Byter lösenordet. Det nuvarande lösenordet krävs, men ingen andra faktor, inte ens med tvåfaktorsautentisering påslagen. Det nya lösenordet följer registreringens regler (kontrolleras efter det nuvarande lösenordet).

        Bytet har följande effekter, i en enda transaktion (alla eller ingen): det återkallar alla andra sessioner (den här förblir inloggad), avbryter ett väntande byte av e-postadress, gör att kontots länkar för återställning av lösenordet slutar fungera, och skickar ett e-postmeddelande till ägaren.

        **Förnyad autentisering:** endast lösenordet. **Gränser:** `reauth`, sedan `reauth_user`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasswordChangeRequest'
            example:
              currentPassword: correct horse battery
              newPassword: a much better passphrase
      responses:
        '200':
          description: Lösenordet är bytt.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    const: password_changed
              example:
                status: password_changed
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`password_not_set` (ett konto med enbart Google-inloggning), `weak_password` (med `reason`), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password`: fel nuvarande lösenord, eller en återställning eller ett byte av lösenordet som skedde medan anropet kontrollerades.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (kontots misslyckade förnyade autentiseringar), eller `rate_limited` (gränsen `reauth` eller `reauth_user`, kontobudgeten, kön för lösenordshashning).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: kön för lösenordshashning, eller databasen förblev låst (`retryAfter: 1`, inget ändrades). `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /account/mfa/totp/setup:
    post:
      operationId: startTotpSetup
      tags:
        - two-step-verification
      summary: Börja aktivera tvåfaktorsautentisering
      description: |-
        Sparar en ny väntande autentiseringshemlighet, som ersätter en eventuell tidigare väntande, och returnerar den för autentiseringsappen (som text och som en `otpauth://`-URI att visa som QR-kod). Tvåfaktorsautentisering är inte påslagen än: `POST /account/mfa/totp/enable` slår på den med en kod från den här hemligheten.

        **Förnyad autentisering:** endast lösenordet. **Gränser:** `reauth`, sedan `reauth_user`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasswordOnlyRequest'
            example:
              password: correct horse battery
      responses:
        '200':
          description: Den väntande hemligheten.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TotpSetup'
              example:
                secret: OCKJVMPMMKMPLBSIYLN6QQRMKIPC2VLH
                uri: otpauth://totp/Scacelith:alice?secret=OCKJVMPMMKMPLBSIYLN6QQRMKIPC2VLH&issuer=Scacelith&algorithm=SHA1&digits=6&period=30
                algorithm: SHA1
                digits: 6
                period: 30
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`password_not_set` (ett konto med enbart Google-inloggning), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password`.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`mfa_already_enabled` (kontrolleras före lösenordet).'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (kontots misslyckade förnyade autentiseringar), eller `rate_limited` (gränsen `reauth` eller `reauth_user`, kontobudgeten, kön för lösenordshashning).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (kön för lösenordshashning, eller databasen förblev låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /account/mfa/totp/enable:
    post:
      operationId: enableTotp
      tags:
        - two-step-verification
      summary: Slutför aktiveringen av tvåfaktorsautentisering och få återställningskoder
      description: |-
        Slår på tvåfaktorsautentisering med en kod från den väntande hemligheten (exakt 6 siffror). Inget lösenord efterfrågas här; det angavs i det första steget (setup). Svaret innehåller 10 återställningskoder, som visas bara denna gång.

        **Gränser:** `reauth`, sedan `reauth_user`; en felaktig kod räknas bland kontots misslyckade förnyade autentiseringar.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TotpEnableRequest'
            example:
              code: '123456'
      responses:
        '200':
          description: Tvåfaktorsautentisering är på.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                  - recoveryCodes
                properties:
                  status:
                    const: mfa_enabled
                  recoveryCodes:
                    $ref: '#/components/schemas/RecoveryCodeList'
              example:
                status: mfa_enabled
                recoveryCodes:
                  - j7v5-3ezx-zn
                  - 4kqm-8w2p-hd
                  - x0ra-c6tn-5g
                  - mb3s-9yzj-k1
                  - 2hvd-pq7e-w8
                  - c5ng-0tka-xr
                  - zz4y-b1me-7q
                  - 8pjh-6dsw-3n
                  - t9xc-2gfk-0v
                  - q6wa-ner5-jy
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_request`: `code` är inte exakt 6 siffror, eller bodyn bryter mot schemat; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_code`: fel kod (kontrollera klockan på enheten).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`mfa_already_enabled`; `mfa_setup_required`: ingen väntande hemlighet (anropa `POST /account/mfa/totp/setup` först).'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (kontots misslyckade förnyade autentiseringar), eller `rate_limited` (gränsen `reauth` eller `reauth_user`, kontobudgeten).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databasen förblev låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /account/mfa/totp/disable:
    post:
      operationId: disableTotp
      tags:
        - two-step-verification
      summary: Stäng av tvåfaktorsautentisering
      description: |-
        Stänger av tvåfaktorsautentisering. Hemligheten och återställningskoderna raderas, och ägaren får ett e-postmeddelande.

        **Förnyad autentisering:** lösenordet och en kod från autentiseringsappen eller en återställningskod (en av `code` och `recoveryCode` krävs). **Gränser:** `reauth`, sedan `reauth_user`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReauthCredentials'
            example:
              password: correct horse battery
              code: '123456'
      responses:
        '200':
          description: Tvåfaktorsautentisering är avstängd.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    const: mfa_disabled
              example:
                status: mfa_disabled
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`password_not_set` (ett konto med enbart Google-inloggning), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`mfa_code_required` (varken `code` eller `recoveryCode`, kontrolleras före lösenordet), `invalid_password`, `invalid_code`.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`mfa_not_enabled`.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (misslyckade förnyade autentiseringar, eller för många prövade koder på det här kontot), eller `rate_limited` (gränsen `reauth` eller `reauth_user`, kontobudgeten, kön för lösenordshashning).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (kön för lösenordshashning, eller databasen förblev låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /account/mfa/recovery-codes:
    post:
      operationId: regenerateRecoveryCodes
      tags:
        - two-step-verification
      summary: Ersätt återställningskoderna
      description: |-
        Ersätter återställningskoderna med 10 nya; de gamla slutar fungera.

        **Förnyad autentisering:** lösenordet och en kod från autentiseringsappen i `code` (en återställningskod avvisas med 403 `invalid_code`). **Gränser:** `reauth`, sedan `reauth_user`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecoveryCodesRequest'
            example:
              password: correct horse battery
              code: '123456'
      responses:
        '200':
          description: De nya återställningskoderna, som visas bara denna gång.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - recoveryCodes
                properties:
                  recoveryCodes:
                    $ref: '#/components/schemas/RecoveryCodeList'
              example:
                recoveryCodes:
                  - j7v5-3ezx-zn
                  - 4kqm-8w2p-hd
                  - x0ra-c6tn-5g
                  - mb3s-9yzj-k1
                  - 2hvd-pq7e-w8
                  - c5ng-0tka-xr
                  - zz4y-b1me-7q
                  - 8pjh-6dsw-3n
                  - t9xc-2gfk-0v
                  - q6wa-ner5-jy
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`password_not_set` (ett konto med enbart Google-inloggning), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password`, `mfa_code_required`, `invalid_code` (även för en återställningskod).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`mfa_not_enabled`.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (misslyckade förnyade autentiseringar, eller för många prövade koder på det här kontot), eller `rate_limited` (gränsen `reauth` eller `reauth_user`, kontobudgeten, kön för lösenordshashning).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (kön för lösenordshashning, eller databasen förblev låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /account/email:
    post:
      operationId: changeEmail
      tags:
        - email-change
      summary: Byt e-postadress
      description: |-
        Byter kontots e-postadress. `newEmail` trimmas och görs om till gemener, och kontrolleras sedan som en adress vid registrering.

        **Med e-postbekräftelse** (standard): 202 `verification_sent`. En länk som gäller i 24 timmar skickas till den nya adressen; den öppnar `/confirm-email-change`, och adressen ändras först när spelaren trycker på knappen på den sidan. Fram till dess visar `GET /account/me` den nya adressen som `pendingEmail`. Ett nytt anrop ersätter det väntande bytet; ett byte eller en återställning av lösenordet avbryter det. Högst en länk skickas till en given ny adress var 5:e minut, oavsett vem som frågar (ett anrop för samma byte som redan väntar behåller den tidigare skickade länken, som förblir giltig); svaret är detsamma. Den nuvarande adressen får ett meddelande om att ett byte till en maskerad adress (`a***@example.org`) har begärts. Svaret och `pendingEmail` är desamma när ett annat konto redan använder den nya adressen: då skickas ingen länk, så det bytet blir aldrig av, och ägaren till den adressen får i stället ett meddelande (högst ett per timme).

        När länken bekräftas ändras adressen och räknas som bekräftad, enheterna förblir inloggade, de länkar som skickats tidigare (bekräftelse, återställning av lösenordet, andra byten) slutar fungera, och den tidigare adressen underrättas, med den nya maskerad.

        **Utan e-postbekräftelse** (`REQUIRE_EMAIL_VERIFICATION=false`): adressen ändras direkt (200 `email_changed`), och den tidigare adressen underrättas. Om ett annat konto använder adressen blir svaret 409 `email_taken`, och ägaren till den adressen får meddelandet.

        **Förnyad autentisering:** lösenordet och, med tvåfaktorsautentisering, en kod från autentiseringsappen eller en återställningskod. `invalid_email` och `same_email` kontrolleras före lösenordet, så de räknas inte som misslyckanden. **Gränser:** `reauth`, sedan `reauth_user`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmailChangeRequest'
            example:
              newEmail: alice.new@example.org
              password: correct horse battery
              code: '123456'
      responses:
        '200':
          description: Adressen ändrades direkt (ingen e-postbekräftelse på den här servern).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                  - email
                properties:
                  status:
                    const: email_changed
                  email:
                    type: string
                    format: email
                    description: Den nya adressen, så som den sparades.
              example:
                status: email_changed
                email: alice.new@example.org
        '202':
          description: En bekräftelselänk skickades till den nya adressen (eller så tillhör adressen ett annat konto; svaret avslöjar inte vilket).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerificationSentStatus'
              example:
                status: verification_sent
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_email`, `same_email` (kontots nuvarande adress), `password_not_set` (ett konto med enbart Google-inloggning), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password` (även när ett byte eller en återställning av lösenordet hann före anropet: ingen länk skickas), `mfa_code_required`, `invalid_code`.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`email_taken`: endast utan e-postbekräftelse.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (misslyckade förnyade autentiseringar, eller för många prövade koder på det här kontot), eller `rate_limited` (gränsen `reauth` eller `reauth_user`, kontobudgeten, kön för lösenordshashning).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: kön för lösenordshashning, eller databasen förblev låst (`retryAfter: 1`: inget ändrades, och samma anrop kan skickas igen). `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /account/export:
    post:
      operationId: exportAccountData
      tags:
        - data-export
      summary: Ladda ned kontots data
      description: |-
        Allt som servern lagrar om kontot, som en enda JSON-fil att spara (`format` `scacelith-account-export`, `version` 1). Exporten registrerar en säkerhetshändelse (`account_exported`). Dokumentets array `notes` talar om för spelaren på enkel engelska vad som utelämnas.

        **Aldrig med i exporten:** lösenordshashen, hemligheten för tvåfaktorsautentisering och återställningskoderna; sessions- och länktoken av alla slag, och hashar av dem; fuskskyddets data (integritetsnivå och poäng, avvikelser, analysen av partierna, en anmälans vikt); de anmälningar som andra spelare har gjort om spelaren; moderatorernas identitet; andra spelares privata data (motståndare visas med sitt offentliga namn och sin rating, ingenting avslöjar om en annan spelare har fått sanktioner, och ingen IP-adress som kan tillhöra en annan person tas med).

        **Förnyad autentisering:** lösenordet och, med tvåfaktorsautentisering, en kod från autentiseringsappen eller en återställningskod. **Gränser:** `account_export` (5 per timme och spelare, för hela servern; varje försök räknas, även misslyckade; kontrolleras först), sedan `reauth` och `reauth_user`. **Tidsgräns:** 60 sekunder.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReauthCredentials'
            example:
              password: correct horse battery
              code: '123456'
      responses:
        '200':
          description: Exportdokumentet, som bilaga.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            Content-Disposition:
              description: '`attachment; filename="scacelith-account-<username>.json"`. Andra tecken än bokstäver, siffror, `_`, `.` och `-` i användarnamnet blir `_`.'
              schema:
                type: string
                pattern: '^attachment; filename="scacelith-account-[A-Za-z0-9_.-]+\.json"$'
              example: attachment; filename="scacelith-account-alice.json"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountExport'
              example:
                format: scacelith-account-export
                version: 1
                exportedAt: 1790882839708
                server:
                  name: Scacelith
                  host: caissa.scacelith.com
                notes:
                  - This file holds the data Scacelith keeps about your account. Times are milliseconds since 1970-01-01 UTC.
                account:
                  id: 1
                  username: alice
                  email: alice@example.org
                  emailVerified: true
                  pendingEmail: null
                  mfaEnabled: false
                  googleLinked: false
                  googleEmail: null
                  hasPassword: true
                  acceptChallenges: all
                  createdAt: 1790882839743
                  lastLoginAt: 1790882839708
                ratings:
                  - category: '3+2'
                    rating: 1510
                    games: 2
                    wins: 1
                    draws: 1
                    losses: 0
                    peak: 1510
                    provisional: true
                    rated: true
                    countedGames: 2
                    updatedAt: 1790882839809
                ratingRefunds:
                  - day: 1790812800000
                    category: '3+2'
                    points: 9
                sessions:
                  - id: 1
                    createdAt: 1790882839708
                    lastSeenAt: 1790882839708
                    expiresAt: 1798658839708
                    revokedAt: null
                    clientLabel: Scacelith 1.4 (Windows)
                    ip: 203.0.113.7
                securityEvents:
                  - kind: login
                    at: 1790882839708
                    ip: 203.0.113.7
                    detail:
                      method: password
                sanctions: []
                conduct:
                  - kind: abort
                    at: 1790800000000
                reportsFiled:
                  - gameId: 4100000000001
                    reported: bob
                    category: other
                    comment: rude
                    createdAt: 1790882840000
                    status: open
                games:
                  total: 1
                  list:
                    - id: 4100000000001
                      category: '3+2'
                      rated: true
                      timeControl: '180+2'
                      white:
                        name: alice
                        rating: 1500
                        ratingAfter: 1510
                        ratingDiff: 10
                      black:
                        name: bob
                        rating: 1520
                        ratingAfter: 1510
                        ratingDiff: -10
                      color: white
                      status: 1
                      reason: 2
                      result: 1-0
                      termination: Resignation
                      plies: 41
                      startedAt: 1790620205000
                      endedAt: 1790620611000
                      baseMs: 180000
                      incMs: 2000
                      outcome: win
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`password_not_set` (ett konto med enbart Google-inloggning), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password`, `mfa_code_required`, `invalid_code`.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (misslyckade förnyade autentiseringar, eller för många prövade koder på det här kontot), eller `rate_limited` (gränsen `account_export`, `reauth` eller `reauth_user`, kontobudgeten, kön för lösenordshashning).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (databasen förblev låst, med headern `Retry-After: 1`), `server_busy` (kön för lösenordshashning), `timeout` (60 sekunder). Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /account/delete:
    post:
      operationId: deleteAccount
      tags:
        - account-deletion
      summary: Radera kontot
      description: |-
        Raderar kontot; det kan inte ångras.

        - Alla sessioner återkallas direkt: tokenen får 401 `invalid_token` från och med då.
        - Användarnamnet blir `deleted#<id>`, i kontot och i varje partipost.
        - Följande raderas: e-postadressen, lösenordshashen, tvåfaktorshemligheten och återställningskoderna, sessionerna och länktoken, kopplingen till Google, fuskskyddets integritetspost och de IP-adresser som sparats med säkerhetshändelser.
        - Ratingtal och partier behålls. Partierna förblir läsbara under det anonyma namnet, och `GET /players/{username}` svarar 404 för det tidigare namnet.

        **Förnyad autentisering:** lösenordet och, med tvåfaktorsautentisering, en kod från autentiseringsappen eller en återställningskod. **Gränser:** `reauth`, sedan `reauth_user`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReauthCredentials'
            example:
              password: correct horse battery
              code: '123456'
      responses:
        '200':
          description: Kontot är raderat.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    const: deleted
              example:
                status: deleted
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`password_not_set` (ett konto med enbart Google-inloggning), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password`, `mfa_code_required`, `invalid_code`.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (misslyckade förnyade autentiseringar, eller för många prövade koder på det här kontot), eller `rate_limited` (gränsen `reauth` eller `reauth_user`, kontobudgeten, kön för lösenordshashning).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (kön för lösenordshashning, eller databasen förblev låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /account/games:
    get:
      operationId: listAccountGames
      tags:
        - game-history
      summary: Lista spelarens partier, filtrerade och sidindelade
      description: |-
        Den inloggade spelarens partier, nyaste först, filtrerade och sidindelade, med antalet partier som matchar filtret. Alla query-parametrar är valfria, och ett tomt värde räknas som utelämnat.

        Sidindelning: skicka föregående sidas `next` som `before`; `next` är `null` på sista sidan. `total` räknar de partier som matchar filtret, över alla sidor.

        **Gräns:** `account_games`.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/BeforeQuery'
        - $ref: '#/components/parameters/LimitQuery'
        - name: category
          in: query
          required: false
          description: Ett officiellt kategori-id (`3+2`, eller `3%2B2`), eller `custom` för alla partier med en annan betänketid.
          schema:
            type: string
            pattern: '^\s*([0-9]+[+ ][0-9]+|custom)\s*$'
          example: '3+2'
        - name: rated
          in: query
          required: false
          description: Endast partier med rating (`true`) eller utan rating (`false`).
          schema:
            type: string
            enum:
              - 'true'
              - 'false'
        - name: result
          in: query
          required: false
          description: Endast de partier som vanns, förlorades eller slutade remi, sett från spelarens sida. Avbrutna partier visas bara utan detta filter.
          schema:
            type: string
            enum:
              - win
              - loss
              - draw
      responses:
        '200':
          description: En sida av historiken.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HistoryPage'
              example:
                games:
                  - id: 4100000000001
                    category: '3+2'
                    rated: true
                    timeControl: '180+2'
                    white:
                      name: alice
                      rating: 1500
                      ratingAfter: 1510
                      ratingDiff: 10
                    black:
                      name: bob
                      rating: 1520
                      ratingAfter: 1510
                      ratingDiff: -10
                    color: white
                    status: 1
                    reason: 2
                    result: 1-0
                    termination: Resignation
                    plies: 41
                    startedAt: 1790620205000
                    endedAt: 1790620611000
                    baseMs: 180000
                    incMs: 2000
                    outcome: win
                next: null
                total: 1
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_cursor` (`before` är inget parti-id), `invalid_limit`, eller `invalid_filter` (`category`, `rated` eller `result`), var och en med `field` som anger parametern.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: gränsen `account_games` eller kontobudgeten.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (databasen förblev låst; `retryAfter: 1` i bodyn, ingen `Retry-After`-header), `server_busy` (sessionsuppslaget fann databasen låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /games/{id}:
    get:
      operationId: getGame
      tags:
        - games
      summary: Hämta en partipost med dess drag och klockor
      description: |-
        En partipost, med dess drag och klockor. Utan token, eller med token för en spelare som inte spelade partiet, är svaret det offentliga. När tokenens spelare spelade partiet lägger svaret till `you` och `reportable`.

        **Gräns:** `public_read` (per spelare med token, per klient utan).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
      responses:
        '200':
          description: Partiposten.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GameRecord'
              example:
                id: 4100000000001
                category: '3+2'
                rated: true
                timeControl: '180+2'
                white:
                  name: alice
                  rating: 1500
                  ratingAfter: 1510
                  ratingDiff: 10
                black:
                  name: bob
                  rating: 1520
                  ratingAfter: 1510
                  ratingDiff: -10
                status: 1
                reason: 2
                result: 1-0
                termination: Resignation
                plies: 2
                startedAt: 1790620205000
                endedAt: 1790620611000
                baseMs: 180000
                incMs: 2000
                statusName: WhiteWins
                rematchOf: null
                moves:
                  - uci: e2e4
                    spentMs: 0
                    clockMs: 180000
                  - uci: e7e5
                    spentMs: 1700
                    clockMs: 180300
                pgn:
                  Event: Scacelith rated 3+2
                  Site: caissa.scacelith.com
                  Date: '2026.09.28'
                  Round: '-'
                  White: alice
                  Black: bob
                  Result: 1-0
                  WhiteElo: 1500
                  BlackElo: 1520
                  TimeControl: '180+2'
                  Termination: Resignation
                  PlyCount: 2
                you: white
                reportable: true
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_game_id`: inte ett positivt heltal med högst 16 siffror (under 2^53); `invalid_request`: inte giltig URL-kodning.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: en token skickades och den är inte giltig.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: partiet finns inte.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: gränsen `public_read` eller kontobudgeten.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (databasen förblev låst; `retryAfter: 1` i bodyn, ingen `Retry-After`-header), `server_busy` (sessionsuppslaget fann databasen låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /games/{id}/pgn:
    get:
      operationId: getGamePgn
      tags:
        - games
      summary: Ladda ned ett parti som PGN-fil
      description: |-
        Samma parti som PGN-fil: ett parti med radslut `\n`, och dragtext på rader kortare än 80 kolumner. Svaret är detsamma med eller utan token.

        Taggar, i denna ordning: `Event` (`<SERVER_NAME> rated <category>` eller `<SERVER_NAME> casual <category>`), `Site` (`SERVER_PUBLIC_HOST`), `Date` (startdatumet i UTC), `Round`, `White`, `Black`, `Result` (`*` för ett avbrutet parti), `UTCDate` och `UTCTime` (starten), `WhiteElo` och `BlackElo` (ratingtalen vid start, eller `-`), `WhiteRatingDiff` och `BlackRatingDiff` (förändringarna, till exempel `+10` och `-10`, i varje parti med rating, `+0` när ratingreglerna lämnar ratingen oförändrad; ett parti utan rating, ett anpassat parti eller ett avbrutet parti har ingen av dem), `TimeControl` (i sekunder), `Termination` (ett standardvärde i PGN: `normal`; `time forfeit`, flaggan föll, även när partiet slutar remi; `abandoned`; `rules infraction`, ett andra otillåtet drag eller en tilldömd förlust för brott mot rent spel; `unterminated`, ett avbrutet parti), `PlyCount`, `ScacelithGameId` (det decimala id:t).

        Varje drag har `{[%clk h:mm:ss.f] [%emt h:mm:ss.f]}`: den dragande spelarens klocka efter draget och den tid som räknades för draget, i tiondels sekunder (avkortat). Ett värde utelämnas när posten saknar det. Efter sista draget följer avslutningsorsaken i ord, sedan resultatet.

        **Gräns:** `public_read` (per spelare med token, per klient utan).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
      responses:
        '200':
          description: PGN-filen.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            Content-Disposition:
              description: '`attachment; filename="scacelith-<id>.pgn"`.'
              schema:
                type: string
                pattern: '^attachment; filename="scacelith-[1-9][0-9]{0,15}\.pgn"$'
              example: attachment; filename="scacelith-4100000000001.pgn"
          content:
            application/x-chess-pgn:
              schema:
                type: string
                description: 'PGN-texten, i UTF-8 (`Content-Type: application/x-chess-pgn; charset=utf-8`).'
              example: |
                [Event "Scacelith rated 3+2"]
                [Site "caissa.scacelith.com"]
                [Date "2026.09.28"]
                [Round "-"]
                [White "alice"]
                [Black "bob"]
                [Result "1-0"]
                [UTCDate "2026.09.28"]
                [UTCTime "18:30:05"]
                [WhiteElo "1500"]
                [BlackElo "1520"]
                [WhiteRatingDiff "+10"]
                [BlackRatingDiff "-10"]
                [TimeControl "180+2"]
                [Termination "normal"]
                [PlyCount "2"]
                [ScacelithGameId "4100000000001"]

                1. e4 {[%clk 0:03:00.0] [%emt 0:00:00.0]} 1... e5 {[%clk 0:03:00.3]
                [%emt 0:00:01.7]} {Resignation} 1-0
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_game_id`: inte ett positivt heltal med högst 16 siffror (under 2^53); `invalid_request`: inte giltig URL-kodning.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: en token skickades och den är inte giltig.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: partiet finns inte.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: gränsen `public_read` eller kontobudgeten.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`internal_error`: bland annat när de sparade dragen inte kan spelas upp till det sparade slutet (servern loggar det).'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (databasen förblev låst; `retryAfter: 1` i bodyn, ingen `Retry-After`-header), `server_busy` (sessionsuppslaget fann databasen låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /games/{id}/gif:
    get:
      operationId: getGameGif
      tags:
        - gifs
      summary: Ladda ned ett parti från den här servern som animerad GIF
      description: |-
        Partiet som animerad GIF. Namnen och ratingtalen är partipostens (ratingtalen vid start, ett raderat konto som `deleted#<id>`), och detsamma gäller resultatet och avslutningen (`Resignation`, `Loss on time`...).

        Kontrollerna görs i denna ordning: `gif_disabled`, bildalternativen (`size`, `orientation`, `delay`, `coords`), parti-id:t, partiet, dess längd.

        **Session krävs:** kvoterna räknas per konto. **Gränser:** `gif` vid varje anrop; `gif_user_min`, `gif_user_hour`, `gif_ip_min` och `gif_ip_hour` endast när GIF:en måste skapas (se avsnittets beskrivelse). **Tidsgräns:** `GIF_QUEUE_TIMEOUT_MS` + `GIF_RENDER_TIMEOUT_MS` + 5 sekunder (45 sekunder som standard).
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
        - name: size
          in: query
          required: false
          description: Bildstorleken, `small` (rutor på 32 px), `medium` (48 px) eller `large` (72 px).
          schema:
            type: string
            enum:
              - small
              - medium
              - large
            default: medium
        - name: orientation
          in: query
          required: false
          description: Den sida som visas nederst på brädet.
          schema:
            type: string
            enum:
              - white
              - black
            default: white
        - name: delay
          in: query
          required: false
          description: Millisekunder per drag (1 till 6 siffror, i decimal form).
          schema:
            type: integer
            minimum: 100
            maximum: 3000
            default: 500
        - name: coords
          in: query
          required: false
          description: Om linjernas bokstäver och radernas siffror ritas runt brädet (`1`) eller inte (`0`).
          schema:
            type: string
            enum:
              - '1'
              - '0'
            default: '1'
      responses:
        '200':
          $ref: '#/components/responses/GifFile'
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_option` med `field` (`size`, `orientation`, `delay` eller `coords`): ett värde utanför de tillåtna. `invalid_game_id`. `invalid_request`: inte giltig URL-kodning.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: partiet finns inte. `gif_disabled`: servern har stängt av GIF:ar (`GIF_ENABLED=false`; tokenen för `gif` lämnas tillbaka).'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '422':
          $ref: '#/components/responses/UnprocessableContent'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: gränsen `gif`, en renderingsgräns, eller kontobudgeten.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`render_failed`: GIF:en kunde inte skapas (servern loggar varför). `internal_error`.'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` med `retryAfter` (3 till 10 sekunder) och `Retry-After`: renderingskön är full, eller GIF:en väntade `GIF_QUEUE_TIMEOUT_MS` (10 sekunder) på en ledig tråd; varje gränstoken som anropet tog lämnas tillbaka. `busy`: databasen förblev låst (`retryAfter: 1` endast i bodyn). `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /gif:
    post:
      operationId: renderPgnGif
      tags:
        - gifs
      summary: Skapa en animerad GIF av valfritt parti som skickas som PGN
      description: |-
        Samma bild som `GET /games/{id}/gif` för valfritt parti som skickas som PGN-text: ett parti som spelet har sparat, en export från en annan webbplats, ett parti som skrivits in för hand. Endast textens första parti används.

        - PGN-läsaren godtar det som spelets egen läsare godtar: varje PGN som den här servern skriver och de vanliga exporterna från andra webbplatser (kommentarer, varianter, NAG:ar och klockannoteringar hoppas över; dragnummer och SAN läses tolerant; en `FEN`-tagg ger startställningen om inte `SetUp` är `"0"`).
        - Namnen och ratingtalen hämtas från taggarna `White`, `Black`, `WhiteElo` och `BlackElo`. Bokstäver med accent förlorar sina accenter, andra tecken utanför utskrivbar ASCII blir `?`, och långa namn kortas av (48 tecken). Resultatet hämtas från taggen `Result`, annars från slutet av dragtexten. Taggen `Termination` visas om den inte är `normal`: slutställningen berättar då vad som hände (schackmatt, patt).
        - Den här endpointen kontrollerar själv sin body: ett okänt fält eller ett `pgn` som saknas eller inte är en sträng ger 400 `invalid_request` med `field`; ett felaktigt alternativ ger 400 `invalid_option` (`delayMs` måste vara ett JSON-tal, `coords` ett booleskt värde; `null` avvisas).

        **Session krävs:** kvoterna räknas per konto. **Gränser:** som för `GET /games/{id}/gif`. **Gräns för bodyn:** 135 168 byte, oavsett `HTTP_BODY_LIMIT` (PGN-texten som JSON-sträng, inklusive dess escape-sekvenser). **Tidsgräns:** 45 sekunder som standard.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GifRequest'
            examples:
              short:
                summary: Ett kort parti inskrivet för hand, utan koordinater
                value:
                  pgn: 1. f3 e5 2. g4 Qh4# 0-1
                  coords: false
              options:
                summary: En PGN-fil med alla alternativ
                value:
                  pgn: |
                    [White "alice"]
                    [Black "bob"]
                    [WhiteElo "1500"]
                    [BlackElo "1520"]
                    [Result "1-0"]

                    1. e4 e5 2. Bc4 Nc6 3. Qh5 Nf6 4. Qxf7# 1-0
                  size: small
                  orientation: black
                  delayMs: 800
                  coords: true
      responses:
        '200':
          $ref: '#/components/responses/GifFile'
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_request` med `field` (ett okänt fält, eller `pgn` saknas eller är inte en sträng; utan `field` när bodyn inte är ett objekt); `invalid_json`; `invalid_option` med `field` (`size`, `orientation`, `delayMs` eller `coords`); `invalid_pgn` med `line` och `column` (från 1, kolumner räknade i tecken) och läsarens `message`: ett otillåtet eller tvetydigt drag, en trasig tagg, en okänd variant, mer än 65 536 byte.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`gif_disabled`: servern har stängt av GIF:ar (`GIF_ENABLED=false`; tokenen för `gif` lämnas tillbaka).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
          description: '`payload_too_large`: en body större än 135 168 byte.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '422':
          $ref: '#/components/responses/UnprocessableContent'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: gränsen `gif`, en renderingsgräns, eller kontobudgeten.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`render_failed`: GIF:en kunde inte skapas (servern loggar varför). `internal_error`.'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` med `retryAfter` (3 till 10 sekunder) och `Retry-After`: renderingskön är full, eller GIF:en väntade för länge på en ledig tråd; varje gränstoken som anropet tog lämnas tillbaka. `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /players/{username}:
    get:
      operationId: getPlayer
      tags:
        - players
      summary: Hämta en spelares offentliga profil
      description: |-
        En spelares offentliga profil: ratingtal i de officiella kategorierna (i serverns ordning) och antal partier. `games.total` räknar alla sparade partier, även partier utan rating och avbrutna partier; `games.rated`, `wins`, `draws` och `losses` summeras över ratingposterna, alltså endast partier med rating. Svaret är detsamma med eller utan token.

        **Gräns:** `public_read` (per spelare med token, per klient utan).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Username'
      responses:
        '200':
          description: Profilen.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlayerProfile'
              example:
                username: alice
                createdAt: 1790882839743
                ratings:
                  - category: '3+2'
                    rating: 1510
                    provisional: true
                    games: 2
                    wins: 1
                    draws: 1
                    losses: 0
                    peak: 1510
                games:
                  total: 3
                  rated: 2
                  wins: 1
                  draws: 1
                  losses: 0
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_username`: inte 2 till 24 tecken ur `[A-Za-z0-9_.-]`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: en token skickades och den är inte giltig.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: spelaren finns inte, eller ett raderat konto.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: gränsen `public_read` eller kontobudgeten.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (databasen förblev låst; `retryAfter: 1` i bodyn, ingen `Retry-After`-header), `server_busy` (sessionsuppslaget fann databasen låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /players/{username}/games:
    get:
      operationId: listPlayerGames
      tags:
        - players
      summary: Lista en spelares senaste partier
      description: |-
        Spelarens senaste partier, nyaste först, sidindelade som historiken men utan filter eller totalantal. `color` är den här spelarens sida. `next` är det sista partiets id när sidan är full, så nästa sida kan vara tom. Spelaren slås upp först: en okänd spelare ger 404 oavsett vad frågesträngen innehåller.

        **Gräns:** `public_read` (per spelare med token, per klient utan).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Username'
        - $ref: '#/components/parameters/BeforeQuery'
        - $ref: '#/components/parameters/LimitQuery'
      responses:
        '200':
          description: En sida av spelarens partier.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlayerGamesPage'
              example:
                username: alice
                games:
                  - id: 4100000000001
                    category: '3+2'
                    rated: true
                    timeControl: '180+2'
                    white:
                      name: alice
                      rating: 1500
                      ratingAfter: 1510
                      ratingDiff: 10
                    black:
                      name: bob
                      rating: 1520
                      ratingAfter: 1510
                      ratingDiff: -10
                    color: white
                    status: 1
                    reason: 2
                    result: 1-0
                    termination: Resignation
                    plies: 41
                    startedAt: 1790620205000
                    endedAt: 1790620611000
                next: 4100000000001
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_username`, `invalid_cursor` (`before` är inget parti-id), `invalid_limit` (de två sistnämnda utan `field`).'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: en token skickades och den är inte giltig.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: spelaren finns inte, eller ett raderat konto.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: gränsen `public_read` eller kontobudgeten.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (databasen förblev låst; `retryAfter: 1` i bodyn, ingen `Retry-After`-header), `server_busy` (sessionsuppslaget fann databasen låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /leaderboard:
    get:
      operationId: getLeaderboard
      tags:
        - leaderboard
      summary: Hämta de bästa spelarna i en kategori
      description: |-
        De 100 bästa ratingposterna i en officiell kategori med minst `minGames` (`PROVISIONAL_GAMES`) räknade partier, utan raderade konton och bekräftade fuskare. Servern beräknar varje lista på nytt högst var 10:e sekund; `updatedAt` anger när. En beräkning per kategori körs åt gången: under tiden får de andra anropen den föregående listan, eller väntar på beräkningen när det ännu inte finns någon.

        **Gränser:** endast lagret per adress.
      security: []
      parameters:
        - name: category
          in: query
          required: true
          description: Ett officiellt kategori-id (`3+2`, eller `3%2B2`).
          schema:
            type: string
            pattern: '^\s*[0-9]+[+ ][0-9]+\s*$'
          example: '3+2'
        - name: limit
          in: query
          required: false
          description: Hur många spelare, 1 till 100. Ett större tal med upp till 3 siffror räknas som 100; ett tomt värde räknas som utelämnat.
          schema:
            type: integer
            minimum: 1
            maximum: 999
            default: 100
      responses:
        '200':
          description: Listan.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Leaderboard'
              example:
                category: '3+2'
                minGames: 30
                updatedAt: 1790882839828
                players:
                  - rank: 1
                    username: bob
                    rating: 1874
                    games: 212
                    wins: 120
                    draws: 30
                    losses: 62
                    peak: 1901
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_category` (saknas, eller är ingen officiell kategori), `invalid_limit`.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: lagret per adress.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (databasen förblev låst; `retryAfter: 1` i bodyn, ingen `Retry-After`-header), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /reports:
    post:
      operationId: reportPlayer
      tags:
        - reports
      summary: Anmäl motståndaren i ett nyligen spelat parti
      description: |-
        Anmäler motståndaren i ett av spelarens egna partier som avslutades under de senaste 7 dagarna. En anmälan ändrar aldrig i sig en rating, en sanktion eller en integritetsnivå. Den höjer den granskningsprioritet som moderatorerna ser, och, utom för `abuse`, begär den en motoranalys av partiet.

        En anmälan av samma motståndare för samma parti får samma svar och ändrar ingenting; svaret avslöjar aldrig något om det anmälda kontot. `GET /games/{id}` talar i förväg om för partiets spelare om en anmälan skulle tas emot (`reportable`).

        Den här endpointen kontrollerar själv sin body, i ordningen `gameId`, `reported`, `category`, `comment`: ett fel ger 400 `invalid_request` utan `field`. Fält som den inte känner till ignoreras.

        **Gränser:** `reports` (per spelare), sedan `REPORTS_PER_DAY` (5) anmälningar per spelare under 24 timmar (429 `report_limit`).
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReportRequest'
            example:
              gameId: 4100000000001
              reported: bob
              category: cheating
              comment: engine-like play
      responses:
        '202':
          description: Anmälan är mottagen (eller var redan inlämnad).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    const: received
              example:
                status: received
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_request` (utan `field`), `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`report_not_allowed`: inte anmälarens motståndare i ett parti som avslutades under de senaste 7 dagarna.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`report_limit` (`retryAfter: 3600`, med en `Retry-After`-header): `REPORTS_PER_DAY` anmälningar under de senaste 24 timmarna. `rate_limited`: gränsen `reports` eller kontobudgeten.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (sessionsuppslaget fann databasen låst), `timeout`. Även `server_busy` med `retryAfter` 2 till 5 när servern ligger långt efter (se inledningen): anropet kördes inte.'
  /verify-email:
    servers:
      - url: https://caissa.scacelith.com
        description: Den officiella servern (sidorna ligger i roten, utanför `/api/v1`).
      - url: https://{host}:{port}
        description: Valfri Scacelith-server (sidorna ligger i roten, utanför `/api/v1`).
        variables:
          host:
            default: caissa.scacelith.com
            description: Serverns offentliga värdnamn (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Den offentliga API-porten (`PUBLIC_API_PORT`, annars `API_PORT`).
    get:
      operationId: showVerifyEmailPage
      tags:
        - pages
      summary: Visa sidan för e-postbekräftelse
      description: |-
        Sidan för en länk som bekräftar e-postadressen (registrering, eller ett bekräftelsemeddelande som skickats på nytt). Den visar bara knappen ”Confirm my e-mail address” (Bekräfta min e-postadress), så att en e-postskanner som öppnar länken inte förbrukar den; knappen skickar formuläret till `POST /verify-email`.

        **Gräns:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Sidan med dess bekräftelseknapp.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Länken är ogiltig eller har gått ut (en HTML-sida).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Gränsen `page` (en HTML-sida), eller lagret per adress (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitVerifyEmailPage
      tags:
        - pages
      summary: Bekräfta e-postadressen
      description: |-
        Formuläret på bekräftelsesidan. Det bekräftar adressen; vid en ny registrering skapar det då kontot, och spelaren kan logga in.

        **Gräns:** `auth`.
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/LinkTokenForm'
            example:
              token: asfioO4P0r_Zn5DxWzcQuF6Drgxu7wXGjCyS-UFbTaw
          application/json:
            schema:
              $ref: '#/components/schemas/LinkTokenForm'
            example:
              token: asfioO4P0r_Zn5DxWzcQuF6Drgxu7wXGjCyS-UFbTaw
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Adressen är bekräftad (och kontot för en ny registrering skapat).
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Länken är ogiltig, använd eller har gått ut, eller formuläret är ogiltigt (en HTML-sida).
        '408':
          $ref: '#/components/responses/HtmlRequestTimeout'
        '409':
          $ref: '#/components/responses/HtmlConflict'
          description: Länken för en ny registrering vars användarnamn eller adress ett annat konto har tagit under tiden; inget konto skapas (en HTML-sida).
        '413':
          $ref: '#/components/responses/HtmlPayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/HtmlUnsupportedMediaType'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Gränsen `auth` (en HTML-sida), eller lagret per adress (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Databasen förblev låst (`Retry-After: 1`), eller servern ligger långt efter (`Retry-After` 2 till 5, se inledningen): inget ändrades och länken fungerar fortfarande. Även när hanterarens tidsgräns överskrids.'
  /reset-password:
    servers:
      - url: https://caissa.scacelith.com
        description: Den officiella servern (sidorna ligger i roten, utanför `/api/v1`).
      - url: https://{host}:{port}
        description: Valfri Scacelith-server (sidorna ligger i roten, utanför `/api/v1`).
        variables:
          host:
            default: caissa.scacelith.com
            description: Serverns offentliga värdnamn (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Den offentliga API-porten (`PUBLIC_API_PORT`, annars `API_PORT`).
    get:
      operationId: showResetPasswordPage
      tags:
        - pages
      summary: Visa formuläret för återställning av lösenordet
      description: |-
        Sidan för en återställningslänk: formuläret för nytt lösenord (lösenordet två gånger). Formuläret skickas till `POST /reset-password`.

        **Gräns:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Formuläret för nytt lösenord.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Länken är ogiltig (även när den skickades till en adress som kontot inte längre har).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Gränsen `page` (en HTML-sida), eller lagret per adress (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitResetPasswordPage
      tags:
        - pages
      summary: Ange ett nytt lösenord via återställningsformuläret
      description: |-
        Formuläret på återställningssidan; det gör det som `POST /auth/password/reset` gör, i en enda transaktion: alla enheter loggas ut, ett väntande byte av e-postadress avbryts, de andra återställningslänkarna slutar fungera, adressen räknas som bekräftad och ägaren får ett e-postmeddelande.

        Länken kontrolleras först, sedan att båda lösenorden är likadana, sedan lösenordsreglerna. När servern är upptagen kommer formuläret tillbaka med `Retry-After` och länken förblir giltig.

        **Gränser:** `auth`, sedan `auth_reset` (delad med `POST /auth/password/reset`).
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ResetPasswordForm'
            example:
              token: A0ATfmXCDKFC8UkSxLJCD85K7au5nMIrZ2P3K-1VWQg
              newPassword: a much better passphrase
              confirmPassword: a much better passphrase
          application/json:
            schema:
              $ref: '#/components/schemas/ResetPasswordForm'
            example:
              token: A0ATfmXCDKFC8UkSxLJCD85K7au5nMIrZ2P3K-1VWQg
              newPassword: a much better passphrase
              confirmPassword: a much better passphrase
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Lösenordet är bytt, och alla enheter är utloggade.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Formuläret igen med felet (lösenorden skiljer sig åt, ett svagt lösenord), länken är ogiltig, eller formuläret är ogiltigt (en HTML-sida).
        '408':
          $ref: '#/components/responses/HtmlRequestTimeout'
        '413':
          $ref: '#/components/responses/HtmlPayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/HtmlUnsupportedMediaType'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Gränsen `auth` eller `auth_reset` (en HTML-sida), formuläret igen med `Retry-After` när den här klienten har för många lösenordshashningar i kö (länken förblir giltig), eller lagret per adress (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Formuläret igen med `Retry-After` när servern är upptagen (kön för lösenordshashning, eller databasen förblev låst); länken förblir giltig. När servern ligger långt efter (se inledningen) i stället en sida ”Server error” (serverfel) med `Retry-After` (2 till 5): inget kördes och länken förblir giltig. Även när hanterarens tidsgräns överskrids.'
  /confirm-email-change:
    servers:
      - url: https://caissa.scacelith.com
        description: Den officiella servern (sidorna ligger i roten, utanför `/api/v1`).
      - url: https://{host}:{port}
        description: Valfri Scacelith-server (sidorna ligger i roten, utanför `/api/v1`).
        variables:
          host:
            default: caissa.scacelith.com
            description: Serverns offentliga värdnamn (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Den offentliga API-porten (`PUBLIC_API_PORT`, annars `API_PORT`).
    get:
      operationId: showConfirmEmailChangePage
      tags:
        - pages
      summary: Visa bekräftelsesidan för bytet av e-postadress
      description: |-
        Sidan för en länk för byte av e-postadress: den visar den nya adressen och kontots namn, med knappen ”Use this e-mail address” (Använd den här e-postadressen) som skickar formuläret till `POST /confirm-email-change`.

        **Gräns:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Sidan med den nya adressen och dess bekräftelseknapp.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Länken är ogiltig eller har gått ut (även när kontots adress har ändrats sedan bytet begärdes).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Gränsen `page` (en HTML-sida), eller lagret per adress (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitConfirmEmailChangePage
      tags:
        - pages
      summary: Bekräfta den nya e-postadressen
      description: |-
        Formuläret på sidan för byte av e-postadress. Adressen ändras och räknas som bekräftad; enheterna förblir inloggade; de länkar som skickats tidigare (bekräftelse, återställning av lösenordet, andra byten) slutar fungera; den tidigare adressen underrättas, med den nya maskerad.

        **Gräns:** `auth`.
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/LinkTokenForm'
            example:
              token: lZ50AvPxUwbGqQCwWPIO28ONray7SzW0Cl_sLImwF3Q
          application/json:
            schema:
              $ref: '#/components/schemas/LinkTokenForm'
            example:
              token: lZ50AvPxUwbGqQCwWPIO28ONray7SzW0Cl_sLImwF3Q
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Adressen är ändrad.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Länken är ogiltig, använd eller har gått ut, eller formuläret är ogiltigt (en HTML-sida).
        '408':
          $ref: '#/components/responses/HtmlRequestTimeout'
        '409':
          $ref: '#/components/responses/HtmlConflict'
          description: Ett annat konto har tagit adressen under tiden (en HTML-sida).
        '413':
          $ref: '#/components/responses/HtmlPayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/HtmlUnsupportedMediaType'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Gränsen `auth` (en HTML-sida), eller lagret per adress (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Databasen förblev låst (`Retry-After: 1`), eller servern ligger långt efter (`Retry-After` 2 till 5, se inledningen): inget ändrades och länken fungerar fortfarande. Även när hanterarens tidsgräns överskrids.'
  /healthz:
    servers:
      - url: https://caissa.scacelith.com
        description: Den officiella servern, i roten.
      - url: https://caissa.scacelith.com/api/v1
        description: Den officiella servern, under `/api/v1`.
      - url: https://{host}:{port}
        description: Valfri Scacelith-server, i roten.
        variables:
          host:
            default: caissa.scacelith.com
            description: Serverns offentliga värdnamn (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Den offentliga API-porten (`PUBLIC_API_PORT`, annars `API_PORT`).
      - url: https://{host}:{port}/api/v1
        description: Valfri Scacelith-server, under `/api/v1`.
        variables:
          host:
            default: caissa.scacelith.com
            description: Serverns offentliga värdnamn (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Den offentliga API-porten (`PUBLIC_API_PORT`, annars `API_PORT`).
    get:
      operationId: getLiveness
      tags:
        - health
      summary: Kontrollera att processen körs
      description: |-
        Svarar 200 `ok` så länge processen körs. `HEAD` fungerar också. Alla andra metoder ger 405 `method_not_allowed` med `Allow: GET, HEAD` (även `OPTIONS`).

        **Gränser:** endast lagret per adress.
      security: []
      responses:
        '200':
          description: Processen körs.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    const: ok
              example:
                status: ok
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: lagret per adress.'
  /readyz:
    servers:
      - url: https://caissa.scacelith.com
        description: Den officiella servern, i roten.
      - url: https://caissa.scacelith.com/api/v1
        description: Den officiella servern, under `/api/v1`.
      - url: https://{host}:{port}
        description: Valfri Scacelith-server, i roten.
        variables:
          host:
            default: caissa.scacelith.com
            description: Serverns offentliga värdnamn (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Den offentliga API-porten (`PUBLIC_API_PORT`, annars `API_PORT`).
      - url: https://{host}:{port}/api/v1
        description: Valfri Scacelith-server, under `/api/v1`.
        variables:
          host:
            default: caissa.scacelith.com
            description: Serverns offentliga värdnamn (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Den offentliga API-porten (`PUBLIC_API_PORT`, annars `API_PORT`).
    get:
      operationId: getReadiness
      tags:
        - health
      summary: Kontrollera att servern tar emot spelare
      description: |-
        Svarar 200 `ready` när servern tar emot spelare, och 503 `not_ready` medan den startar eller stängs av. `HEAD` fungerar också. Alla andra metoder ger 405 `method_not_allowed` med `Allow: GET, HEAD` (även `OPTIONS`).

        **Gränser:** endast lagret per adress.
      security: []
      responses:
        '200':
          description: Servern tar emot spelare.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    const: ready
              example:
                status: ready
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: lagret per adress.'
        '503':
          description: Servern startar eller stängs av.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    const: not_ready
              example:
                status: not_ready
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sct_[A-Za-z0-9_-]{43}
      description: |-
        En sessionstoken (`sct_` följt av 43 base64url-tecken) från ett svar på en inloggning, i headern `Authorization`: `Authorization: Bearer <token>`. Prefixet är exakt `Bearer` (skiftlägeskänsligt) och ett mellanslag; ett värde som saknar det, eller en token som inte består av 1 till 512 utskrivbara ASCII-tecken, ger 401 `invalid_token` som alla andra ogiltiga token. En tom header räknas som ingen header.
  parameters:
    GameId:
      name: id
      in: path
      required: true
      description: Partiets id, ett positivt decimalt heltal med högst 16 siffror utan inledande nollor, under 2^53.
      schema:
        type: integer
        format: int64
        minimum: 1
        maximum: 9007199254740991
      example: 4100000000001
    SessionId:
      name: id
      in: path
      required: true
      description: Sessionens id, så som `GET /auth/sessions` anger det, skrivet utan inledande nollor (`03` är inte session 3).
      schema:
        type: integer
        format: int64
        minimum: 1
      example: 3
    Username:
      name: username
      in: path
      required: true
      description: Spelarens användarnamn, utan hänsyn till skiftläge. Servern godtar alla namn som består av 2 till 24 tecken ur `[A-Za-z0-9_.-]` (den vidaste regel den någonsin har tillåtit).
      schema:
        type: string
        pattern: '^[A-Za-z0-9_.-]{2,24}$'
      example: alice
    BeforeQuery:
      name: before
      in: query
      required: false
      description: Ett parti-id; endast äldre partier listas. Skicka föregående sidas `next`. Ett tomt värde räknas som utelämnat.
      schema:
        type: integer
        format: int64
        minimum: 1
        maximum: 9007199254740991
      example: 4100000000002
    LimitQuery:
      name: limit
      in: query
      required: false
      description: Sidstorleken, 1 till 50. Ett större tal med upp till 3 siffror räknas som 50; ett tomt värde räknas som utelämnat.
      schema:
        type: integer
        minimum: 1
        maximum: 999
        default: 20
    LinkTokenQuery:
      name: token
      in: query
      required: false
      description: E-postlänkens token (43 base64url-tecken). Om den saknas eller är fel visas sidan ”link invalid or expired” (länken är ogiltig eller har gått ut).
      schema:
        type: string
        pattern: '^[A-Za-z0-9_-]{43}$'
      example: asfioO4P0r_Zn5DxWzcQuF6Drgxu7wXGjCyS-UFbTaw
  headers:
    CacheControl:
      description: Alla svar från servern förbjuder cachning.
      schema:
        type: string
        const: no-store
    RetryAfter:
      description: Sekunder att vänta innan nästa försök; samma värde som `retryAfter` i bodyn.
      schema:
        type: integer
        minimum: 1
      example: 30
    WwwAuthenticate:
      description: Bearer-challengen, med `error="invalid_token"` för en ogiltig token.
      schema:
        type: string
        enum:
          - Bearer realm="scacelith"
          - Bearer realm="scacelith", error="invalid_token"
    ConnectionClose:
      description: Servern stänger anslutningen efter det här svaret.
      schema:
        type: string
        const: close
  responses:
    BadRequest:
      description: '`invalid_request` (med `field` när ett fält i bodyn är felaktigt) eller `invalid_json`.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalidRequest:
              summary: Ett fält bryter mot schemat
              value:
                error: invalid_request
                message: '"password" is required'
                field: password
            invalidJson:
              summary: Bodyn är inte JSON
              value:
                error: invalid_json
                message: The body is not valid JSON.
            weakPassword:
              summary: Ett svagt lösenord
              value:
                error: weak_password
                message: The password must have at least 10 characters.
                reason: too_short
            invalidPgn:
              summary: En PGN som inte kan läsas
              value:
                error: invalid_pgn
                message: illegal move 'Ke3'
                line: 1
                column: 13
    Unauthorized:
      description: '`unauthorized` (ingen `Authorization`-header) eller `invalid_token` (tokenen är felformaterad, har gått ut, har återkallats eller tillhör ett raderat konto).'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        WWW-Authenticate:
          $ref: '#/components/headers/WwwAuthenticate'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            unauthorized:
              summary: Ingen session
              value:
                error: unauthorized
                message: Log in first.
            invalidToken:
              summary: En ogiltig token
              value:
                error: invalid_token
                message: The session is invalid or has expired; log in again.
    Forbidden:
      description: Anropet avvisas.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalidPassword:
              summary: Fel lösenord vid förnyad autentisering
              value:
                error: invalid_password
                message: Wrong password.
            banned:
              summary: Ett avstängt konto
              value:
                error: banned
                message: This account is banned.
                until: 1791487639708
    NotFound:
      description: '`not_found`, eller en funktion som den här servern har stängt av.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: not_found
            message: No such game.
    RequestTimeout:
      description: '`request_timeout`: bodyn kom inte fram inom 10 sekunder. Servern stänger anslutningen.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Connection:
          $ref: '#/components/headers/ConnectionClose'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: request_timeout
            message: The request body took too long.
    Conflict:
      description: Anropet står i konflikt med serverns tillstånd.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: username_taken
            message: This username is already taken.
    Gone:
      description: '`sso_expired`: försöket till Google-inloggning eller biljetten är okänd, redan använd eller har gått ut; börja om inloggningen.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: sso_expired
            message: This sign-in has expired; start again from Scacelith.
    PayloadTooLarge:
      description: '`payload_too_large`: bodyn överskrider `HTTP_BODY_LIMIT` (16 384 byte som standard). Servern stänger anslutningen.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Connection:
          $ref: '#/components/headers/ConnectionClose'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: payload_too_large
            message: The body must not exceed 16384 bytes.
    UriTooLong:
      description: '`uri_too_long`: anropsmålet (request target) överskrider 4096 tecken.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: uri_too_long
            message: The URL is too long.
    UnsupportedMediaType:
      description: '`unsupported_media_type`: bodyn är inte `application/json`, eller dess teckenkodning är inte UTF-8.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: unsupported_media_type
            message: Content-Type must be application/json.
    UnprocessableContent:
      description: '`game_too_long`: partiet har fler än `GIF_MAX_PLIES` (600) halvdrag.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: game_too_long
            message: The game is too long for a GIF (742 plies, at most 600).
    PowRequired:
      description: '`pow_required`: lös challengen i `pow` och skicka samma anrop igen med `pow: { challenge, nonce }`. `reason` anger varför: `required`, `malformed`, `signature`, `endpoint`, `network`, `expired`, `bits`, `work` eller `replayed`.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: pow_required
            message: Proof of work required.
            reason: required
            pow:
              challenge: eyJ2IjoxLCJlIjoicmVnaXN0ZXIiLCJiIjoxOCwieCI6MTc5MDg4Mjk5MTIwMH0.6Ku_D7kw3S69OYxj-9KxsXszRCwz6ECnOQ-B6-v2erM
              bits: 18
              expiresAt: 1790882991200
    TooManyRequests:
      description: '`rate_limited` (en anropsgräns, kontobudgeten, eller kön för lösenordshashning) eller `too_many_attempts` (en strypning av kontot), med `retryAfter` och en `Retry-After`-header.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            rateLimited:
              summary: En anropsgräns
              value:
                error: rate_limited
                message: Too many requests; try again later.
                retryAfter: 30
            tooManyAttempts:
              summary: För många misslyckanden på det här kontot
              value:
                error: too_many_attempts
                message: Too many attempts; wait before trying again.
                retryAfter: 4
    InternalError:
      description: '`internal_error`: ett oväntat fel. Servern loggar det.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: internal_error
            message: Internal server error.
    BadGateway:
      description: '`sso_failed`: fel `state` eller `iss`, eller så avvisade Google koden eller skickade en ID-token som inte går att verifiera (även en kod eller ID-token från den andra klienten, spelets eller webbplatsens). Felet innehåller aldrig Googles text.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: sso_failed
            message: Google sign-in could not be completed.
    ServiceUnavailable:
      description: '`server_busy`, `busy` eller `timeout`: försök igen efter `retryAfter` när det anges.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            serverBusy:
              summary: Servern är upptagen
              value:
                error: server_busy
                message: The server is busy; try again in a few seconds.
                retryAfter: 9
            busy:
              summary: Databasen förblev låst (en läsning)
              value:
                error: busy
                message: Try again shortly.
                retryAfter: 1
            timeout:
              summary: Servern tog för lång tid
              value:
                error: timeout
                message: The server took too long to answer; try again.
    GifFile:
      description: Den animerade GIF:en, som bilaga.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Content-Disposition:
          description: '`attachment; filename="scacelith-<id>.gif"` för ett parti från den här servern, `attachment; filename="scacelith-game.gif"` för en PGN som skickats med `POST /gif`.'
          schema:
            type: string
            pattern: '^attachment; filename="scacelith-([1-9][0-9]{0,15}|game)\.gif"$'
          example: attachment; filename="scacelith-4100000000001.gif"
        Content-Length:
          description: Filens storlek i byte.
          schema:
            type: integer
            minimum: 1
      content:
        image/gif:
          schema:
            type: string
            format: binary
    HtmlPage:
      description: En HTML-sida.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlBadRequest:
      description: En HTML-sida som förklarar avvisningen.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlRequestTimeout:
      description: Formuläret kom inte fram inom 10 sekunder (en HTML-sida). Servern stänger anslutningen.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Connection:
          $ref: '#/components/headers/ConnectionClose'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlConflict:
      description: En HTML-sida som förklarar konflikten.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlPayloadTooLarge:
      description: Formuläret överskrider `HTTP_BODY_LIMIT` (en HTML-sida). Servern stänger anslutningen.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Connection:
          $ref: '#/components/headers/ConnectionClose'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlUnsupportedMediaType:
      description: Bodyn är varken ett formulär (`application/x-www-form-urlencoded`) eller JSON, eller så är dess teckenkodning inte UTF-8 (en HTML-sida).
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlTooManyRequests:
      description: En anropsgräns (en HTML-sida), med `Retry-After`.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlInternalError:
      description: Ett oväntat fel (en HTML-sida med rubriken ”Server error”, serverfel). Servern loggar det.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlServiceUnavailable:
      description: Servern är upptagen (databasen förblev låst, eller servern ligger långt efter; med `Retry-After`) eller tog för lång tid (en HTML-sida med rubriken ”Server error”, serverfel).
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
  schemas:
    Error:
      type: object
      description: Den enda formen för alla felsvar i JSON-API:t.
      required:
        - error
        - message
      properties:
        error:
          type: string
          pattern: '^[a-z][a-z0-9_]*$'
          description: Felkoden i `snake_case`. En klient väljer utifrån den vad som ska visas.
          examples:
            - rate_limited
        message:
          type: string
          description: En mening på engelska, avsedd för loggar och som reserv.
        retryAfter:
          type: integer
          minimum: 1
          description: Sekunder att vänta innan nästa försök, finns bara vid avvisningar som upphör efter en viss tid. Svaret har då också en `Retry-After`-header med samma värde, utom vid 503 `busy` från läsningar av historiken, partier, spelare och topplistan.
        field:
          type: string
          description: Det felaktiga fältet eller den felaktiga query-parametern (`invalid_request`, `invalid_option`, samt `invalid_cursor`, `invalid_limit` och `invalid_filter` från `GET /account/games`). Ett nästlat fält skrivs med punkt (`pow.nonce`).
        reason:
          type: string
          description: 'Varför: den regel som ett `weak_password` bryter mot, eller varför ett proof of work begärdes eller avvisades (`pow_required`).'
          enum:
            - too_short
            - too_long
            - contains_username
            - contains_email
            - too_common
            - required
            - malformed
            - signature
            - endpoint
            - network
            - expired
            - bits
            - work
            - replayed
        pow:
          $ref: '#/components/schemas/PowChallenge'
        until:
          type:
            - integer
            - 'null'
          format: int64
          description: 'Endast `banned`: avstängningens slut (Unix-tid i ms), `null` för en permanent avstängning.'
        line:
          type: integer
          minimum: 1
          description: 'Endast `invalid_pgn`: felets rad, från 1.'
        column:
          type: integer
          minimum: 1
          description: 'Endast `invalid_pgn`: felets kolumn, från 1, i tecken.'
    PowChallenge:
      type: object
      description: En proof-of-work-challenge (`pow` i ett 428 `pow_required`).
      required:
        - challenge
        - bits
        - expiresAt
      properties:
        challenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{16,400}\.[A-Za-z0-9_-]{43}$'
          description: Den signerade challengen, att skicka tillbaka oförändrad.
        bits:
          type: integer
          minimum: 1
          maximum: 26
          description: Antalet inledande nollbitar som `SHA-256(challenge + ":" + nonce)` måste ha.
        expiresAt:
          type: integer
          format: int64
          description: När challengen går ut (Unix-tid i ms), 2 minuter efter att den utfärdades.
    PowAnswer:
      type: object
      description: Svaret på en proof-of-work-challenge.
      additionalProperties: false
      required:
        - challenge
        - nonce
      properties:
        challenge:
          type: string
          minLength: 16
          maxLength: 512
          description: '`challenge` från svaret 428, så som den angavs.'
        nonce:
          type: string
          pattern: '^[0-9]{1,20}$'
          description: En decimal sträng med högst 20 siffror sådan att `SHA-256(challenge + ":" + nonce)` börjar med `bits` nollbitar.
    ServerInfo:
      type: object
      description: Det som en klient behöver innan den loggar in eller ansluter.
      required:
        - name
        - serverId
        - motd
        - protocol
        - wsPort
        - wsPath
        - registration
        - emailVerification
        - sso
        - mfa
        - pow
        - categories
        - limits
      properties:
        name:
          type: string
          description: Serverns namn (`SERVER_NAME`).
        serverId:
          type:
            - string
            - 'null'
          format: uuid
          description: Ett UUID som databasen får vid sin första start. Det förblir detsamma över omstarter. Det är `null` när databasen inte kan ge det.
        motd:
          type: string
          description: Dagens meddelande (`SERVER_MOTD`), eventuellt tomt.
        protocol:
          type: object
          description: WebSocket-protokollet (`PROTOCOL.md`).
          required:
            - min
            - max
            - schema
            - subprotocol
          properties:
            min:
              type: integer
              minimum: 1
              description: Den äldsta protokollversion som servern talar.
            max:
              type: integer
              minimum: 1
              description: Den nyaste protokollversion som servern talar.
            schema:
              type: integer
              format: int64
              minimum: 0
              maximum: 4294967295
              description: 'Protokollschemats fingeravtryck (de första 4 byten av SHA-256 för dess kanoniska form, som ett heltal utan tecken: `0xc649140f`, 3326678031, för protokollets minor-version 2); informativt, jämförs aldrig.'
            subprotocol:
              type: string
              description: Token för WebSocket-subprotokollet (`Sec-WebSocket-Protocol`).
        wsPort:
          type: integer
          minimum: 0
          maximum: 65535
          description: Den WebSocket-port som spelarna använder (`PUBLIC_WS_PORT`, annars `WS_PORT`, annars `API_PORT`).
        wsPath:
          type: string
          const: /ws
          description: WebSocketens sökväg.
        registration:
          type: string
          enum:
            - open
            - closed
          description: Om nya konton kan skapas.
        emailVerification:
          type: boolean
          description: Om nya konton bekräftar sin adress (`REQUIRE_EMAIL_VERIFICATION`).
        sso:
          type: object
          required:
            - google
            - googleWeb
          properties:
            google:
              type: boolean
              description: Om Google-inloggning erbjuds spelet.
            googleWeb:
              type: boolean
              description: Om Google-inloggning även erbjuds en webbplats (`POST /auth/sso/google/web/start`).
        mfa:
          type: boolean
          const: true
          description: Tvåfaktorsautentisering är alltid tillgänglig.
        pow:
          type: object
          required:
            - register
          properties:
            register:
              type: integer
              minimum: 0
              maximum: 26
              description: De proof-of-work-bitar som registreringen kräver (0 för inga).
        categories:
          type: array
          description: De officiella betänketiderna för partier med rating (`RATED_CATEGORIES`), i serverns ordning. Alla andra betänketider är `custom`.
          items:
            $ref: '#/components/schemas/Category'
        limits:
          type: object
          description: De regler som en klient kan kontrollera innan den skickar ett formulär.
          required:
            - usernameMin
            - usernameMax
            - usernamePattern
            - passwordMinLength
            - passwordMaxBytes
            - customTimeControls
            - reportsPerDay
            - wsMaxMessageBytes
          properties:
            usernameMin:
              type: integer
              description: Det kortaste användarnamnet (`USERNAME_MIN`).
            usernameMax:
              type: integer
              description: Det längsta användarnamnet (`USERNAME_MAX`).
            usernamePattern:
              type: string
              description: Det reguljära uttryck som ett nytt användarnamn måste matcha.
            passwordMinLength:
              type: integer
              description: Det kortaste lösenordet, i tecken (`PASSWORD_MIN_LENGTH`).
            passwordMaxBytes:
              type: integer
              description: Det längsta lösenordet, i byte UTF-8.
            customTimeControls:
              type: boolean
              description: Om utmaningar och privata partier får använda anpassade betänketider.
            reportsPerDay:
              type: integer
              description: Hur många anmälningar en spelare får göra per 24 timmar (`REPORTS_PER_DAY`).
            wsMaxMessageBytes:
              type: integer
              description: Det största WebSocket-meddelande som en klient får skicka, i byte.
      example:
        name: Scacelith
        serverId: 07dd26af-672a-43af-a8af-34011c7e977b
        motd: ''
        protocol:
          min: 1
          max: 1
          schema: 3326678031
          subprotocol: scacelith.rt1
        wsPort: 443
        wsPath: /ws
        registration: open
        emailVerification: true
        sso:
          google: false
          googleWeb: false
        mfa: true
        pow:
          register: 18
        categories:
          - id: '1+0'
            baseSec: 60
            incSec: 0
          - id: '3+2'
            baseSec: 180
            incSec: 2
        limits:
          usernameMin: 3
          usernameMax: 20
          usernamePattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$
          passwordMinLength: 10
          passwordMaxBytes: 256
          customTimeControls: true
          reportsPerDay: 5
          wsMaxMessageBytes: 512
    Category:
      type: object
      description: En officiell betänketid.
      required:
        - id
        - baseSec
        - incSec
      properties:
        id:
          type: string
          pattern: '^[0-9]+\+[0-9]+$'
          description: Kategorins id, minuter och tillägg i sekunder (`3+2`).
        baseSec:
          type: number
          minimum: 0
          description: Grundtiden, i sekunder.
        incSec:
          type: number
          minimum: 0
          description: Tillägget per drag, i sekunder.
    AccountView:
      type: object
      description: Kontot som dess spelare ser det (`user` i svaren på inloggning och i `GET /account/me`).
      required:
        - id
        - username
        - email
        - emailVerified
        - mfaEnabled
        - googleLinked
        - hasPassword
        - acceptChallenges
        - createdAt
        - lastLoginAt
        - pendingEmail
      properties:
        id:
          type: integer
          format: int64
          description: Kontots id.
        username:
          type: string
          description: Användarnamnet.
        email:
          type: string
          format: email
          description: E-postadressen.
        emailVerified:
          type: boolean
          description: Om adressen är bekräftad.
        mfaEnabled:
          type: boolean
          description: Om tvåfaktorsautentisering är på.
        googleLinked:
          type: boolean
          description: Om ett Google-konto är kopplat.
        hasPassword:
          type: boolean
          description: '`false` för ett konto som skapats via Google och som ännu inte har valt något lösenord.'
        acceptChallenges:
          type: string
          enum:
            - all
            - none
          description: Om direkta utmaningar via namn tas emot (se `PUT /account/preferences`).
        createdAt:
          type: integer
          format: int64
          description: När kontot skapades (Unix-tid i ms).
        lastLoginAt:
          type:
            - integer
            - 'null'
          format: int64
          description: Den senaste inloggningen (Unix-tid i ms), eller `null`.
        pendingEmail:
          type:
            - string
            - 'null'
          format: email
          description: Den nya adressen för ett byte av e-postadress som väntar på sin länk, eller `null`.
    SessionAnswer:
      type: object
      description: En ny session.
      required:
        - token
        - expiresAt
        - user
      properties:
        token:
          type: string
          pattern: '^sct_[A-Za-z0-9_-]{43}$'
          description: Sessionstokenen, för headern `Authorization` och för WebSocketen.
        expiresAt:
          type: integer
          format: int64
          description: Sessionens absoluta slut (Unix-tid i ms); gränsen för inaktivitet kan avsluta den tidigare.
        user:
          $ref: '#/components/schemas/AccountView'
      example:
        token: sct_L_8GDd7uzfQ3QQWtqrsWXDTsFWzRwIvJcwIGHhjWPS8
        expiresAt: 1798658839708
        user:
          id: 1
          username: alice
          email: alice@example.org
          emailVerified: true
          mfaEnabled: true
          googleLinked: false
          hasPassword: true
          acceptChallenges: all
          createdAt: 1790882839743
          lastLoginAt: 1790882839708
          pendingEmail: null
    MfaChallenge:
      type: object
      description: Andra steget i en inloggning med tvåfaktorsautentisering; fortsätt med `POST /auth/login/mfa`.
      required:
        - mfaRequired
        - mfaToken
        - expiresIn
      properties:
        mfaRequired:
          type: boolean
          const: true
        mfaToken:
          type: string
          pattern: '^mfa_[A-Za-z0-9_-]{43}$'
          description: Stegets token, för `POST /auth/login/mfa`.
        expiresIn:
          type: integer
          const: 300
          description: Sekunder som återstår för att slutföra steget.
    LoginAnswer:
      description: En session, eller andra steget i en inloggning med tvåfaktorsautentisering.
      oneOf:
        - $ref: '#/components/schemas/SessionAnswer'
        - $ref: '#/components/schemas/MfaChallenge'
    SsoNeedsUsername:
      type: object
      description: En första inloggning med Google; fortsätt med `POST /auth/sso/complete`.
      required:
        - needsUsername
        - ssoTicket
        - suggestedUsername
      properties:
        needsUsername:
          type: boolean
          const: true
        ssoTicket:
          type: string
          pattern: '^sso_[A-Za-z0-9_-]{43}$'
          description: Biljetten för `POST /auth/sso/complete`, giltig i 10 minuter.
        suggestedUsername:
          type: string
          description: Ett användarnamn som bildats av Google-namnet eller adressen, eller `""` när inget passar.
    SsoNeedsPassword:
      type: object
      description: Ett konto med lösenord använder adressen; fortsätt med `POST /auth/sso/google/link`. Inget är kopplat än.
      required:
        - needsPassword
        - linkTicket
        - username
        - expiresIn
      properties:
        needsPassword:
          type: boolean
          const: true
        linkTicket:
          type: string
          pattern: '^sso_[A-Za-z0-9_-]{43}$'
          description: Biljetten för `POST /auth/sso/google/link`.
        username:
          type: string
          description: Användarnamnet för det kontot (endast den som har bevisat adressen för Google ser det).
        expiresIn:
          type: integer
          const: 600
          description: Antal sekunder som biljetten förblir giltig.
    SsoFinishAnswer:
      description: Svaret från `POST /auth/sso/google/finish`.
      oneOf:
        - $ref: '#/components/schemas/SessionAnswer'
        - $ref: '#/components/schemas/MfaChallenge'
        - $ref: '#/components/schemas/SsoNeedsUsername'
        - $ref: '#/components/schemas/SsoNeedsPassword'
    SsoStartAnswer:
      type: object
      description: Ett försök till Google-inloggning.
      required:
        - attemptId
        - authUrl
        - state
        - expiresIn
      properties:
        attemptId:
          type: string
          pattern: '^sso_[A-Za-z0-9_-]{43}$'
          description: Försöket, för `POST /auth/sso/google/finish`.
        authUrl:
          type: string
          format: uri
          description: Google-URL:en som ska öppnas i systemets webbläsare, efter kontroll.
        state:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: Den `state` som Google måste skicka tillbaka.
        expiresIn:
          type: integer
          const: 600
          description: Antal sekunder som försöket förblir giltigt.
    VerificationSentStatus:
      type: object
      required:
        - status
      properties:
        status:
          const: verification_sent
    AcceptedStatus:
      type: object
      required:
        - status
      properties:
        status:
          const: accepted
    LoggedOutStatus:
      type: object
      required:
        - status
      properties:
        status:
          const: logged_out
    RegisterRequest:
      type: object
      additionalProperties: false
      required:
        - username
        - email
        - password
      properties:
        username:
          type: string
          minLength: 1
          maxLength: 64
          description: Användarnamnet; därefter gäller serverns regler (se beskrivningen).
        email:
          type: string
          minLength: 1
          maxLength: 254
          description: E-postadressen. Trimmas och sparas med gemener.
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Lösenordet; därefter gäller lösenordsreglerna.
        pow:
          $ref: '#/components/schemas/PowAnswer'
    LoginRequest:
      type: object
      additionalProperties: false
      required:
        - login
        - password
      properties:
        login:
          type: string
          minLength: 1
          maxLength: 254
          description: Användarnamnet, eller e-postadressen (all text med `@`).
        password:
          type: string
          minLength: 1
          maxLength: 1024
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
        pow:
          $ref: '#/components/schemas/PowAnswer'
    ClientLabel:
      type: string
      maxLength: 64
      description: Valfritt. Visas i listan över inloggade enheter (till exempel `Scacelith 1.4 (Windows)`).
    MfaLoginRequest:
      type: object
      additionalProperties: false
      required:
        - mfaToken
      properties:
        mfaToken:
          type: string
          minLength: 1
          maxLength: 64
          description: '`mfaToken` från svaret på inloggningen (eller från svaret på en Google-inloggning).'
        code:
          type: string
          maxLength: 32
          description: Valfritt. En 6-siffrig kod från autentiseringsappen, eller en återställningskod.
        recoveryCode:
          type: string
          maxLength: 32
          description: Valfritt. En återställningskod. En av `code` och `recoveryCode` krävs.
    EmailRequest:
      type: object
      additionalProperties: false
      required:
        - email
      properties:
        email:
          type: string
          minLength: 1
          maxLength: 254
          description: E-postadressen.
    PasswordResetRequest:
      type: object
      additionalProperties: false
      required:
        - token
        - newPassword
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: Parametern `token` i återställningslänken.
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Det nya lösenordet; registreringens lösenordsregler gäller.
    SsoStartRequest:
      type: object
      additionalProperties: false
      required:
        - codeChallenge
        - redirectPort
      properties:
        codeChallenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: S256-PKCE-challengen för klientens `codeVerifier` (`BASE64URL(SHA-256(codeVerifier))`).
        redirectPort:
          type: integer
          minimum: 1024
          maximum: 65535
          description: Porten för klientens lyssnare på `127.0.0.1`.
    SsoWebStartRequest:
      type: object
      additionalProperties: false
      required:
        - codeChallenge
      properties:
        codeChallenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: S256-PKCE-challengen för sidans `codeVerifier` (`BASE64URL(SHA-256(codeVerifier))`).
    SsoFinishRequest:
      type: object
      additionalProperties: false
      required:
        - attemptId
        - codeVerifier
        - state
        - code
      properties:
        attemptId:
          type: string
          minLength: 1
          maxLength: 64
          description: '`attemptId` från `POST /auth/sso/google/start`.'
        codeVerifier:
          type: string
          pattern: '^[A-Za-z0-9._~-]{43,128}$'
          description: PKCE-verifiern; dess SHA-256 måste stämma med den challenge som gavs till `start`.
        state:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: '`state` så som Google skickade tillbaka den.'
        code:
          type: string
          minLength: 1
          maxLength: 2048
          pattern: '^[\x21-\x7E]+$'
          description: '`code` så som Google skickade tillbaka den (utskrivbar ASCII utan mellanslag).'
        iss:
          type: string
          minLength: 1
          maxLength: 256
          description: Valfritt. `iss` så som Google skickade tillbaka den, när Google gjorde det.
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
    SsoLinkRequest:
      type: object
      additionalProperties: false
      required:
        - linkTicket
        - password
      properties:
        linkTicket:
          type: string
          minLength: 1
          maxLength: 64
          description: '`linkTicket` från `finish`, giltig i 10 minuter.'
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Kontots lösenord.
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
        pow:
          $ref: '#/components/schemas/PowAnswer'
    SsoCompleteRequest:
      type: object
      additionalProperties: false
      required:
        - ssoTicket
        - username
      properties:
        ssoTicket:
          type: string
          minLength: 1
          maxLength: 64
          description: '`ssoTicket` från `finish`, giltig i 10 minuter.'
        username:
          type: string
          minLength: 1
          maxLength: 64
          description: Användarnamnet; registreringens regler gäller.
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
    SessionList:
      type: object
      required:
        - sessions
      properties:
        sessions:
          type: array
          description: De aktiva sessionerna, den senast använda först.
          items:
            $ref: '#/components/schemas/SessionEntry'
    SessionEntry:
      type: object
      description: En aktiv session (en inloggad enhet).
      required:
        - id
        - createdAt
        - lastSeenAt
        - expiresAt
        - clientLabel
        - current
      properties:
        id:
          type: integer
          format: int64
          description: Sessionens id, för `DELETE /auth/sessions/{id}`.
        createdAt:
          type: integer
          format: int64
          description: Inloggningen (Unix-tid i ms).
        lastSeenAt:
          type: integer
          format: int64
          description: Senaste användningen (Unix-tid i ms), uppdateras högst var 5:e minut (senare medan servern ligger långt efter).
        expiresAt:
          type: integer
          format: int64
          description: Det absoluta slutet (Unix-tid i ms); gränsen för inaktivitet kan avsluta sessionen tidigare.
        clientLabel:
          type:
            - string
            - 'null'
          description: Inloggningens `clientLabel`, eller `null` när ingen skickades.
        current:
          type: boolean
          description: Om detta är sessionen som gör anropet.
    AccountMe:
      type: object
      required:
        - user
        - ratings
        - sanctions
        - ban
      properties:
        user:
          $ref: '#/components/schemas/AccountView'
        ratings:
          type: array
          description: En post per kategori där spelaren har spelat partier med rating.
          items:
            $ref: '#/components/schemas/RatingSummary'
        sanctions:
          type: array
          description: De aktiva sanktionerna.
          items:
            $ref: '#/components/schemas/ActiveSanction'
        ban:
          description: '`{ until }` under en avstängning, annars `null`.'
          oneOf:
            - $ref: '#/components/schemas/Ban'
            - type: 'null'
    RatingSummary:
      type: object
      description: Ratingposten för en kategori.
      required:
        - category
        - rating
        - games
        - wins
        - draws
        - losses
        - peak
        - provisional
      properties:
        category:
          type: string
          description: Kategorins id.
        rating:
          type: integer
          description: Ratingen.
        games:
          type: integer
          minimum: 0
          description: De partier som spelats i kategorin (räknade eller inte).
        wins:
          type: integer
          minimum: 0
        draws:
          type: integer
          minimum: 0
        losses:
          type: integer
          minimum: 0
        peak:
          type: integer
          description: Den högsta rating som uppnåtts.
        provisional:
          type: boolean
          description: '`true` medan ratingen ännu inte är fastställd eller har färre än `PROVISIONAL_GAMES` räknade partier (spelet visar den som `1510?`).'
    ActiveSanction:
      type: object
      required:
        - kind
        - reason
        - startsAt
        - endsAt
      properties:
        kind:
          type: string
          enum:
            - ban
            - mm_block
            - warning
          description: Typen av sanktion.
        reason:
          type:
            - string
            - 'null'
          description: Det angivna skälet, om något.
        startsAt:
          type: integer
          format: int64
          description: Starten (Unix-tid i ms).
        endsAt:
          type:
            - integer
            - 'null'
          format: int64
          description: Slutet (Unix-tid i ms), `null` när sanktionen är permanent.
    Ban:
      type: object
      required:
        - until
      properties:
        until:
          type:
            - integer
            - 'null'
          format: int64
          description: Avstängningens slut (Unix-tid i ms), `null` när den är permanent.
    Preferences:
      type: object
      additionalProperties: false
      required:
        - acceptChallenges
      properties:
        acceptChallenges:
          type: string
          enum:
            - all
            - none
          description: '`all` för att ta emot direkta utmaningar via namn, `none` för att avvisa dem.'
    PasswordChangeRequest:
      type: object
      additionalProperties: false
      required:
        - currentPassword
        - newPassword
      properties:
        currentPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Det nuvarande lösenordet.
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Det nya lösenordet; registreringens lösenordsregler gäller.
    PasswordOnlyRequest:
      type: object
      additionalProperties: false
      required:
        - password
      properties:
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Kontots lösenord.
    TotpEnableRequest:
      type: object
      additionalProperties: false
      required:
        - code
      properties:
        code:
          type: string
          pattern: '^[0-9]{6}$'
          description: En kod från den väntande hemligheten, exakt 6 siffror.
    ReauthCredentials:
      type: object
      additionalProperties: false
      required:
        - password
      properties:
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Kontots lösenord.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: Valfritt; krävs med tvåfaktorsautentisering (eller `recoveryCode`). En kod från autentiseringsappen, eller en återställningskod.
        recoveryCode:
          type: string
          minLength: 1
          maxLength: 32
          description: Valfritt. En återställningskod (`xxxx-xxxx-xx`; skiftläge, mellanslag och bindestreck spelar ingen roll).
    RecoveryCodesRequest:
      type: object
      additionalProperties: false
      required:
        - password
        - code
      properties:
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Kontots lösenord.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: En kod från autentiseringsappen (en återställningskod avvisas).
    EmailChangeRequest:
      type: object
      additionalProperties: false
      required:
        - newEmail
        - password
      properties:
        newEmail:
          type: string
          minLength: 1
          maxLength: 254
          description: Den nya adressen; trimmas och görs om till gemener, och kontrolleras sedan som en adress vid registrering.
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Kontots lösenord.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: Valfritt; krävs med tvåfaktorsautentisering (eller `recoveryCode`). En kod från autentiseringsappen, eller en återställningskod.
        recoveryCode:
          type: string
          minLength: 1
          maxLength: 32
          description: Valfritt. En återställningskod.
    TotpSetup:
      type: object
      description: Den väntande hemligheten för autentiseringsappen.
      required:
        - secret
        - uri
        - algorithm
        - digits
        - period
      properties:
        secret:
          type: string
          pattern: '^[A-Z2-7]+$'
          description: Hemligheten i base32, att skriva in i appen.
        uri:
          type: string
          format: uri
          description: URI:n `otpauth://totp/...`, att visa som QR-kod.
        algorithm:
          type: string
          const: SHA1
        digits:
          type: integer
          const: 6
        period:
          type: integer
          const: 30
          description: Sekunder per steg.
    RecoveryCodeList:
      type: array
      description: De 10 återställningskoderna, som visas bara denna gång. Var och en fungerar en gång.
      minItems: 10
      maxItems: 10
      items:
        type: string
        pattern: '^[0-9a-hjkmnp-tv-z]{4}-[0-9a-hjkmnp-tv-z]{4}-[0-9a-hjkmnp-tv-z]{2}$'
    PlayerSide:
      type: object
      description: En sida i ett parti.
      required:
        - name
        - rating
        - ratingAfter
        - ratingDiff
      properties:
        name:
          type: string
          description: Namnet i partiposten (`deleted#<id>` för ett raderat konto).
        rating:
          type:
            - integer
            - 'null'
          description: Ratingen vid start, `null` när den är okänd.
        ratingAfter:
          type:
            - integer
            - 'null'
          description: Ratingen efter partiet. Ett parti med rating har den alltid; `null` endast för ett parti som inte räknas för ratingen (utan rating, anpassat, avbrutet).
        ratingDiff:
          type:
            - integer
            - 'null'
          description: Ratingförändringen, `0` när ratingreglerna lämnar ratingen oförändrad (FIDE:s nollpoängsregel, en motståndare utan fastställd rating, de första partierna för en spelare utan fastställd rating); `null` som för `ratingAfter`.
    GameSummary:
      type: object
      description: Sammanfattningen av ett sparat parti.
      required:
        - id
        - category
        - rated
        - timeControl
        - white
        - black
        - status
        - reason
        - result
        - termination
        - plies
        - startedAt
        - endedAt
      properties:
        id:
          type: integer
          format: int64
          minimum: 1
          maximum: 9007199254740991
          description: Partiets id.
        category:
          type: string
          description: Det officiella kategori-id:t (`3+2`), eller `custom`.
        rated:
          type: boolean
          description: Om partiet räknas för ratingen.
        timeControl:
          type: string
          pattern: '^[0-9]+\+[0-9]+$'
          description: Betänketiden i sekunder (`180+2`).
        white:
          $ref: '#/components/schemas/PlayerSide'
        black:
          $ref: '#/components/schemas/PlayerSide'
        status:
          type: integer
          enum:
            - 1
            - 2
            - 3
            - 4
          description: '1 `WhiteWins`, 2 `BlackWins`, 3 `Draw`, 4 `Aborted` (se partikoderna).'
        reason:
          type: integer
          minimum: 0
          maximum: 255
          description: Koden för avslutningsorsaken (se partikoderna).
        result:
          type: string
          enum:
            - 1-0
            - 0-1
            - 1/2-1/2
            - '*'
          description: Resultatet, `*` för ett avbrutet parti.
        termination:
          $ref: '#/components/schemas/Termination'
        plies:
          type: integer
          minimum: 0
          description: Antalet halvdrag.
        startedAt:
          type: integer
          format: int64
          description: Starten (Unix-tid i ms).
        endedAt:
          type: integer
          format: int64
          description: Slutet (Unix-tid i ms).
    Termination:
      type: string
      description: Avslutningsorsakens namn (se partikoderna); `Unknown` för en kod som protokollet inte känner till.
      enum:
        - Checkmate
        - Resignation
        - Timeout
        - IllegalMoves
        - Stalemate
        - InsufficientMaterial
        - TimeoutVsInsufficient
        - FivefoldRepetition
        - SeventyFiveMoves
        - ThreefoldClaim
        - FiftyMoveClaim
        - Agreement
        - IllegalMovesVsInsufficient
        - ResignationVsInsufficient
        - Abandonment
        - AbandonmentVsInsufficient
        - Aborted
        - NoShow
        - Forfeit
        - ServerAborted
        - BothDisconnected
        - Unknown
    PlayerGameSummary:
      description: En partisammanfattning sedd från en spelares sida.
      allOf:
        - $ref: '#/components/schemas/GameSummary'
        - type: object
          required:
            - color
          properties:
            color:
              type: string
              enum:
                - white
                - black
              description: Spelarens sida.
    HistoryGameSummary:
      description: Ett parti i den inloggade spelarens historik.
      allOf:
        - $ref: '#/components/schemas/PlayerGameSummary'
        - type: object
          required:
            - baseMs
            - incMs
            - outcome
          properties:
            baseMs:
              type: integer
              format: int64
              minimum: 0
              description: Grundtiden i millisekunder.
            incMs:
              type: integer
              format: int64
              minimum: 0
              description: Tillägget i millisekunder.
            outcome:
              type: string
              enum:
                - win
                - loss
                - draw
                - aborted
              description: Utfallet sett från spelarens sida.
    HistoryPage:
      type: object
      required:
        - games
        - next
        - total
      properties:
        games:
          type: array
          description: Sidans partier, nyaste först.
          items:
            $ref: '#/components/schemas/HistoryGameSummary'
        next:
          type:
            - integer
            - 'null'
          format: int64
          description: Det id som ska skickas som `before` för nästa sida, eller `null` på sista sidan.
        total:
          type: integer
          minimum: 0
          description: Antalet partier som matchar filtret, över alla sidor.
    MoveRecord:
      type: object
      description: Ett halvdrag i ett parti.
      required:
        - uci
        - spentMs
        - clockMs
      properties:
        uci:
          type: string
          pattern: '^[a-h][1-8][a-h][1-8][nbrq]?$'
          description: Draget i UCI-notation, med `n`, `b`, `r` eller `q` tillagt vid förvandling.
        spentMs:
          type:
            - integer
            - 'null'
          minimum: 0
          description: Den tid som räknades för draget (ms), `null` när posten inte har den.
        clockMs:
          type:
            - integer
            - 'null'
          minimum: 0
          description: Den dragande spelarens klocka efter draget (ms), `null` när posten inte har den.
    PgnTagsView:
      type: object
      description: De viktigaste PGN-taggarna, för visning. `Termination` är här avslutningsorsakens namn; PGN-filen använder standardvärdena i PGN.
      required:
        - Event
        - Site
        - Date
        - Round
        - White
        - Black
        - Result
        - WhiteElo
        - BlackElo
        - TimeControl
        - Termination
        - PlyCount
      properties:
        Event:
          type: string
          description: '`<SERVER_NAME> rated <category>` eller `<SERVER_NAME> casual <category>`.'
        Site:
          type: string
          description: '`SERVER_PUBLIC_HOST`.'
        Date:
          type: string
          pattern: '^[0-9]{4}\.[0-9]{2}\.[0-9]{2}$'
          description: Startdatumet i UTC.
        Round:
          type: string
          const: '-'
        White:
          type: string
        Black:
          type: string
        Result:
          type: string
          enum:
            - 1-0
            - 0-1
            - 1/2-1/2
            - '*'
        WhiteElo:
          description: Ratingen vid start, eller `"-"`.
          oneOf:
            - type: integer
            - type: string
              const: '-'
        BlackElo:
          description: Ratingen vid start, eller `"-"`.
          oneOf:
            - type: integer
            - type: string
              const: '-'
        TimeControl:
          type: string
          pattern: '^[0-9]+\+[0-9]+$'
        Termination:
          $ref: '#/components/schemas/Termination'
        PlyCount:
          type: integer
          minimum: 0
    GameRecord:
      description: En partipost med dess drag och klockor. Den har en partisammanfattnings fält, utan `color` och `outcome`.
      allOf:
        - $ref: '#/components/schemas/GameSummary'
        - type: object
          required:
            - baseMs
            - incMs
            - statusName
            - rematchOf
            - moves
            - pgn
          properties:
            baseMs:
              type: integer
              format: int64
              minimum: 0
              description: Grundtiden i millisekunder.
            incMs:
              type: integer
              format: int64
              minimum: 0
              description: Tillägget i millisekunder.
            statusName:
              type: string
              enum:
                - WhiteWins
                - BlackWins
                - Draw
                - Aborted
              description: Namnet på `status`.
            rematchOf:
              type:
                - integer
                - 'null'
              format: int64
              description: Id för det parti som detta är en revansch av, eller `null`.
            moves:
              type: array
              description: En post per halvdrag.
              items:
                $ref: '#/components/schemas/MoveRecord'
            pgn:
              $ref: '#/components/schemas/PgnTagsView'
            you:
              type: string
              enum:
                - white
                - black
              description: Endast när tokenens spelare spelade det här partiet; spelarens sida.
            reportable:
              type: boolean
              description: Endast när tokenens spelare spelade det här partiet; `true` när `POST /reports` skulle ta emot en anmälan av motståndaren för det här partiet nu (partiet avslutades inom 7 dagar, dygnskvoten är inte förbrukad, och motståndaren är inte redan anmäld för det här partiet).
    PlayerProfile:
      type: object
      required:
        - username
        - createdAt
        - ratings
        - games
      properties:
        username:
          type: string
          description: Användarnamnet, så som spelaren skrev det.
        createdAt:
          type: integer
          format: int64
          description: När kontot skapades (Unix-tid i ms).
        ratings:
          type: array
          description: Endast de officiella kategorierna, i serverns ordning.
          items:
            $ref: '#/components/schemas/RatingSummary'
        games:
          type: object
          required:
            - total
            - rated
            - wins
            - draws
            - losses
          properties:
            total:
              type: integer
              minimum: 0
              description: Alla sparade partier, även partier utan rating och avbrutna partier.
            rated:
              type: integer
              minimum: 0
              description: Partier med rating, summerade över ratingposterna.
            wins:
              type: integer
              minimum: 0
            draws:
              type: integer
              minimum: 0
            losses:
              type: integer
              minimum: 0
    PlayerGamesPage:
      type: object
      required:
        - username
        - games
        - next
      properties:
        username:
          type: string
          description: Användarnamnet, så som spelaren skrev det.
        games:
          type: array
          description: Sidans partier, nyaste först.
          items:
            $ref: '#/components/schemas/PlayerGameSummary'
        next:
          type:
            - integer
            - 'null'
          format: int64
          description: Det sista partiets id när sidan är full (nästa sida kan då vara tom), annars `null`.
    Leaderboard:
      type: object
      required:
        - category
        - minGames
        - updatedAt
        - players
      properties:
        category:
          type: string
          description: Kategorins id.
        minGames:
          type: integer
          minimum: 0
          description: Antalet räknade partier som en post behöver för att listas (`PROVISIONAL_GAMES`).
        updatedAt:
          type: integer
          format: int64
          description: När listan beräknades (Unix-tid i ms).
        players:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/LeaderboardEntry'
    LeaderboardEntry:
      type: object
      required:
        - rank
        - username
        - rating
        - games
        - wins
        - draws
        - losses
        - peak
      properties:
        rank:
          type: integer
          minimum: 1
        username:
          type: string
        rating:
          type: integer
        games:
          type: integer
          minimum: 0
        wins:
          type: integer
          minimum: 0
        draws:
          type: integer
          minimum: 0
        losses:
          type: integer
          minimum: 0
        peak:
          type: integer
    GifRequest:
      type: object
      additionalProperties: false
      required:
        - pgn
      properties:
        pgn:
          type: string
          description: PGN-texten, högst 65 536 byte UTF-8. Endast det första partiet används.
        size:
          type: string
          enum:
            - small
            - medium
            - large
          default: medium
          description: Valfritt. Bildstorleken.
        orientation:
          type: string
          enum:
            - white
            - black
          default: white
          description: Valfritt. Den sida som visas nederst på brädet.
        delayMs:
          type: integer
          minimum: 100
          maximum: 3000
          default: 500
          description: Valfritt. Millisekunder per drag, ett JSON-tal med heltalsvärde.
        coords:
          type: boolean
          default: true
          description: Valfritt. Om linjernas bokstäver och radernas siffror ritas runt brädet.
    ReportRequest:
      type: object
      required:
        - gameId
        - reported
        - category
      properties:
        gameId:
          description: Partiet, som ett heltal eller en sträng med 1 till 16 siffror.
          oneOf:
            - type: integer
              format: int64
              minimum: 1
              maximum: 9007199254740991
            - type: string
              pattern: '^[0-9]{1,16}$'
        reported:
          type: string
          minLength: 1
          maxLength: 24
          description: Motståndarens användarnamn, som i partiposten eller som det är nu, utan hänsyn till skiftläge. Det får inte vara tomt.
        category:
          type: string
          enum:
            - cheating
            - abuse
            - other
          description: Vad anmälan gäller.
        comment:
          type:
            - string
            - 'null'
          description: Valfritt. Högst 500 tecken efter att kontrolltecken (utom tabb och radmatning) har tagits bort och texten har trimmats.
    AccountExport:
      type: object
      description: Allt som servern lagrar om kontot (tider som Unix-tid i ms).
      required:
        - format
        - version
        - exportedAt
        - server
        - notes
        - account
        - ratings
        - ratingRefunds
        - sessions
        - securityEvents
        - sanctions
        - conduct
        - reportsFiled
        - games
      properties:
        format:
          type: string
          const: scacelith-account-export
        version:
          type: integer
          const: 1
        exportedAt:
          type: integer
          format: int64
          description: När exporten gjordes.
        server:
          type: object
          required:
            - name
            - host
          properties:
            name:
              type: string
              description: '`SERVER_NAME`.'
            host:
              type: string
              description: '`SERVER_PUBLIC_HOST`.'
        notes:
          type: array
          description: Vad filen innehåller och utelämnar, på enkel engelska för spelaren.
          items:
            type: string
        account:
          description: Kontovyn från `GET /account/me`, med `googleEmail`.
          allOf:
            - $ref: '#/components/schemas/AccountView'
            - type: object
              required:
                - googleEmail
              properties:
                googleEmail:
                  type:
                    - string
                    - 'null'
                  description: Adressen till det kopplade Google-kontot, eller `null`.
        ratings:
          type: array
          description: De fullständiga ratingposterna.
          items:
            $ref: '#/components/schemas/ExportRating'
        ratingRefunds:
          type: array
          description: Ratingpoäng som getts tillbaka efter att en motståndare har befunnits fuska, summerade per UTC-dygn och kategori, nyaste först. Varken partierna eller fuskarna namnges.
          items:
            $ref: '#/components/schemas/RatingRefund'
        sessions:
          type: array
          description: Alla sparade sessioner, nyaste först, utan någon token. Gallringen raderar en utgången session, och en utloggad session ett dygn efter utloggningen.
          items:
            $ref: '#/components/schemas/ExportSession'
        securityEvents:
          type: array
          description: Säkerhetshändelserna, nyaste först, som sparas under `RETENTION_SECURITY_DAYS`.
          items:
            $ref: '#/components/schemas/SecurityEvent'
        sanctions:
          type: array
          description: Alla sanktioner, även hävda. Moderatorns namn tas aldrig med.
          items:
            $ref: '#/components/schemas/ExportSanction'
        conduct:
          type: array
          description: De uppförandehändelser som registrerats för partier som spelaren lämnade, som avbröts eller där spelaren uteblev, sparas i 30 dagar.
          items:
            $ref: '#/components/schemas/ConductEvent'
        reportsFiled:
          type: array
          description: De anmälningar som spelaren har gjort.
          items:
            $ref: '#/components/schemas/FiledReport'
        games:
          type: object
          required:
            - total
            - list
          properties:
            total:
              type: integer
              minimum: 0
              description: Antalet partier.
            list:
              type: array
              description: Alla partier, nyaste först, som sammanfattningar från `GET /account/games`. Dragen finns i `GET /games/{id}` och PGN i `GET /games/{id}/pgn`.
              items:
                $ref: '#/components/schemas/HistoryGameSummary'
    ExportRating:
      description: En fullständig ratingpost.
      allOf:
        - $ref: '#/components/schemas/RatingSummary'
        - type: object
          required:
            - rated
            - countedGames
            - updatedAt
          properties:
            rated:
              type: boolean
              description: Om spelaren har lämnat fasen utan fastställd rating.
            countedGames:
              type: integer
              minimum: 0
              description: De partier som har räknats in i ratingen.
            updatedAt:
              type: integer
              format: int64
              description: När posten senast ändrades.
    RatingRefund:
      type: object
      required:
        - day
        - category
        - points
      properties:
        day:
          type: integer
          format: int64
          description: Dygnets 00:00 UTC (Unix-tid i ms).
        category:
          type: string
        points:
          type: integer
          description: De poäng som gavs tillbaka det dygnet i den kategorin.
    ExportSession:
      type: object
      required:
        - id
        - createdAt
        - lastSeenAt
        - expiresAt
        - revokedAt
        - clientLabel
        - ip
      properties:
        id:
          type: integer
          format: int64
        createdAt:
          type: integer
          format: int64
        lastSeenAt:
          type: integer
          format: int64
        expiresAt:
          type: integer
          format: int64
        revokedAt:
          type:
            - integer
            - 'null'
          format: int64
          description: När sessionen loggades ut, eller `null`.
        clientLabel:
          type:
            - string
            - 'null'
        ip:
          type:
            - string
            - 'null'
          description: Inloggningens IP-adress, raderas efter `RETENTION_IP_DAYS`.
    SecurityEvent:
      type: object
      description: |-
        En säkerhetshändelse. `ip` anges bara för det som gjorts i inloggat läge, med kontots lösenord (och andra faktor) eller med en länk som skickats till dess adress: `register`, `email_verified`, `login`, `sso_login`, `sso_linked`, `sso_account_created`, `recovery_code_used`, `password_reset`, `password_changed`, `reauth_failed`, `mfa_setup_started`, `mfa_enabled`, `mfa_disabled`, `recovery_codes_regenerated`, `session_revoked`, `sessions_revoked_all`, `email_change_requested`, `email_changed`, `email_change_refused` och `account_exported`; alla andra typer har `ip: null`, och `ip` raderas efter `RETENTION_IP_DAYS`.

        `detail` behåller bara dessa fält: `login`: `method`; `sso_login`, `sso_account_created`: `provider`; `sso_linked`: `provider` och `method` (`password` eller `password+totp`); `login_failed`: `failures`; `login_lockout`: `retryAfterMs`; `mfa_failed`: `attempts`; `recovery_code_used`: `remaining`; `reauth_failed`: `factor`; `session_revoked` och `sessions_revoked_all`: `reason`; `email_change_refused`: `reason`; `sanction_auto`: `kind`, `gameId`, `until`. En händelse `moderator_action` behåller bara `{ action }`, för åtgärderna `ban`, `unban`, `reset_mfa`, `verify_email` och `revoke_sessions` (moderatorernas övriga åtgärder utelämnas). Alla andra typer har `detail: null`. Händelserna `rating_refund` utelämnas: deras poäng finns i `ratingRefunds`.
      required:
        - kind
        - at
        - ip
        - detail
      properties:
        kind:
          type: string
          description: Typen av händelse (`login`, `password_changed`...).
        at:
          type: integer
          format: int64
          description: När den inträffade.
        ip:
          type:
            - string
            - 'null'
        detail:
          type:
            - object
            - 'null'
    ExportSanction:
      type: object
      required:
        - id
        - kind
        - reason
        - source
        - gameId
        - startsAt
        - endsAt
        - createdAt
        - liftedAt
      properties:
        id:
          type: integer
          format: int64
        kind:
          type: string
          enum:
            - ban
            - mm_block
            - warning
        reason:
          type:
            - string
            - 'null'
        source:
          type: string
          enum:
            - auto
            - moderator
        gameId:
          type:
            - integer
            - 'null'
          format: int64
        startsAt:
          type: integer
          format: int64
        endsAt:
          type:
            - integer
            - 'null'
          format: int64
        createdAt:
          type: integer
          format: int64
        liftedAt:
          type:
            - integer
            - 'null'
          format: int64
    ConductEvent:
      type: object
      required:
        - kind
        - at
      properties:
        kind:
          type: string
          enum:
            - abandon
            - abort
            - noshow
        at:
          type: integer
          format: int64
    FiledReport:
      type: object
      required:
        - gameId
        - reported
        - category
        - comment
        - createdAt
        - status
      properties:
        gameId:
          type:
            - integer
            - 'null'
          format: int64
        reported:
          type: string
          description: Den anmälda spelarens nuvarande offentliga namn.
        category:
          type: string
          enum:
            - cheating
            - abuse
            - other
        comment:
          type:
            - string
            - 'null'
        createdAt:
          type: integer
          format: int64
        status:
          type: string
          enum:
            - open
            - closed
          description: Om anmälan fortfarande är öppen (om den anmälda spelaren har fått sanktioner sägs inte).
    LinkTokenForm:
      type: object
      additionalProperties: false
      required:
        - token
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: E-postlänkens token (det dolda fältet i sidans formulär).
    ResetPasswordForm:
      type: object
      additionalProperties: false
      required:
        - token
        - newPassword
        - confirmPassword
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: Återställningslänkens token (formulärets dolda fält).
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Det nya lösenordet; registreringens lösenordsregler gäller.
        confirmPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Det nya lösenordet igen; det måste vara likadant.
    HtmlDocument:
      type: string
      contentMediaType: text/html
      description: En HTML-sida i UTF-8 (`text/html; charset=utf-8`), utan JavaScript eller externa resurser.
