openapi: 3.1.1
info:
  title: Scacelithi serveri HTTPS-API
  version: "1.1.0"
  summary: 'Kõigi Scacelithi serverite HTTPS-API: nii ametliku serveri kui ka kogukonna serverite oma.'
  description: |-
    Iga Scacelithi server, nii ametlik `caissa.scacelith.com` kui ka iga kogukonna server, pakub seda HTTPS-API-t. Mäng kasutab seda kõige jaoks, mis jääb väljapoole partiid ennast: registreerumine ja sisselogimine (sh kaheastmeline autentimine ja Google), kontoleht, partiide ajalugu, partiide PGN-failide allalaadimine ja animeeritud GIF-id, sisselogitud seadmed, andmete allalaadimine, konto kustutamine ja teated.

    Reaalajas mäng (vastase otsimine, väljakutsed, käigud, kellad) käib sama serveri WebSocketi (`wss://<host>/ws`) kaudu, mis avatakse selle API seansitokeniga; seda kirjeldab `PROTOCOL.md`, mitte see dokument.

    Allpool toodud vaikeväärtused kehtivad muutmata konfiguratsiooniga serveri kohta; kogukonna server võib neid muuta (`CONFIG.md` loetleb kõik seaded).

    ## Baas-URL, port ja transport

    - Ametlik server: `https://caissa.scacelith.com/api/v1`, TCP-port 443.
    - Kogukonna server: `https://<SERVER_PUBLIC_HOST>[:<port>]/api/v1`. `API_PORT` on vaikimisi 443; NAT-i või puhverserveri taga on mängijate kasutatav port `PUBLIC_API_PORT`.
    - Mängu WebSocket on vaikimisi samas pordis (`WS_PORT` viib selle teise porti; `GET /info` ütleb kliendile, kus see asub).
    - HTTP/1.1 üle TLS-i. Seadega `TLS_MODE=proxy` lõpetab TLS-ühenduse serveri ees asuv pöördpuhverserver. `TLS_MODE=off` (lihtne HTTP) on mõeldud ainult kohalikuks arenduseks.
    - E-kirjade linkidest avatavad HTML-lehed (`/verify-email`, `/reset-password`, `/confirm-email-change`) asuvad serveri juurtasemel, väljaspool `/api/v1`. Tervisekontrolli otspunktid vastavad nii juurtasemel kui ka `/api/v1` all. Google'iga sisselogimisel pole serveris lehte: Google saadab brauseri tagasi otse mängule, aadressile `127.0.0.1`, või sisselogimise alustanud veebisaidi lehele (`GOOGLE_WEB_REDIRECT_URI`).
    - Lõpus olevat kaldkriipsu eiratakse (`/api/v1/info/` on sama mis `/api/v1/info`) ja teeparameetrid URL-dekodeeritakse (parameetrile, mis pole kehtiv URL-kodeering, vastatakse 400 `invalid_request`).

    ## Päringud

    - **JSON-kehad.** Päringutel `POST`, `PUT` ja `DELETE` on JSON-objektist keha päisega `Content-Type: application/json`. Muu `charset` kui UTF-8 lükatakse tagasi, samuti iga muu sisutüüp (415 `unsupported_media_type`). Tühi keha loetakse objektiks `{}` ja just seda võtavad vastu parameetriteta otspunktid (`POST /auth/logout`, `POST /auth/logout-all`, `DELETE /auth/sessions/{id}`).
    - **Suurus ja aeg.** Keha suurus on piiratud `HTTP_BODY_LIMIT` baidiga (vaikimisi 16 384; otspunktil `POST /gif` on oma piir 135 168 baiti): 413 `payload_too_large`. Keha peab saabuma 10 sekundi jooksul: 408 `request_timeout`. Kummagi vea järel sulgeb server ühenduse.
    - **Ranged skeemid.** Väli, mida otspunkt ei tunne, lükatakse tagasi, valikuliseks märkimata väli on kohustuslik ning tüüpe ja pikkusi kontrollitakse. Igale sellisele veale vastatakse 400 `invalid_request`, kus `field` nimetab välja (pesastatud välja puhul punktidega, nt `pow.nonce`). Sõned ei tohi sisaldada juhtmärke. Pikkust loetakse UTF-16 koodiühikutes, nii et väljaspool mitmekeelset põhitasandit (BMP) asuv märk (emoji) loeb kahena. Kehale, mis pole JSON, vastatakse 400 `invalid_json`. `POST /gif` ja `POST /reports` kontrollivad oma keha ise (vt vastavat otspunkti).
    - **Päringuparameetrid.** Arvesse läheb parameetri esimene esinemine, tundmatuid parameetreid eiratakse ja `+` dekodeeritakse tühikuks: otspunktid, mis võtavad ajakontrolli kategooria, aktsepteerivad nii `3%2B2` kui ka `3+2`.
    - **Meetodid.** `HEAD` on `GET` ilma kehata. `OPTIONS` olemasoleval teel vastab 204 koos päisega `Allow` (ja CORS-i eelpäringu päistega, vt allpool). Igale muule meetodile, mida teel pole, vastatakse 405 `method_not_allowed` koos päisega `Allow`.
    - **Päringu sihtmärk.** See ei tohi ületada 4096 märki (414 `uri_too_long`). Päring, mida ei saa üldse parsida või mille päiseosa on liiga suur või saabub liiga aeglaselt, saab tühja vastuse 400, 431 või 408 ja ühendus suletakse.
    - **CORS: lubatud loend, vaikimisi väljas.** API teenindab mängu, mis päist `Origin` ei saada. Kui `CORS_ORIGINS` on tühi (vaikimisi), ei saadeta kunagi ühtegi päist `Access-Control-*`, nii et ükski veebileht ei saa vastust lugeda. `CORS_ORIGINS` loetleb veebipäritolud, mille lehed tohivad API-t brauserist kutsuda, igaüks täpselt nii, nagu brauserid selle päises `Origin` saadavad: `https://host` või `https://host:port`, väiketähtedega, ilma tee, lõpukaldkriipsu ja vaikepordita; `http://` ainult `localhost`, `127.0.0.1` ja `[::1]` jaoks (kohalik arendus); metamärke ei lubata. Ametlik server seab oma veebisaidi jaoks `CORS_ORIGINS=https://scacelith.com,https://www.scacelith.com`. Siis kehtib järgmine:
      - Iga vastus `/api/v1` all asuvale teele, välja arvatud tervisekontrolli otspunktid, nii õnnestumised kui ka vead (sh aadressipõhise kihi 429), sisaldab päist `Vary: Origin`. Kui päringu `Origin` ühtib baidi haaval mõne loetletud päritoluga, sisaldab vastus ka päist `Access-Control-Allow-Origin: <that origin>` ning, välja arvatud eelpäringu vastuses, päist `Access-Control-Expose-Headers: Retry-After, Content-Disposition`, et leht saaks lugeda veakoode, päist `Retry-After` ning PGN- ja GIF-allalaadimiste failinimesid.
      - Loetletud päritolust olemasolevale teele tehtud eelpäringule (`OPTIONS` koos päisega `Access-Control-Request-Method`) vastatakse 204, ilma kehata, päistega `Allow`, `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods` (päise `Allow` meetodid), `Access-Control-Allow-Headers: Authorization, Content-Type`, `Access-Control-Max-Age: 600` ja `Vary: Origin`. See ei vaja tokenit ega lähe arvesse ühegi konto kvoodis ega otspunkti piirangus; nagu iga päring, võtab see aadressipõhise kihi žetooni. Brauser kasutab eelpäringut uuesti 10 minutit, kuid ainult täpselt sama URL-i puhul, päringuparameetrid kaasa arvatud. Päring ilma päiseta `Authorization` ja ilma JSON-kehata, nt `GET /info` või `GET /leaderboard`, eelpäringut ei vaja.
      - Iga muu vastus eelpäringule tähendab ebaõnnestunud eelpäringut, isegi CORS-i päistega: 404 olematule teele või aadressipõhise kihi 429. Brauser jätab siis päringu saatmata ja leht näeb võrguviga, ilma vastuse ja selle päiseta `Retry-After`.
      - Päist `Access-Control-Allow-Credentials` ei saadeta kunagi, samuti mitte `Access-Control-Allow-Origin: *`: leht saadab oma seansitokeni päises `Authorization: Bearer`, nagu mäng, ja API ei sea ühtegi küpsist. HTML-lehtedel, tervisekontrolli otspunktidel ja loetamatutele päringutele antavatel tühjadel vastustel CORS-i päiseid pole. WebSocket CORS-i ei kasuta: `WS_ALLOWED_ORIGINS` loetleb lehed, mis tohivad selle avada.
      - Loetlemata päritolu leht ei loe midagi ja selle saitidevahelisi kirjutuspäringuid ei saadeta kunagi: JSON-kehad ja päis `Authorization` vajavad eelpäringut, mille vastuses pole selle päritolu jaoks päist `Access-Control-Allow-Origin`. `Cross-Origin-Resource-Policy: same-origin` jääb igasse vastusesse: see kehtib ainult `no-cors`-päringutele (teine sait, mis vastuse oma lehele manustab), mitte kunagi loetletud lehe `cors`-päringutele.

    ## Vastused ja vead

    - Vastused on UTF-8-kodeeringus JSON, välja arvatud PGN-i allalaadimine (`application/x-chess-pgn`), animeeritud GIF-id (`image/gif`) ja HTML-lehed. Ajad on millisekundid alates 01.01.1970 UTC. ID-d on täisarvud. Partiide ID-del on kuni 16 numbrit, kuid need jäävad alla 2^53, nii et JSON-arv (double) mahutab need täpselt.
    - Iga API vastus sisaldab päiseid `Cache-Control: no-store`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`, `X-Frame-Options: DENY`, `Cross-Origin-Resource-Policy: same-origin` ja `Content-Security-Policy: default-src 'none'; frame-ancestors 'none'` (HTML-lehtedel on oma poliitika). Seadega `TLS_MODE=native` sisaldab see ka päist `Strict-Transport-Security: max-age=31536000`. Vastustel, mille kuulaja kirjutab ise enne API-t (tervisekontrolli otspunktide `GET` ja `HEAD`, aadressipõhise kihi 429 ning 500 `internal_error`, kui API täielikult ebaõnnestub), on samad päised peale `Cross-Origin-Resource-Policy` ja nende `Content-Type` on `application/json` ilma märgistikuta.
    - Vastus tuleb läbi lugeda 60 sekundi jooksul alates hetkest, mil server on selle valmis saanud, mis on oluline ainult suurte vastuste puhul (GIF, andmete eksport, pikk PGN). Aeg, mille server kulutab vastuse ettevalmistamiseks, ei lähe arvesse ei selles ega ka nendes 30 sekundis, mille järel ühendus suletakse, kui ühtegi baiti pole saabunud ega saadetud.

    Vigadel on üks kuju (skeem `Error`):

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

    `message` on mõeldud logide jaoks ja varulahenduseks; klient valib kuvatava teksti välja `error` põhjal. `retryAfter` (sekundites) on olemas ainult keeldumistel, mis aja möödudes lõpevad, ja siis sisaldab vastus ka sama väärtusega päist `Retry-After`; ainus erand on ajaloo, partiide, mängijate ja edetabeli lugemispäringute 503 `busy`, millel on ainult see väli. Mõned vead lisavad välju: `field` (vigane sisend), `reason` (`weak_password`, `pow_required`), `pow` (`pow_required`), `until` (`banned`), `line` ja `column` (`invalid_pgn`).

    Vead, mida võib anda iga otspunkt:

    | Olek | `error` | Millal |
    |---|---|---|
    | 400 | `invalid_request` | Keha rikub otspunkti skeemi (`field` ütleb, kus), päringu sihtmärk või `Content-Length` on vigane, teeparameeter pole kehtiv URL-kodeering või keha katkes. |
    | 400 | `invalid_json` | Keha pole JSON. |
    | 401 | `unauthorized` | Seanssi nõudval otspunktil puudub päis `Authorization`. |
    | 401 | `invalid_token` | Token on vigane, aegunud, tühistatud või kuulub kustutatud kontole; ka otspunktidel, kus seanss on valikuline. |
    | 404 | `not_found` | Sellist otspunkti pole. Mõned otspunktid kasutavad seda ka: sellist partiid, mängijat või seanssi pole. |
    | 405 | `method_not_allowed` | Tee on olemas teiste meetodite jaoks (vt `Allow`). |
    | 408 | `request_timeout` | Keha ei saabunud 10 sekundi jooksul. |
    | 413 | `payload_too_large` | Keha ületab piiri `HTTP_BODY_LIMIT` (otspunktil `POST /gif` 135 168 baiti). |
    | 414 | `uri_too_long` | Päringu sihtmärk ületab 4096 märki. |
    | 415 | `unsupported_media_type` | Keha pole `application/json` või selle märgistik pole UTF-8. |
    | 429 | `rate_limited` | Päringute piirang (vt allpool): `retryAfter` ja lisaks päis `Retry-After`. |
    | 500 | `internal_error` | Ootamatu tõrge. Server logib selle. |
    | 503 | `timeout` | Server ei vastanud 30 sekundi jooksul (ekspordi puhul 60 sekundit, GIF-ide puhul vaikeseadetega 45 sekundit). |
    | 503 | `server_busy` | Seansi otsimisel või konto muutmisel oli andmebaas lukus (`retryAfter` 1) või paroolide räsimise järjekord on täis (vt allpool). Samuti, väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud: päringu `POST`, `PUT`, `PATCH` või `DELETE` puhul ootab andmebaasis liiga palju kirjutusi; mis tahes päringu puhul töötab liiga palju varasemaid päringuid veel üle oma ajapiiri või ei õnnestunud seansitokenit 5 sekundi jooksul kontrollida. Päringut ei täidetud ja selle päringute piirangutest ei kulunud midagi. |

    Lugemisotspunktid (partiide ajalugu, partiid, mängijad, edetabel) ja eksport vastavad 503 `busy` koos `retryAfter: 1`, kui andmebaas jäi lukustatuks.

    Otspunktid, mis parooli kontrollivad või räsivad, võivad vastata ka ühega järgmistest, mõlemad juhusliku `retryAfter` väärtusega 5 kuni 15 sekundit:

    - 503 `server_busy`: serveri paroolide räsimise järjekord (`PASSWORD_HASH_QUEUE_MAX`) on täis või ooteaeg sai otsa.
    - 429 `rate_limited`: järjekord on vähemalt pooleldi täis ja sellel kliendil (IPv4-aadress või IPv6 /48) ootab juba `PASSWORD_HASH_WAITERS_PER_SOURCE` räsimist. See 429 annab tagasi päringute piirangute žetoonid, mille päring võttis.

    Midagi ei muudetud ega loetud ühtegi ebaõnnestunud katset (välja arvatud otspunktil `POST /auth/sso/google/link`, mille pileti katse ja konto ebaõnnestumine arvestati enne räsimist) ning lähtestamislink jääb kehtima.

    HTML-lehed annavad oma vead HTML-lehtedena samade olekukoodidega.

    ## Autentimine

    Seanssi nõudvad otspunktid võtavad päises `Authorization` vastu kandjatokeni (bearer token; skeem `bearerAuth`):

    ```
    Authorization: Bearer sct_L_8GDd7uzfQ3QQWtqrsWXDTsFWzRwIvJcwIGHhjWPS8
    ```

    - **Tokeni saamine.** Token (`sct_`, millele järgneb 43 base64url-märki) tuleb otspunktist `POST /auth/login`, seejärel otspunktist `POST /auth/login/mfa`, kui kaheastmeline autentimine on sees, otspunktidest `POST /auth/sso/google/finish` ja `POST /auth/sso/google/link` (Google'iga sisselogimine) ning otspunktist `POST /auth/sso/complete`. Igaüks neist vastab `{ token, expiresAt, user }`. Server säilitab tokenist ainult SHA-256 räsi.
    - **Nõutav seanss.** Puuduvale päisele vastatakse 401 `unauthorized` koos päisega `WWW-Authenticate: Bearer realm="scacelith"`. Kehtetule tokenile vastatakse 401 `invalid_token` koos päisega `WWW-Authenticate: Bearer realm="scacelith", error="invalid_token"`. Klient peaks tokeni, mis saab vastuseks `invalid_token`, unustama ja uuesti sisse logima.
    - **Valikuline seanss.** Otspunktidel `GET /games/{id}`, `GET /games/{id}/pgn`, `GET /players/{username}` ja `GET /players/{username}/games` on seanss valikuline. Ilma päiseta vastavad need avaliku vaatega; kui päis saadetakse, peab selle token kehtima.
    - **Hõivatud andmehoidla.** Kui andmebaas jääb tokeni kontrollimise ajal lukustatuks, vastab seanssi nõudev või aktsepteeriv otspunkt 503 `server_busy` koos `retryAfter: 1` (ja päisega `Retry-After`); kui kontroll kestab üle 5 sekundi, vastab see 503 `server_busy` väärtusega `retryAfter` 2 kuni 5. Mõlemal juhul jäi token hindamata: hoidke see alles ja saatke päring uuesti.
    - **Eluiga.** Seanss lõpeb sellel hetkel, mis saabub varem: `SESSION_MAX_DAYS` (90) päeva pärast sisselogimist (see on `expiresAt`) või `SESSION_IDLE_DAYS` (30) päeva pärast viimast kasutust. Iga kasutus lükkab jõudeoleku piirangu edasi; server kirjutab uue väärtuse taustal mitte sagedamini kui iga 5 minuti järel. Kui andmebaasis ootab palju kirjutusi, lükkab server selle kirjutuse hilisemale päringule, välja arvatud viimasel tunnil enne jõudeoleku piirangu saabumist, nii et kasutuses olev seanss ei aegu. Kontol on kõige rohkem `MAX_SESSIONS_PER_USER` (10) seanssi: uus sisselogimine tühistab vanimad seansid, mis seda arvu ületavad.
    - **Tühistamine.** Väljalogimine (`POST /auth/logout`, `POST /auth/logout-all`, `DELETE /auth/sessions/{id}`) tühistab seansse; sama teevad parooli muutmine (teised seansid), parooli lähtestamine ja konto kustutamine (kõik seansid) ning administraator. API kaudu tehtud tühistamine jõustub kohe ja tühistatud seansiga avatud WebSocket suletakse. Parooli muutmine või lähtestamine ja konto kustutamine tühistavad seansid samas transaktsioonis kui muudatus ise: kui neist mõni ebaõnnestub, ei tühistatud ühtegi seanssi ega muudetud ka midagi muud. Server hoiab seansiotsingute tulemusi vahemälus 30 sekundit, nii et administraatori käsuga (eraldi protsess) tehtud tühistamine jõustub 30 sekundi jooksul.
    - **Ulatus.** Token kuulub ühele serverile ja avab ka selle WebSocketi. Ärge saatke seda kunagi teisele serverile.

    ## Päringute piirangud ja muu piiramine

    Piirangud kehtivad ühes kahest ulatusest. **Klient:** IPv4-aadress või IPv6 /64 (puhverserveri režiimis tuleb aadress päisest `X-Forwarded-For`, mille saatis `TRUSTED_PROXIES` loendis olev aadress); mõned piirangud arvestavad lisaks iga /64-võrgule ka iga IPv6 /48 tervikuna. **Mängija:** sisselogitud konto, olenemata selle aadressist; mängija kohta arvestatavat piirangut arvestatakse otspunktil, kus seanss on valikuline, ilma tokenita päringu puhul kliendi kohta.

    Iga piirang on žetoonide ämber (token bucket), mis mahutab `limit` päringut ja täitub pidevalt kiirusega `limit / window`; `retryAfter` on aeg järgmise žetoonini. Piiranguid märkega *ühine* arvestatakse lisaks sama pikkusega libiseva akna järgi, nii et ükski aken ei sisalda palju rohkem kui `limit` päringut. Iga piirangut arvestatakse kogu serveri peale. Kui üks otspunkti piirangutest päringu tagasi lükkab, antakse tagasi žetoonid, mille otspunkti teised piirangud selle päringu eest võtsid. Keeldumisele vastatakse 429 `rate_limited` koos väljaga `retryAfter` ja päisega `Retry-After`.

    - **Aadressipõhine kiht.** Iga päring (mis tahes tee ja meetod, sh tervisekontrolli otspunktid ja WebSocketi upgrade-päringud) võtab esmalt ühe žetooni oma kliendi kvoodist: `HTTP_RATE_PER_IP` (600) minutis poole minuti mahuga purskega ning IPv6 /48 jaoks `HTTP_RATE_PER_PREFIX` (4 x `HTTP_RATE_PER_IP`). Kliendil võib korraga pooleli olla ka kõige rohkem `IP_MAX_INFLIGHT` (32 x `WORKERS`) päringut (sellest üle `retryAfter` 1). Klient, kes jätkab pärast keeldumisi, blokeeritakse: `ABUSE_BLOCK_REFUSALS_PER_MIN` (600) keeldumist ühe minuti jooksul blokeerib selle 1 minutiks, seejärel iga uue blokeeringu korral 6 tunni jooksul 4, 16 ja 60 minutiks. Piirangute `auth`, `auth_*` ja `reauth` keeldumine loetakse 5 keeldumiseks; mängija kohta arvestatavate piirangute keeldumisi ei loeta kunagi. Loendis `ABUSE_EXEMPT` olevad aadressid jätavad selle kihi vahele, kuid mitte konto kvooti ega otspunktide piiranguid.
    - **Konto kvoot.** Iga päring, millel on kehtiv seansitoken, läheb arvesse ka selle konto kvoodis: `USER_RATE_PER_MIN` (120) minutis kõigi otspunktide peale kokku, olenemata aadressist, poole minuti mahuga purskega.
    - **Otspunktide piirangud:**

    | Piirang | Vaikimisi | Arvestatakse | Otspunktid |
    |---|---|---|---|
    | `auth` | `AUTH_RATE_PER_IP` (20) / 10 min, ühine | kliendi kohta ning `AUTH_RATE_PER_PREFIX` (5 x `AUTH_RATE_PER_IP`) IPv6 /48 kohta | `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) / tund, ühine | kliendi kohta, IPv6 /48 kohta 3 korda rohkem | `POST /auth/register` |
    | `auth_mail` | `AUTH_MAIL_PER_HOUR` (10) / tund, ühine | kliendi kohta, IPv6 /48 kohta 3 korda rohkem | `POST /auth/verify-email/resend` |
    | `auth_forgot` | `AUTH_FORGOT_PER_HOUR` (3) / tund, ühine | kliendi kohta, IPv6 /48 kohta 3 korda rohkem | `POST /auth/password/forgot` |
    | `auth_forgot_day` | `AUTH_FORGOT_PER_DAY` (10) / 24 tundi, ühine | kliendi kohta, IPv6 /48 kohta 3 korda rohkem | `POST /auth/password/forgot` |
    | `auth_reset` | `AUTH_RESET_PER_HOUR` (10) / tund, ühine | kliendi kohta, IPv6 /48 kohta 3 korda rohkem | `POST /auth/password/reset`, `POST /reset-password` |
    | `reauth` | samad arvud mis piirangul `auth`, eraldi ämber, ühine | kliendi ja IPv6 /48 kohta | `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, ühine | mängija kohta | samad otspunktid mis piirangul `reauth` |
    | `account` | 60 / min | mängija kohta | `GET /account/me`, `PUT /account/preferences` |
    | `account_games` | 60 / min | mängija kohta | `GET /account/games` |
    | `account_export` | 5 / tund, ühine | mängija kohta | `POST /account/export` (kontrollitakse enne piirangut `reauth`; iga katse läheb arvesse) |
    | `sessions` | 60 / min | mängija kohta | `POST /auth/logout`, `/auth/logout-all`, `GET /auth/sessions`, `DELETE /auth/sessions/{id}` |
    | `public_read` | 60 / min | mängija kohta (ilma tokenita kliendi kohta) | `GET /players/{username}`, `/players/{username}/games`, `/games/{id}`, `/games/{id}/pgn` (üks ämber kõigi nelja jaoks) |
    | `gif` | 30 / min | mängija kohta | `GET /games/{id}/gif`, `POST /gif` (üks ämber mõlema jaoks) |
    | `gif_user_min`, `gif_user_hour` | `GIF_USER_RENDERS_PER_MIN` (4) / min ja `GIF_USER_RENDERS_PER_HOUR` (30) / tund, ühine | mängija kohta | samad kaks, ainult siis, kui GIF tuleb valmistada (mitte vahemälust) |
    | `gif_ip_min`, `gif_ip_hour` | `GIF_IP_RENDERS_PER_MIN` (12) / min ja `GIF_IP_RENDERS_PER_HOUR` (120) / tund, ühine | kliendi kohta (kõik selle kontod kokku), IPv6 /48 kohta 3 korda rohkem | samad, nagu eespool |
    | `reports` | 30 / tund | mängija kohta | `POST /reports` |
    | `sso_start` | 30 / 10 min, ühine | kliendi kohta ning 90 IPv6 /48 kohta | `POST /auth/sso/google/start`, `POST /auth/sso/google/web/start` (kokku) |
    | `sso_finish` | 30 / min | kliendi kohta | `POST /auth/sso/google/finish` |
    | `page` | 60 / min | kliendi kohta | `GET /verify-email`, `/reset-password`, `/confirm-email-change` |

    Otspunktidel `GET /info` ja `GET /leaderboard` pole oma piirangut: ainult aadressipõhine kiht. Mitme piiranguga otspunkt kontrollib neid oma kirjelduses toodud järjekorras.

    Server töötleb päringut selles järjekorras: aadressipõhine kiht, seejärel tervisekontrolli otspunktid; otspunkti leidmine; kontrollid, et server pole tööga kaugele maha jäänud (503 `server_busy`): liiga palju varasemaid päringuid töötab veel üle oma ajapiiri ning päringu `POST`, `PUT`, `PATCH` või `DELETE` puhul andmebaasi kirjutuste järjekord; autentimine 5 sekundi jooksul (kauem: 503 `server_busy`); konto kvoot, kui päringul on seanss; otspunkti piirangud; keha; otspunkt ise (GIF-i otspunktid võtavad oma renderduspiirangud ainult siis, kui neil on GIF valmistada). Seega ei kuluta seansitokeni tõttu tagasi lükatud päring ühtegi otspunkti žetooni, vigase kehaga päring aga kulutab neid.

    Muud piirangud, mida otspunktid ise rakendavad:

    - **Ebaõnnestunud sisselogimised ühe sisselogimisnimega** (kasutajanimi või e-posti aadress): alates `AUTH_FAILURES_PER_ACCOUNT` (5) ebaõnnestumisest peab iga katse ootama kaks korda kauem kui eelmine (2 s, 4 s jne, kuni 15 minutit): 429 `too_many_attempts` koos väljaga `retryAfter`. Loendur nullitakse pärast tundi ilma ebaõnnestumisteta.
    - **Ebaõnnestunud teised tegurid sisselogimisel:** sama reegel alates konto 5. valest koodist. Üks sisselogimise samm aktsepteerib kõige rohkem 5 valet koodi.
    - **Ühe konto teise teguri koodid:** kõige rohkem `AUTH_MFA_PER_ACCOUNT` (10) koodi (autentimisrakenduse koodid või taastekoodid, õiged või valed) 15 minuti jooksul, mis tahes aadressilt, sisselogimisel ja uuesti autentimisel; sellest üle tuleb 429 `too_many_attempts` enne koodi kontrollimist, nii et taastekood ei kulu ära.
    - **Konto ebaõnnestunud uuesti autentimised** (vale parool või kood): sama reegel (429 `too_many_attempts`), ühine kõigile otspunktidele, mis autendivad uuesti.
    - **E-kirjad:** üks kinnitus- või lähtestamiskiri aadressi kohta iga 5 minuti järel (vastus jääb samaks); üks teavitus „keegi proovis teie aadressi kasutada“ aadressi kohta tunnis.
    - **Teated:** `REPORTS_PER_DAY` (5) mängija kohta 24 tunni jooksul: 429 `report_limit`.

    ## Töötõend (proof of work)

    `POST /auth/register` nõuab alati töötõendit, kui `POW_REGISTER_BITS` on suurem kui 0 (vaikimisi 18; `GET /info` annab selle väljana `pow.register`). `POST /auth/login` ja `POST /auth/sso/google/link` (mõlemal sama tüüpi väljakutse) nõuavad seda ainult 5 minuti jooksul pärast seda, kui server märkab ebaõnnestunud sisselogimiste lainet (`POW_LOGIN_TRIGGER_PER_MIN`, seejärel `POW_LOGIN_BITS`); klient saab sellest teada vastusest.

    1. Päringule ilma tõendita (või tagasi lükatud tõendiga) vastatakse 428 `pow_required` koos väljaga `reason` ja väljakutsega `pow` `{ challenge, bits, expiresAt }`.
    2. Leidke nonce: kuni 20-numbriline kümnendsõne, mille korral `SHA-256(challenge + ":" + nonce)` algab `bits` nullbitiga (esimese baidi kõrgeimast bitist alates). 18 biti jaoks kulub keskmiselt umbes 260 000 räsi.
    3. Saatke sama päring uuesti, nii et kehas on `"pow": { "challenge": "...", "nonce": "123456" }`.

    Väljakutse kehtib 2 minutit ja ainult ühe korra, ühe otspunkti ja ühe kliendivõrgu (IPv4-aadress või IPv6 /64) jaoks. See on allkirjastatud, nii et server ei säilita selle kohta midagi enne, kui see tagasi tuleb. `reason` ütleb, miks tõend tagasi lükati: `required`, `malformed`, `signature`, `endpoint`, `network`, `expired`, `bits`, `work` või `replayed`.

    ## Uuesti autentimine

    Konto muutmisel küsitakse uuesti parooli ja, kui kaheastmeline autentimine on sees, teist tegurit:

    - ainult parool: `POST /account/password` ja `POST /account/mfa/totp/setup`;
    - parool ja autentimisrakenduse kood (taastekoodi ei aktsepteerita): `POST /account/mfa/recovery-codes`;
    - parool ja autentimisrakenduse kood või taastekood: `POST /account/mfa/totp/disable`, `POST /account/email`, `POST /account/export` ja `POST /account/delete`.

    Kehades sisaldab `code` 6-kohalist autentimisrakenduse koodi ja `recoveryCode` taastekoodi (`xxxx-xxxx-xx`; suur- ja väiketähtedel, tühikutel ja sidekriipsudel pole tähtsust). Seal, kus taastekoode aktsepteeritakse, võib taastekoodi saata ka väljal `code`. Iga kood töötab üks kord: kasutatud autentimisrakenduse kood lükatakse tagasi kuni järgmise 30-sekundilise sammuni ja taastekood kaob pärast kasutamist.

    | Olek | `error` | Millal |
    |---|---|---|
    | 403 | `invalid_password` | Vale parool või parool lähtestati või seda muudeti päringu täitmise ajal (midagi ei muudetud). |
    | 403 | `mfa_code_required` | Kaheastmeline autentimine on sees ja ei saadetud ei välja `code` ega `recoveryCode`. |
    | 403 | `invalid_code` | Vale või juba kasutatud kood. |
    | 400 | `password_not_set` | Ainult Google'iga sisse logival kontol pole veel parooli („Unustasite parooli?“ määrab selle). |
    | 429 | `too_many_attempts` | Sellel kontol on liiga palju ebaõnnestumisi või on proovitud liiga palju koode. |
    | 503 / 429 | `server_busy` / `rate_limited` | Paroolide räsimise järjekord on hõivatud. |

    Valed paroolid ja koodid lähevad arvesse konto ebaõnnestumiste loenduris ja salvestatakse turvasündmustena.

    ## Mõõdikute port

    Lisaks API pordile vastab server lihtsa HTTP-ga mõõdikute pordis (`METRICS_PORT`, 9464, seotud aadressiga `METRICS_BIND`, vaikimisi 127.0.0.1; hoidke see privaatsena). See ei kuulu selle API juurde: `GET /healthz` vastab `ok`; `GET /readyz` vastab `ready` alates käivituse lõpuleviimisest (iga kild on oma žurnaali taasesitanud ja kuulajad on seotud) kuni seiskamise alguseni, muidu 503 `not ready`; `GET /metrics` pakub Prometheuse mõõdikuid ja nõuab seatud `METRICS_TOKEN` korral päist `Authorization: Bearer <token>` täpselt selle tokeniga.
  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: Scacelithi serveri lähtekood ja selle viitedokumentatsioon (API.md, PROTOCOL.md, CONFIG.md).
  url: https://github.com/DarkCenobyte/scacelith-chess-server
servers:
  - url: https://caissa.scacelith.com/api/v1
    description: Ametlik server.
  - url: https://{host}:{port}/api/v1
    description: Mis tahes Scacelithi server, näiteks kogukonna server.
    variables:
      host:
        default: caissa.scacelith.com
        description: Serveri avalik hostinimi (`SERVER_PUBLIC_HOST`).
      port:
        default: "443"
        description: Avalik API port (`PUBLIC_API_PORT`, selle puudumisel `API_PORT`).
tags:
  - name: server-info
    x-displayName: Serveri teave
    description: Mida klient peab teadma enne sisselogimist või ühendumist.
  - name: health
    x-displayName: Tervisekontroll
    description: |-
      Serveri elusolek ja valmisolek seire jaoks. Need otspunktid vastavad API pordis enne autentimist ja kõiki otspunktide piiranguid, kuid nagu iga päring, võtavad need aadressipõhise kihi žetooni; seirehosti saab lisada loendisse `ABUSE_EXEMPT`. Iga otspunkti puhul töötavad mõlemad teed: serveri juurtasemel ja `/api/v1` all.

      Mõõdikute pordil on oma tervisekontrolli otspunktid, mida kirjeldab sissejuhatus.
  - name: auth
    x-displayName: Registreerumine ja sisselogimine
    description: |-
      Konto loomine, parooliga (ja teise teguriga) sisselogimine, kinnitus- ja parooli lähtestamise e-kirjad.

      `POST /auth/login`, `POST /auth/login/mfa`, `POST /auth/sso/google/finish`, `POST /auth/sso/google/link` ja `POST /auth/sso/complete` vastavad seansiga `{ token, expiresAt, user }` (või esimeste puhul teise sammuga).
  - name: google-sign-in
    x-displayName: Google'iga sisselogimine
    description: |-
      Saadaval, kui `GET /info` ütleb `sso.google: true`; muidu vastab iga allpool olev otspunkt 404 `sso_disabled`. Veebisait logib samamoodi sisse oma kliendiga, kui `sso.googleWeb` on true (allpool, **Veebisaidil**). Mäng logib sisse süsteemi brauseri kaudu RFC 8252 kohase paigaldatud rakenduste vooga (klient tüübiga „Desktop app“): Google saadab brauseri tagasi mängu kuulajale aadressil `127.0.0.1`, mitte kunagi sellele serverile, ja mäng ei näe kunagi ühtegi Google'i mandaati.

      1. Klient kuulab aadressil `127.0.0.1:0` (pordi valib süsteem) ja loob PKCE paari: väärtuse `codeVerifier`, mis koosneb 43–128 märgist `[A-Za-z0-9._~-]`, ja väärtuse `codeChallenge = BASE64URL(SHA-256(codeVerifier))`, milles on 43 märki ja täidet pole.
      2. `POST /auth/sso/google/start` koos väljakutse ja pordiga tagastab Google'i URL-i ja katse väärtuse `state`. Klient kontrollib URL-i (vt allpool) ja avab selle brauseris.
      3. Google saadab brauseri aadressile `http://127.0.0.1:<port>/oauth2/google/<tag>?code=...&state=...`. Klient aktsepteerib ainult alustamise vastuses saadud väärtust `state` ega saada midagi pärast ümbersuunamist `error=`.
      4. `POST /auth/sso/google/finish` koos katse ID, väärtuste `codeVerifier`, `state` ja `code` (ning `iss`, kui Google selle saatis).
      5. Vastus on seanss, kaheastmelise autentimise samm (jätkake otspunktiga `POST /auth/login/mfa`), uue mängija puhul `needsUsername` (jätkake otspunktiga `POST /auth/sso/complete`) või `needsPassword`, kui seda aadressi kasutab parooliga konto (jätkake otspunktiga `POST /auth/sso/google/link`).

      **Päritolutunnus.** Ümbersuunamise URI sisaldab mängija lisatud serveri tunnust, et mäng lükkaks tagasi URL-i, mille teine server oma mängijate jaoks sai. Päritolu on väiketähtedega host (IPv6-literaal nurksulgudes), `:` ja kümnendsüsteemis API port, mis kirjutatakse alati välja, ka 443: server võtab `SERVER_PUBLIC_HOST` ja oma avaliku API pordi, mäng aadressi, millega see on ühendatud. Tunnus on SHA-256(UTF-8 `"scacelith-sso-origin-v1\n"` + päritolu) base64url-kuju (ilma täiteta) esimesed 22 märki. Ümbersuunamise URI on `"http://127.0.0.1:" + port + "/oauth2/google/" + tag`: alati IPv4-literaal ja mängu kuulaja port (1024 kuni 65535). Server ei võta kliendilt kunagi URI-t, hosti ega teed.

      | Päritolu | Tunnus |
      |---|---|
      | `play.scacelith.example:443` | `IhcScoV7eDOzTEcSnqPUPt` |
      | `localhost:8443` | `TFGx7zQ_8QlGZW5zpqznCr` |
      | `[::1]:8443` | `XToJm0DG5PjciEVmZa9Cho` |
      | `127.0.0.1:50443` | `3r653wM5ZjYsHcAJljmCwY` |

      Seega töötab Google'iga sisselogimine ainult mängijatele, kes lisasid serveri täpselt nimega `SERVER_PUBLIC_HOST` ja selle avaliku API pordiga.

      **Mida mäng kontrollib enne brauseri avamist.** `authUrl` algab täpselt `https://accounts.google.com/o/oauth2/v2/auth?` ja on prinditav ASCII alla 4096 märgi ning selle päringuosas on täpselt üks `response_type=code`, täpselt üks `redirect_uri`, mis võrdub URI-ga, mille mäng arvutab oma pordist ja päritolutunnusest, täpselt üks `state`, mis võrdub vastuse väärtusega `state`, `code_challenge_method=S256` ja 43-märgiline `code_challenge`. Vastasel juhul peatab mäng oma kuulaja ega ava midagi. Päringud `finish` ja `link` saadab mäng ainult serverile, mis vastas päringule `start`.

      **Veebisaidil.** Veebisait (ametlik sait: <https://scacelith.com/>) logib sisse sama Google'i projekti kliendiga tüübiga „Web application“ (`GOOGLE_WEB_CLIENT_ID`). Google saadab brauseri tagasi saidi ühele lehele, mille haldaja on seadistanud (`GOOGLE_WEB_REDIRECT_URI`, mis on kirjas kliendi loendis „Authorized redirect URIs“), ja see leht postitab koodi otspunktile `finish`; ka siin ei võta server kliendilt ümbersuunamise URI-t.

      1. Leht loob PKCE paari nagu eespool, hoiab verifitseerijat vahekaardil (`sessionStorage`) ja saadab väljakutsega päringu `POST /auth/sso/google/web/start`.
      2. Leht kontrollib väärtust `authUrl` samamoodi nagu mäng, ainult et selle `redirect_uri` peab olema lehe enda päritolu leht, ning liigub seejärel sellele aadressile. Ühtegi Google'i skripti ei laadita.
      3. Google saadab brauseri aadressile `GOOGLE_WEB_REDIRECT_URI?code=...&state=...`. Leht aktsepteerib ainult alustamise vastuses saadud väärtust `state` ega saada midagi pärast ümbersuunamist `error=`.
      4. `finish`, `link` ja `complete` järgnevad nagu mängu puhul. Katse mäletab, et selle alustas veebisait: kood vahetatakse veebisaidi kliendi ja lehega ning ID-token peab nimetama seda klienti (mängu kliendi kood või ID-token ebaõnnestub veaga 502 `sso_failed`, ja vastupidi).

      Google'i konto jõuab mängust ja veebisaidilt samale Scacelithi kontole: seos sisaldab Google'i subjekti (`sub`), mis tuvastab Google'i konto olenemata kliendist. Veebisaidi päritolu peab olema loendis `CORS_ORIGINS` (muidu hoiatab `check-config`).

      **Millisele kontole Google'iga sisselogimine jõuab:** kontole, mis on selle Google'i kontoga juba seotud; muidu aktiivsele kontole, millel on Google'i kinnitatud aadress (parooliga konto korral vastab `finish` väärtusega `needsPassword` ja seos salvestatakse alles siis, kui konto parool ja seejärel, kui see on sees, teine tegur on kontrolli läbinud; paroolita konto korral 409 `sso_account_exists`); muidu uuele kontole (`needsUsername`). Google'i kontot ei seota olemasoleva kontoga kunagi ainult aadressi alusel. Konto aadressile saadetakse e-kiri, kui Google'iga sisselogimine loob konto ja kui see lisatakse olemasolevale kontole.
  - name: sessions
    x-displayName: Seansid
    description: Konto sisselogitud seadmed ja väljalogimine.
  - name: account
    x-displayName: Konto
    description: Konto vaade, selle eelistused ja parool.
  - name: two-step-verification
    x-displayName: Kaheastmeline autentimine
    description: Autentimisrakendused (TOTP, RFC 6238) SHA-1, 6 numbri, 30-sekundiliste sammude ja ühe sammu tolerantsiga kummaski suunas, lisaks ühekordsed taastekoodid.
  - name: email-change
    x-displayName: E-posti aadressi muutmine
    description: Konto aadressi muutmine, mis kinnitatakse uuele aadressile saadetud lingiga.
  - name: data-export
    x-displayName: Andmete eksport
    description: Kõik, mida server konto kohta säilitab, ühe JSON-failina.
  - name: account-deletion
    x-displayName: Konto kustutamine
    description: Konto lõplik kustutamine.
  - name: game-history
    x-displayName: Partiide ajalugu
    description: Sisselogitud mängija enda partiid, filtreeritult ja lehekülgedena.
  - name: games
    x-displayName: Partiid ja PGN
    description: |-
      Partiikirjed koos käikude ja kelladega ning nende PGN-failid.

      **Partiide koodid.** `status`: 1 `WhiteWins`, 2 `BlackWins`, 3 `Draw`, 4 `Aborted` (salvestatakse ainult lõppenud partiid). `result`: `1-0`, `0-1`, `1/2-1/2` või `*` (katkestatud). `reason` koos oma nimega (`termination`) ja sõnadega, millega PGN-faili käigutekst lõpeb (failis inglise keeles, siin koos tõlkega); koodid 7, 14 ja 21 on viigid (mängija, kelle aeg sai otsa, kes andis alla või lahkus partiist, oli vastamisi vastasega, kes ei saanud matti panna: FIDE 6.9 ja 5.1.2; reegli annab `DESIGN.md` 6.3) ja server ei lõpeta kunagi partiid koodidega 4 ja 13 (need kuuluvad ühisesse põhjuste loendisse):

      | Kood | `termination` | Sõnad PGN-is |
      |---|---|---|
      | 1 | `Checkmate` | Checkmate (matt) |
      | 2 | `Resignation` | Resignation (allaandmine) |
      | 3 | `Timeout` | Loss on time (aja ületamine) |
      | 4 | `IllegalMoves` | Second illegal move (forfeit) (teine reeglivastane käik, kaotus) |
      | 5 | `Stalemate` | Stalemate (patt) |
      | 6 | `InsufficientMaterial` | Dead position (insufficient material) (surnud seis, ebapiisav materjal) |
      | 7 | `TimeoutVsInsufficient` | Flag fall, but the opponent cannot checkmate (aja ületamine, kuid vastane ei saa matti panna) |
      | 8 | `FivefoldRepetition` | Fivefold repetition (viiekordne kordus) |
      | 9 | `SeventyFiveMoves` | 75-move rule (75 käigu reegel) |
      | 10 | `ThreefoldClaim` | Threefold repetition (claimed) (kolmekordne kordus, nõutud) |
      | 11 | `FiftyMoveClaim` | 50-move rule (claimed) (50 käigu reegel, nõutud) |
      | 12 | `Agreement` | Draw by agreement (kokkuleppeline viik) |
      | 13 | `IllegalMovesVsInsufficient` | Second illegal move, but the opponent cannot checkmate (teine reeglivastane käik, kuid vastane ei saa matti panna) |
      | 14 | `ResignationVsInsufficient` | Resignation, but the opponent cannot checkmate (allaandmine, kuid vastane ei saa matti panna) |
      | 20 | `Abandonment` | Abandoned (disconnected for too long) (lahkus partiist, ühendus oli liiga kaua katkenud) |
      | 21 | `AbandonmentVsInsufficient` | Abandoned, but the opponent cannot checkmate (lahkus partiist, kuid vastane ei saa matti panna) |
      | 22 | `Aborted` | Game aborted (partii katkestati) |
      | 23 | `NoShow` | Aborted: first move not played in time (katkestatud: esimest käiku ei tehtud õigel ajal) |
      | 24 | `Forfeit` | Forfeit (fair play violation) (loobumiskaotus, ausa mängu reeglite rikkumine) |
      | 25 | `ServerAborted` | Aborted by the server (server katkestas partii) |
      | 26 | `BothDisconnected` | Aborted: both players disconnected (katkestatud: mõlema mängija ühendus katkes) |

      Kood 14 on uus protokolli alamversioonis (minor) 1: enne seda kirjutatud klient peaks näitama tundmatut koodi tavalise partii lõpuna koos selle väärtusega `result`.
  - name: gifs
    x-displayName: Animeeritud GIF-id
    description: |-
      Partii animeeritud GIF-ina, alleshoidmiseks või jagamiseks: malelaud ülaltvaates, üks kaader iga seisu kohta algseisust lõppseisuni, mängijate nimed ja reitingud laua kohal, viimane käik laua all ning viimasel kaadril tulemus ja see, kuidas partii lõppes.

      **Kulu, vahemälu ja kvoodid.** GIF valmistatakse eraldi renderduslõimel, mitte kunagi partiisid käitavatel lõimedel, ja madalaima protsessoriprioriteediga: `GIF_THREADS` lõime (vaikimisi `WORKERS`), mis käivitatakse esimese GIF-iga ja peatatakse pärast minutit ilma GIF-ideta. Kuni `GIF_QUEUE_MAX` (4 x `WORKERS`) GIF-i ootab lõime, igaüks kõige rohkem `GIF_QUEUE_TIMEOUT_MS` (10 s); renderdamine võib kesta `GIF_RENDER_TIMEOUT_MS` (30 s). Server hoiab valmistatud GIF-e `GIF_CACHE_MB` MB suuruses vahemälus (32 x `WORKERS`), kust kõige kauem kasutamata GIF-id eemaldatakse esimesena; vahemälust saadud GIF või GIF, mida juba valmistatakse mõne teise päringu jaoks, renderdamist ei kuluta. Vahemälu võti hõlmab kõike, mis pilti muudab, ka nimesid.

      Iga päring läheb arvesse piirangus `gif` (mängija kohta 30 minutis mõlema otspunkti peale kokku). GIF, mis tuleb valmistada, läheb arvesse ka renderduspiirangutes: mängija kohta `GIF_USER_RENDERS_PER_MIN` (4) minutis ja `GIF_USER_RENDERS_PER_HOUR` (30) tunnis; kliendi kohta, kõik selle kontod kokku, `GIF_IP_RENDERS_PER_MIN` (12) minutis ja `GIF_IP_RENDERS_PER_HOUR` (120) tunnis ning IPv6 /48 kohta 3 korda rohkem. Klient peaks alla laaditud faili alles hoidma, mitte seda uuesti küsima, ning pärast vastust 429 või 503 ootama aja `retryAfter`.

      Suurused: `small` (32 px väljad: 284 x 350 pikslit), `medium` (48 px: 424 x 515), `large` (72 px: 628 x 762); ilma koordinaatideta 268 x 342, 400 x 503 ja 600 x 748. Algseis püsib vähemalt 1 s (või viivituse aja, kui see on pikem), lõppseis 3 s ja GIF kordub lõputult; pärast esimest kaadrit salvestatakse ainult pildi muutuv osa. 40-käiguline partii võtab umbes 135, 205 ja 325 KiB (small, medium, large), 150-käiguline partii 0,5; 0,8 ja 1,2 MiB.
  - name: players
    x-displayName: Mängijad
    description: Avalikud profiilid ja hiljutised partiid. Ainult avalikud andmed, mitte kunagi e-posti aadress, seanss, sanktsioon ega usaldusväärsuse tase. Kustutatud kontol pole profiili.
  - name: leaderboard
    x-displayName: Edetabel
    description: Iga ametliku kategooria parimad mängijad.
  - name: reports
    x-displayName: Teatamine
    description: Hiljutise partii vastasest teatamine moderaatoritele.
  - name: pages
    x-displayName: HTML-lehed
    description: |-
      Lehed brauseri jaoks, mis avatakse e-kirjade linkidest. Nende lingid viitavad aadressile `https://<SERVER_PUBLIC_HOST>` (koos osaga `:<PUBLIC_API_PORT>`, kui see pole 443), väljaspool `/api/v1`.

      - Lehed ei käivita JavaScripti ega laadi ühtegi välist ressurssi. Neid serveeritakse päisega `Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'`.
      - `GET` näitab ainult nuppu või vormi, et linki avav e-kirjade skanner seda ära ei kasutaks. Muudatus toimub päringuga `POST`: vorm, mis saadetakse kujul `application/x-www-form-urlencoded` (aktsepteeritakse ka JSON-keha), lingi tokeniga peidetud väljas. Kaks korda antud väli lükatakse tagasi.
      - Vead (päringute piirangud, vigased väljad) on samuti HTML-lehed pealkirjaga „Request refused“ (päring lükati tagasi) või alates koodist 500 „Server error“ (serveri viga). Ainult enne lehe tuvastamist leitud tõrgetele (liiga pikk või vigane päringu sihtmärk, aadressipõhine kiht) vastatakse JSON-is.
x-tagGroups:
  - name: server
    x-displayName: Server
    tags:
      - server-info
      - health
  - name: accounts
    x-displayName: Kontod
    tags:
      - auth
      - google-sign-in
      - sessions
      - account
      - two-step-verification
      - email-change
      - data-export
      - account-deletion
  - name: games
    x-displayName: Partiid
    tags:
      - game-history
      - games
      - gifs
  - name: community
    x-displayName: Mängijad ja teatamine
    tags:
      - players
      - leaderboard
      - reports
  - name: browser-pages
    x-displayName: Brauserilehed
    tags:
      - pages
paths:
  /info:
    get:
      operationId: getServerInfo
      tags:
        - server-info
      summary: Too serveri nimi, versioonid, pordid ja registreerumise reeglid
      description: |-
        Mida klient vajab enne sisselogimist või ühendumist: serveri nimi ja ID, WebSocketi protokolli versioonid ja WebSocketi asukoht, registreerumise reeglid, ametlikud ajakontrollid ning piirangud, mida klient saab enne vormi saatmist kontrollida.

        **Piirangud:** ainult aadressipõhine kiht.
      security: []
      responses:
        '200':
          description: Serveri kirjeldus.
          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`: aadressipõhine kiht.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /auth/register:
    post:
      operationId: registerAccount
      tags:
        - auth
      summary: Loo konto
      description: |-
        Loob konto: pärast seda, kui selle e-posti aadress on sinna saadetud lingiga kinnitatud, või e-posti kinnituseta kohe.

        **E-posti kinnitusega** (`REQUIRE_EMAIL_VERIFICATION`, vaikimisi): 202 `verification_sent`. Kontot veel ei ole: registreerumine ootab 24 tundi ja hoiab seni oma kasutajanime kinni. Aadressile saadetakse nende 24 tunni jooksul kehtiv link, kõige rohkem üks aadressi kohta iga 5 minuti järel (`POST /auth/verify-email/resend` saadab uue); konto luuakse ja selle aadress kinnitatakse lingi kasutamisel (lehe `/verify-email` nupp) ning mängija saab seejärel sisse logida. Seni vastatakse selle kasutajanimega sisselogimisele `invalid_credentials` nagu tundmatu konto puhul ja avalikku profiili ei ole. Uus registreerumine sama aadressiga asendab ootel registreerumise. Vastus on sama ka siis, kui aadressi kasutab juba mõni teine konto: siis linki ei saadeta, selle omanik saab hoopis teavituse (kõige rohkem ühe tunnis) ja kasutajanimi hoitakse samamoodi kinni, nii et miski ei reeda, kas aadressil on konto. Registreerumine, mille linki ei kasutatud, kustutatakse 24 tunni pärast ja selle kasutajanimi vabaneb.

        **E-posti kinnituseta** (`REQUIRE_EMAIL_VERIFICATION=false`): 201 `ready`, konto luuakse kohe ja sellega saab sisse logida.

        **Reeglid.** `username`: `USERNAME_MIN` kuni `USERNAME_MAX` märki (3 kuni 20), tähed, numbrid, `_` ja `-`, algab tähe või numbriga (`GET /info` annab need väljal `limits`); reserveeritud nimed (`admin`, `moderator`, `deleted`...) ja mõned eesliited lükatakse tagasi; unikaalne tõstutundetult. `email`: otstest eemaldatakse tühikud ja see salvestatakse väiketähtedega, lihtne ASCII punkti sisaldava domeeniga. `password`: vähemalt `PASSWORD_MIN_LENGTH` (10) märki ja kõige rohkem 256 baiti UTF-8-s; see ei tohi sisaldada kasutajanime ega e-posti aadressi kohalikku osa ega olla levinud parool.

        **Vead**, kontrollitakse selles järjekorras: 403 `registration_closed`; 400 `invalid_username`; 400 `invalid_email`; 400 `weak_password`; 409 `username_taken`; 428 `pow_required`; paroolide räsimise järjekorra vead; 409 `email_taken` (ainult e-posti kinnituseta).

        **Piirangud:** `auth`, seejärel `auth_register` (10 registreerumist tunnis kliendi kohta). **Töötõend:** alati, kui `POW_REGISTER_BITS` on suurem kui 0.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
            examples:
              first:
                summary: Esimene katse, ilma töötõendita
                value:
                  username: alice
                  email: alice@example.org
                  password: correct horse battery
              withPow:
                summary: Uuesti saadetud koos töötõendiga
                value:
                  username: alice
                  email: alice@example.org
                  password: correct horse battery
                  pow:
                    challenge: eyJ2IjoxLCJlIjoicmVnaXN0ZXIiLCJiIjoxOCwieCI6MTc5MDg4Mjk5MTIwMH0.6Ku_D7kw3S69OYxj-9KxsXszRCwz6ECnOQ-B6-v2erM
                    nonce: '123456'
      responses:
        '201':
          description: Konto on loodud ja sellega saab sisse logida (selles serveris e-posti kinnitust pole).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    const: ready
              example:
                status: ready
        '202':
          description: Registreerumine ootab oma kinnituslingi kasutamist (või aadressil on juba konto; vastus seda ei ütle).
          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` (koos väljaga `field`), `invalid_json`, `invalid_username`, `invalid_email` või `weak_password` koos väljaga `reason`: `too_short`, `too_long`, `contains_username`, `contains_email` või `too_common`.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`registration_closed`: registreerumine on selles serveris suletud (`GET /info` ütleb `registration: closed`).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`username_taken`: kasutajanimi on mõnel kontol või selle hoiab kinni mõne teise aadressi ootel registreerumine. `email_taken`: aadressi kasutab teine konto (ainult e-posti kinnituseta).'
        '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`: piirang `auth` või `auth_register` või paroolide räsimise järjekord.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (paroolide räsimise järjekord või andmebaas jäi lukustatuks), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /auth/login:
    post:
      operationId: logIn
      tags:
        - auth
      summary: Logi sisse parooliga
      description: |-
        Logib sisse kasutajanime või e-posti aadressi ja parooliga. Võimalikke vastuseid on kaks, mõlemad olekuga 200: seanss või, kui kaheastmeline autentimine on sees, teine samm, mis tuleb 5 minuti jooksul lõpetada otspunktiga `POST /auth/login/mfa`.

        Tundmatu konto, vale parool ja paroolita konto saavad sama vastuse sama aja järel: 401 `invalid_credentials`. Konto kontrollid (`banned`, `email_unverified`) tehakse alles pärast õiget parooli. Alates `AUTH_FAILURES_PER_ACCOUNT` (5) ebaõnnestumisest ühe sisselogimisnimega peab iga katse ootama (429 `too_many_attempts`).

        **Piirang:** `auth`. **Töötõend:** ainult ebaõnnestunud sisselogimiste laine ajal (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: Seanss või kaheastmelise autentimisega sisselogimise teine samm.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginAnswer'
              examples:
                session:
                  summary: Sisse logitud
                  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: Kaheastmeline autentimine on sees
                  value:
                    mfaRequired: true
                    mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                    expiresIn: 300
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_credentials`: tundmatu konto, vale parool või paroolita konto.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: 'Ainult pärast õiget parooli: `banned` koos väljaga `until` (Unixi aeg, ms; alalise peatamise korral `null`) või `email_unverified` (vanem konto, mille aadress pole veel kinnitatud).'
        '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` (selle sisselogimisnime ebaõnnestumiste viivitus) või `rate_limited` (piirang `auth`, paroolide räsimise järjekord).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (paroolide räsimise järjekord või andmebaas jäi lukustatuks), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /auth/login/mfa:
    post:
      operationId: logInWithSecondFactor
      tags:
        - auth
      summary: Lõpeta sisselogimine teise teguriga
      description: |-
        Kaheastmelise autentimisega sisselogimise teine samm pärast seda, kui `POST /auth/login` või Google'iga sisselogimine (`POST /auth/sso/google/finish` või `POST /auth/sso/google/link`) vastas `mfaRequired`. Saatke `code` (6-kohaline autentimisrakenduse kood või taastekood) või `recoveryCode`. Siin kasutatud taastekood on ära kulutatud.

        Üks samm aktsepteerib kõige rohkem 5 valet koodi; samm lõpeb ka siis, kui parool lähtestatakse või seda muudetakse. Pärast Google'i sidumise paroolisammu salvestatakse seos alles siis, kui kood siin kontrolli läbib.

        **Piirangud:** `auth` ja konto kohta kõige rohkem `AUTH_MFA_PER_ACCOUNT` (10) koodi 15 minuti jooksul, mis tahes aadressilt; sellest üle koodi ei kontrollita, nii et taastekood ei kulu.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MfaLoginRequest'
            examples:
              authenticator:
                summary: Autentimisrakenduse kood
                value:
                  mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                  code: '123456'
              recovery:
                summary: Taastekood
                value:
                  mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                  recoveryCode: j7v5-3ezx-zn
      responses:
        '200':
          description: Sisse logitud.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionAnswer'
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_request`: ei saadetud ei välja `code` ega `recoveryCode` või keha rikub skeemi; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_mfa_token`: samm aegus, on kasutatud või lõppes pärast 5 valet koodi või parool on pärast esimest sammu lähtestatud või seda on muudetud (logige uuesti sisse). `invalid_code`: vale või juba kasutatud kood.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`banned` (koos väljaga `until`), `email_unverified`.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_already_linked`: ainult pärast Google''i sidumise paroolisammu – Google''i konto seoti vahepeal teise kontoga.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: ainult pärast Google''i sidumise paroolisammu – konto muutus vahepeal; alustage sisselogimist uuesti.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts`: konto ebaõnnestumiste viivitus või selle viimase 15 minuti `AUTH_MFA_PER_ACCOUNT` koodi on ära kasutatud. `rate_limited`: piirang `auth`.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` koos `retryAfter: 1`: andmebaas jäi lukustatuks (Google''i sidumise sammu järel ei seotud midagi: alustage sisselogimist uuesti). `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /auth/logout:
    post:
      operationId: logOut
      tags:
        - sessions
      summary: Logi see seanss välja
      description: |-
        Tühistab kasutatud tokeni seansi. Sellega avatud WebSocket suletakse. Keha puudub (või `{}`).

        **Piirang:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Välja logitud.
          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`: muu keha kui `{}`; `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`: piirang `sessions` või konto kvoot.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (andmebaas jäi lukustatuks), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /auth/logout-all:
    post:
      operationId: logOutEverywhere
      tags:
        - sessions
      summary: Logi kõik seansid välja
      description: |-
        Tühistab konto kõik seansid, sealhulgas selle. Nendega avatud WebSocketid suletakse. Keha puudub (või `{}`).

        **Piirang:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Kõik seansid on välja logitud.
          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`: muu keha kui `{}`; `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`: piirang `sessions` või konto kvoot.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (andmebaas jäi lukustatuks), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /auth/verify-email/resend:
    post:
      operationId: resendVerificationEmail
      tags:
        - auth
      summary: Saada kinnituslink uuesti
      description: |-
        Saadab e-posti kinnituslingi uuesti. Vastus on olenemata aadressist 202 `accepted`, nii et see ei reeda kunagi, kas aadressi kasutab mõni konto või registreerumine.

        Päring toimib aadressi kohta kõige rohkem kord 5 minuti jooksul. Selle aadressiga ootel registreerumine saab oma 24 tundi uuesti, olenemata sellest, kas aadressi kasutab mõni teine konto, nii et selle kasutajanimi jääb mõlemal juhul sama kauaks kinni. Link saadetakse ainult sellele registreerumisele, kui aadressil pole kontot (uus, 24 tundi kehtiv link asendab eelmise), või selle aadressiga aktiivsele kinnitamata kontole.

        **Piirangud:** `auth`, seejärel `auth_mail` (10 tunnis kliendi kohta).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmailRequest'
            example:
              email: alice@example.org
      responses:
        '202':
          description: Vastu võetud (olenemata aadressist).
          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`: piirang `auth` või `auth_mail` (olenemata aadressist).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` koos `retryAfter: 1`: andmebaas jäi lukustatuks konto otsimise või kinnitamata konto lingi salvestamise ajal (ootel registreerumise uuendamine tehakse võimaluse korral ega põhjusta kunagi päringu ebaõnnestumist); aadressi 5 minutit algavad sellegipoolest, nii et nende jooksul tehtud uus päring ei tee midagi. `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /auth/password/forgot:
    post:
      operationId: requestPasswordReset
      tags:
        - auth
      summary: Saada parooli lähtestamise link
      description: |-
        Saadab ühe tunni kehtiva parooli lähtestamise lingi, mis avab lehe `/reset-password`. Vastus on olenemata aadressist 202 `accepted`. Link saadetakse ainult aktiivsele kontole, kõige rohkem kord 5 minuti jooksul aadressi kohta. Ainult Google'iga sisse logiv konto määrab nii oma esimese parooli.

        Parooli taastamisel on API kõige rangemad piirangud, mida kõiki arvestatakse kogu serveri peale: 3 päringut tunnis (`AUTH_FORGOT_PER_HOUR`) ja 10 päringut 24 tunni jooksul (`AUTH_FORGOT_PER_DAY`) kliendi kohta (IPv4-aadress või IPv6 /64), IPv6 /48 kohta 3 korda rohkem, lisaks piirangule `auth` (20 päringut 10 minuti jooksul) ja ühele e-kirjale aadressi kohta iga 5 minuti järel. Ei 202 ega 429 reeda, kas aadressi kasutab mõni konto. Uue parooli määramisel on oma piirang (`auth_reset`).

        **Piirangud:** `auth`, `auth_forgot`, seejärel `auth_forgot_day`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmailRequest'
            example:
              email: alice@example.org
      responses:
        '202':
          description: Vastu võetud (olenemata aadressist).
          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`: piirang `auth`, `auth_forgot` või `auth_forgot_day` (olenemata aadressist).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` koos `retryAfter: 1`: andmebaas jäi lukustatuks konto otsimise või lingi salvestamise ajal; aadressi 5 minutit algavad sellegipoolest, nii et nende jooksul tehtud uus päring linki ei saada. `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /auth/password/reset:
    post:
      operationId: resetPassword
      tags:
        - auth
      summary: Määra lähtestamislingi tokeniga uus parool
      description: |-
        Määrab lähtestamislingi tokeniga uue parooli (leht `/reset-password` teeb sama). Uus parool peab vastama registreerumise reeglitele.

        Lähtestamisel on ühes transaktsioonis (kõik või mitte ükski) järgmised mõjud: see tühistab kõik seansid ja tühistab ootel e-posti aadressi muutmise; konto teised lähtestamislingid lakkavad töötamast; aadress loetakse kinnitatuks (link tõestas selle); omanik saab e-kirja. Kaheastmelist autentimist see ei puuduta. Pärast paroolide räsimise järjekorra viga, 503 või 500 pole midagi muutunud: vana parool ja seansid jäävad alles ning link jääb kehtima.

        **Piirangud:** `auth`, seejärel `auth_reset` (10 tunnis kliendi kohta, 30 IPv6 /48 kohta, ühine lehega: iga katse räsib parooli).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasswordResetRequest'
            example:
              token: A0ATfmXCDKFC8UkSxLJCD85K7au5nMIrZ2P3K-1VWQg
              newPassword: a much better passphrase
      responses:
        '200':
          description: Parool on muudetud ja kõik seansid on välja logitud.
          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`: link on kehtetu, kasutatud või aegunud või saadeti aadressile, mida kontol enam pole. `weak_password` (koos väljaga `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`: piirang `auth` või `auth_reset` või paroolide räsimise järjekord.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: paroolide räsimise järjekord või andmebaas jäi lukustatuks (`retryAfter: 1`, midagi ei muutunud). `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /auth/sso/google/start:
    post:
      operationId: startGoogleSignIn
      tags:
        - google-sign-in
      summary: Alusta Google'iga sisselogimist
      description: |-
        Alustab Google'iga sisselogimise katset PKCE väljakutse ja mängu kuulaja pordiga aadressil `127.0.0.1`. Vastus annab süsteemi brauseris avatava Google'i URL-i ja katse väärtuse `state`; katse kehtib 10 minutit.

        `authUrl` sisaldab parameetreid `client_id`, `redirect_uri` (koostatud väärtusest `redirectPort` ja serveri päritolutunnusest), `response_type=code`, `scope=openid email profile`, `state`, `nonce`, `code_challenge` (serveri enda, Google'i jaoks mõeldud verifitseerija S256) koos `code_challenge_method=S256` ning `prompt=select_account`. Mäng kontrollib seda enne avamist (vt jaotise kirjeldust).

        **Piirang:** `sso_start`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SsoStartRequest'
            example:
              codeChallenge: 6e7diXEYxG7OTYw7STfNOltEeLxAilPthC_txzaE0xA
              redirectPort: 51234
      responses:
        '200':
          description: Katse.
          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`: see server ei paku Google''iga sisselogimist.'
        '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`: piirang `sso_start`.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (andmebaas jäi lukustatuks), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /auth/sso/google/web/start:
    post:
      operationId: startGoogleWebSignIn
      tags:
        - google-sign-in
      summary: Alusta Google'iga sisselogimist veebisaidil
      description: |-
        Alustab veebisaidi Google'iga sisselogimise katset selle lehe PKCE väljakutsega. Vastus on sama mis mängule: Google'i URL, millele liikuda, ja katse väärtus `state`; katse kehtib 10 minutit. Saadaval, kui `GET /info` ütleb `sso.googleWeb: true`.

        `authUrl` sisaldab veebisaidi parameetreid `client_id` ja `redirect_uri=GOOGLE_WEB_REDIRECT_URI` ning ülejäänud parameetreid samamoodi nagu mängu puhul. Leht kontrollib seda enne sellele liikumist (vt jaotise kirjeldusest lõiku **Veebisaidil**).

        **Piirang:** `sso_start`, ühine mängu alustamisega.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SsoWebStartRequest'
            example:
              codeChallenge: 6e7diXEYxG7OTYw7STfNOltEeLxAilPthC_txzaE0xA
      responses:
        '200':
          description: Katse.
          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`: puuduv või vigane `codeChallenge` või mis tahes muu väli (sh `redirectPort`).'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: see server ei paku Google''iga sisselogimist või ei paku seda veebisaitidele.'
        '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`: piirang `sso_start` (ühine mängu alustamisega).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (andmebaas jäi lukustatuks), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /auth/sso/google/finish:
    post:
      operationId: finishGoogleSignIn
      tags:
        - google-sign-in
      summary: Anna Google'i vastus serverile üle
      description: |-
        Annab serverile üle väärtused `code` ja `state`, mille Google saatis mängu kuulajale või veebisaidi lehele, koos katse ID ja PKCE verifitseerijaga. Katse ID on ilma verifitseerijata kasutu ja kood on kasutu ilma serveri enda PKCE verifitseerija ja kliendi salajase võtmeta.

        Server kontrollib katset (tundmatu, kasutatud või aegunud: 410), seejärel verifitseerijat (vale verifitseerija korral jääb katse kasutatavaks), seejärel kasutab katse ühe korra ära, kontrollib väärtusi `state` ja `iss`, vahetab koodi Google'iga ja kontrollib ID-tokenit. Vastus (200) on üks järgmistest:

        - `{ token, expiresAt, user }`: sisse logitud seotud kontole;
        - `{ mfaRequired, mfaToken, expiresIn }`: jätkake otspunktiga `POST /auth/login/mfa`;
        - `{ needsUsername, ssoTicket, suggestedUsername }`: uus konto; jätkake 10 minuti jooksul otspunktiga `POST /auth/sso/complete`. `suggestedUsername` tuleb Google'i nimest või aadressist ja on `""`, kui miski ei sobi;
        - `{ needsPassword, linkTicket, username, expiresIn }`: selle aadressiga kontol on parool; jätkake otspunktiga `POST /auth/sso/google/link`. Midagi pole veel seotud.

        **Piirang:** `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: Seanss, teine samm või esimese Google'iga sisselogimise järgmine samm.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SsoFinishAnswer'
              examples:
                session:
                  summary: Sisse logitud seotud kontole
                  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: Kaheastmeline autentimine on sees
                  value:
                    mfaRequired: true
                    mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                    expiresIn: 300
                needsUsername:
                  summary: Uus mängija
                  value:
                    needsUsername: true
                    ssoTicket: sso_gYIKKhwnz1AqWfFxOC3R-ZvCACw06q74fEMtGEOJQWU
                    suggestedUsername: alice
                needsPassword:
                  summary: Aadressi kasutab parooliga konto
                  value:
                    needsPassword: true
                    linkTicket: sso_ZfEI6iZjePTHbnbevupaD9O7GjBCzcL-GJu2XGOdFGc
                    username: alice
                    expiresIn: 600
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_verifier`: verifitseerija ei vasta katse väljakutsele (katse jääb kasutatavaks). `sso_email_unverified`: Google pole aadressi kinnitanud. `registration_closed`. `account_disabled`. Ainult seotud konto puhul: `banned` (koos väljaga `until`), `email_unverified`.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: see server ei paku Google''iga sisselogimist.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_account_exists`: aadressi kasutab aktiivne paroolita konto (seda ei nimetata).'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: tundmatu, kasutatud või aegunud katse.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: piirang `sso_finish`.'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/BadGateway'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (andmebaas jäi lukustatuks), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /auth/sso/google/link:
    post:
      operationId: linkGoogleAccount
      tags:
        - google-sign-in
      summary: Seo Google olemasoleva kontoga selle parooli abil
      description: |-
        Kui `finish` vastas `needsPassword`: seob Google'i olemasoleva kontoga selle konto parooli abil, mis sisestatakse mängus. Vastus (200) on seanss (seos salvestatakse ja mängija logitakse sisse) või, kui kaheastmeline autentimine on sees, `{ mfaRequired, mfaToken, expiresIn }`: jätkake otspunktiga `POST /auth/login/mfa` ja seos salvestatakse alles siis, kui kood seal kontrolli läbib.

        Vale parooli korral jääb pilet kasutatavaks, kokku 5 katseks. Katse arvestatakse vahetult enne parooli kontrollimist: 429 `too_many_attempts` või 428 `pow_required` ei võta ühtegi katset, paroolide räsimise järjekorra keeldumine (503 `server_busy`, 429 `rate_limited`) aga on ühe katse juba võtnud ja läheb arvesse konto ebaõnnestumisena. Ebaõnnestumiste viivitus kasutab sama loendurit mis `POST /auth/login`.

        **Piirang:** `auth` (koos selle IPv6 /48 loendusega). **Töötõend:** nagu otspunktil `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: Seanss või sisselogimise teine samm.
          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`: vale parool; pilet jääb kehtima, kokku 5 katseks.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`banned` (koos väljaga `until`), ainult pärast õiget parooli.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: see server ei paku Google''iga sisselogimist.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_already_linked`: Google''i konto seoti vahepeal teise kontoga.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: tundmatu, kasutatud või aegunud pilet, 5. vale parool, konto, mille olek või aadress on pärast sammu `finish` muutunud, või konto, mille olek, aadress, parool või kaheastmeline autentimine muutus seose salvestamise ajal. Alustage sisselogimist uuesti.'
        '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` (konto ebaõnnestumiste viivitus) või `rate_limited` (piirang `auth`, paroolide räsimise järjekord).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: paroolide räsimise järjekord või andmebaas jäi lukustatuks (`retryAfter: 1`, midagi ei seotud). `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /auth/sso/complete:
    post:
      operationId: completeGoogleSignUp
      tags:
        - google-sign-in
      summary: Loo konto esimesel Google'iga sisselogimisel
      description: |-
        Kui `finish` vastas `needsUsername`: loob valitud kasutajanimega konto (kehtivad registreerumise reeglid) ja logib selle sisse. Kontol pole parooli: `hasPassword` on `false` ja „Unustasite parooli?“ (`POST /auth/password/forgot`) annab sellele parooli.

        **Piirang:** `auth` (koos selle IPv6 /48 loendusega).
      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: Konto on loodud ja sisse logitud.
          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`: see server ei paku Google''iga sisselogimist.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`username_taken` (kasutajanimi on mõnel kontol või selle hoiab kinni mõne teise aadressi ootel registreerumine), `sso_already_linked`, `email_taken`.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: tundmatu, kasutatud või aegunud pilet.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: piirang `auth`.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (andmebaas jäi lukustatuks), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /auth/sessions:
    get:
      operationId: listSessions
      tags:
        - sessions
      summary: Loetle sisselogitud seadmed
      description: |-
        Konto aktiivsed seansid (sisselogitud seadmed), viimati kasutatud esimesena. `current` märgib seanssi, mis päringu teeb. `lastSeenAt` uuendatakse mitte sagedamini kui iga 5 minuti järel (hiljem, kui server on tööga kaugele maha jäänud); `expiresAt` on absoluutne lõpp ja jõudeoleku piirang võib seansi varem lõpetada.

        **Piirang:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Aktiivsed seansid.
          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`: piirang `sessions` või konto kvoot.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (andmebaas jäi lukustatuks), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /auth/sessions/{id}:
    delete:
      operationId: revokeSession
      tags:
        - sessions
      summary: Logi üks seade välja
      description: |-
        Logib välja konto ühe seansi; välja võib logida ka praeguse seansi. Sellega avatud WebSocket suletakse. Keha puudub (või `{}`).

        **Piirang:** `sessions`.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/SessionId'
      responses:
        '200':
          description: Seanss on välja logitud.
          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`: muu keha kui `{}` või `id`, mis pole kehtiv URL-kodeering; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: sellel kontol pole selle ID-ga aktiivset seanssi.'
        '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`: piirang `sessions` või konto kvoot.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (andmebaas jäi lukustatuks), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /account/me:
    get:
      operationId: getAccount
      tags:
        - account
      summary: Too konto, selle reitingud ja aktiivsed sanktsioonid
      description: |-
        Konto nii, nagu selle mängija seda näeb: konto vaade (ka iga sisselogimisvastuse `user`), üks reitingukirje iga kategooria kohta, milles mängija on reitinguga partiisid mänginud, aktiivsed sanktsioonid ja peatamine. Sohitõrje usaldusväärsuse taset ei näidata kunagi.

        **Piirang:** `account`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Konto.
          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` või `invalid_token` (ka siis, kui konto on kustutatud).'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: piirang `account` või konto kvoot.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (andmebaas jäi lukustatuks), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /account/preferences:
    put:
      operationId: updatePreferences
      tags:
        - account
      summary: Võta otseseid väljakutseid vastu või lükka need tagasi
      description: |-
        Määrab, kas teised mängijad võivad seda mängijat nime järgi välja kutsuda. Väärtusega `none` lükatakse otsesed väljakutsed tagasi: väljakutsujale öeldakse, et mängija pole saadaval. Uuesti autentimist ei nõuta.

        **Piirang:** `account`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Preferences'
            example:
              acceptChallenges: none
      responses:
        '200':
          description: Praegu kehtivad eelistused.
          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`: piirang `account` või konto kvoot.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (andmebaas jäi lukustatuks), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /account/password:
    post:
      operationId: changePassword
      tags:
        - account
      summary: Muuda parooli
      description: |-
        Muudab parooli. Vaja on praegust parooli, kuid mitte teist tegurit, isegi kui kaheastmeline autentimine on sisse lülitatud. Uus parool peab vastama registreerumise reeglitele (kontrollitakse pärast praegust parooli).

        Muudatusel on ühes transaktsioonis (kõik või mitte ükski) järgmised tagajärjed: see tühistab kõik teised seansid (see seanss jääb sisse logituks), tühistab ootel e-posti aadressi muutmise, muudab konto parooli lähtestamislingid kehtetuks ja saadab omanikule e-kirja.

        **Uuesti autentimine:** ainult parool. **Piirangud:** `reauth`, seejärel `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: Parool on muudetud.
          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` (ainult Google''iga sisse logiv konto), `weak_password` (koos väljaga `reason`), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password`: vale praegune parool või parooli lähtestamine või muutmine, mis toimus päringu kontrollimise ajal.'
        '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` (konto ebaõnnestunud uuesti autentimised) või `rate_limited` (piirang `reauth` või `reauth_user`, konto kvoot, paroolide räsimise järjekord).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: paroolide räsimise järjekord või andmebaas jäi lukustatuks (`retryAfter: 1`, midagi ei muutunud). `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /account/mfa/totp/setup:
    post:
      operationId: startTotpSetup
      tags:
        - two-step-verification
      summary: Alusta kaheastmelise autentimise sisselülitamist
      description: |-
        Salvestab autentimisrakenduse jaoks uue ootel salajase võtme, mis asendab iga varasema ootel võtme, ja tagastab selle autentimisrakenduse jaoks (tekstina ja `otpauth://` URI-na, mida näidata QR-koodina). Kaheastmeline autentimine pole veel sisse lülitatud: `POST /account/mfa/totp/enable` lülitab selle sisse selle võtme koodiga.

        **Uuesti autentimine:** ainult parool. **Piirangud:** `reauth`, seejärel `reauth_user`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasswordOnlyRequest'
            example:
              password: correct horse battery
      responses:
        '200':
          description: Ootel salajane võti.
          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` (ainult Google''iga sisse logiv konto), `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` (kontrollitakse enne parooli).'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (konto ebaõnnestunud uuesti autentimised) või `rate_limited` (piirang `reauth` või `reauth_user`, konto kvoot, paroolide räsimise järjekord).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (paroolide räsimise järjekord või andmebaas jäi lukustatuks), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /account/mfa/totp/enable:
    post:
      operationId: enableTotp
      tags:
        - two-step-verification
      summary: Lõpeta kaheastmelise autentimise sisselülitamine ja saa taastekoodid
      description: |-
        Lülitab kaheastmelise autentimise sisse ootel salajase võtme koodiga (täpselt 6 numbrit). Siin parooli ei küsita; see anti seadistamisel. Vastuses on 10 taastekoodi, mida näidatakse ainult see üks kord.

        **Piirangud:** `reauth`, seejärel `reauth_user`; vale kood läheb arvesse konto uuesti autentimise ebaõnnestumiste hulka.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TotpEnableRequest'
            example:
              code: '123456'
      responses:
        '200':
          description: Kaheastmeline autentimine on sisse lülitatud.
          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` ei ole täpselt 6 numbrit või keha rikub skeemi; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_code`: vale kood (kontrollige seadme kellaaega).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`mfa_already_enabled`; `mfa_setup_required`: ootel salajast võtit pole (kutsuge kõigepealt välja `POST /account/mfa/totp/setup`).'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (konto ebaõnnestunud uuesti autentimised) või `rate_limited` (piirang `reauth` või `reauth_user`, konto kvoot).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (andmebaas jäi lukustatuks), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /account/mfa/totp/disable:
    post:
      operationId: disableTotp
      tags:
        - two-step-verification
      summary: Lülita kaheastmeline autentimine välja
      description: |-
        Lülitab kaheastmelise autentimise välja. Salajane võti ja taastekoodid kustutatakse ning omanik saab e-kirja.

        **Uuesti autentimine:** parool ja autentimisrakenduse kood või taastekood (nõutav on üks väljadest `code` või `recoveryCode`). **Piirangud:** `reauth`, seejärel `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: Kaheastmeline autentimine on välja lülitatud.
          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` (ainult Google''iga sisse logiv konto), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`mfa_code_required` (pole välja `code` ega `recoveryCode`; kontrollitakse enne parooli), `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` (ebaõnnestunud uuesti autentimised või sellel kontol on proovitud liiga palju koode) või `rate_limited` (piirang `reauth` või `reauth_user`, konto kvoot, paroolide räsimise järjekord).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (paroolide räsimise järjekord või andmebaas jäi lukustatuks), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /account/mfa/recovery-codes:
    post:
      operationId: regenerateRecoveryCodes
      tags:
        - two-step-verification
      summary: Asenda taastekoodid
      description: |-
        Asendab taastekoodid 10 uue koodiga; vanad lakkavad töötamast.

        **Uuesti autentimine:** parool ja autentimisrakenduse kood väljal `code` (taastekood lükatakse tagasi vastusega 403 `invalid_code`). **Piirangud:** `reauth`, seejärel `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: Uued taastekoodid, mida näidatakse ainult see üks kord.
          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` (ainult Google''iga sisse logiv konto), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password`, `mfa_code_required`, `invalid_code` (ka taastekoodi korral).'
        '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` (ebaõnnestunud uuesti autentimised või sellel kontol on proovitud liiga palju koode) või `rate_limited` (piirang `reauth` või `reauth_user`, konto kvoot, paroolide räsimise järjekord).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (paroolide räsimise järjekord või andmebaas jäi lukustatuks), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /account/email:
    post:
      operationId: changeEmail
      tags:
        - email-change
      summary: Muuda e-posti aadressi
      description: |-
        Muudab konto e-posti aadressi. Väärtuselt `newEmail` eemaldatakse algus- ja lõputühikud ning see teisendatakse väiketähtedeks, seejärel kontrollitakse seda nagu registreerumisel antud aadressi.

        **E-posti kinnitusega** (vaikimisi): 202 `verification_sent`. Uuele aadressile saadetakse 24 tundi kehtiv link; see avab lehe `/confirm-email-change` ja aadress muutub alles siis, kui mängija vajutab sellel lehel nuppu. Seni näitab `GET /account/me` uut aadressi väljal `pendingEmail`. Uus päring asendab ootel päringu; parooli muutmine või lähtestamine tühistab selle. Antud uuele aadressile saadetakse kõige rohkem üks link iga 5 minuti jooksul, ükskõik kes seda küsib (päring juba ootel oleva muutmise kohta säilitab varem saadetud lingi, mis jääb kehtima); vastus on sama. Praegune aadress saab teavituse, et taotleti muutmist varjatud aadressile (`a***@example.org`). Vastus ja `pendingEmail` on samad, kui mõni teine konto juba kasutab uut aadressi: siis linki ei saadeta, nii et see muutmine ei jõua kunagi lõpule, ja selle aadressi omanik saab hoopis teavituse (kõige rohkem ühe tunnis).

        Kui link kinnitatakse, aadress muutub ja loetakse kinnitatuks, seadmed jäävad sisse logituks, varem saadetud lingid (kinnitus, parooli lähtestamine, muud muutmised) lakkavad töötamast ja endist aadressi teavitatakse, kusjuures uus aadress on varjatud.

        **E-posti kinnituseta** (`REQUIRE_EMAIL_VERIFICATION=false`): aadress muutub kohe (200 `email_changed`) ja endist aadressi teavitatakse. Kui mõni teine konto kasutab seda aadressi, on vastus 409 `email_taken` ja selle aadressi omanik saab teavituse.

        **Uuesti autentimine:** parool ja kaheastmelise autentimise korral autentimisrakenduse kood või taastekood. `invalid_email` ja `same_email` kontrollitakse enne parooli, seega ei loeta neid ebaõnnestumiseks. **Piirangud:** `reauth`, seejärel `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: Aadress muutus kohe (selles serveris pole e-posti kinnitust).
          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: Uus aadress salvestatud kujul.
              example:
                status: email_changed
                email: alice.new@example.org
        '202':
          description: Uuele aadressile saadeti kinnituslink (või kuulub aadress teisele kontole; vastus seda ei ütle).
          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` (konto praegune aadress), `password_not_set` (ainult Google''iga sisse logiv konto), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password` (ka siis, kui parooli muutmine või lähtestamine jõudis päringust ette: linki ei saadeta), `mfa_code_required`, `invalid_code`.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`email_taken`: ainult e-posti kinnituseta.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (ebaõnnestunud uuesti autentimised või sellel kontol on proovitud liiga palju koode) või `rate_limited` (piirang `reauth` või `reauth_user`, konto kvoot, paroolide räsimise järjekord).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: paroolide räsimise järjekord või andmebaas jäi lukustatuks (`retryAfter: 1`: midagi ei muutunud ja sama päringu võib uuesti saata). `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /account/export:
    post:
      operationId: exportAccountData
      tags:
        - data-export
      summary: Laadi konto andmed alla
      description: |-
        Kõik, mida server konto kohta hoiab, ühe salvestatava JSON-failina (`format` `scacelith-account-export`, `version` 1). Eksport salvestab turvasündmuse (`account_exported`). Dokumendi massiiv `notes` ütleb mängijale lihtsas inglise keeles, mis on välja jäetud.

        **Ekspordis pole kunagi:** parooli räsi, kaheastmelise autentimise salajast võtit ega taastekoode; ühtki seansi- ega lingitokenit ega selle räsi; sohitõrje andmeid (usaldusväärsuse taset ja skoori, anomaaliaid, partiide analüüsi, teate kaalu); teateid, mida teised mängijad on mängija kohta teinud; moderaatorite identiteeti; teiste mängijate privaatandmeid (vastased on näidatud avaliku nime ja reitinguga, miski ei ütle, kas mõnda teist mängijat on sanktsioneeritud, ja ühtki IP-aadressi, mis võib kuuluda teisele isikule, ei lisata).

        **Uuesti autentimine:** parool ja kaheastmelise autentimise korral autentimisrakenduse kood või taastekood. **Piirangud:** `account_export` (5 tunnis mängija kohta kogu serveri peale; iga katse läheb arvesse, ka ebaõnnestunud; kontrollitakse esimesena), seejärel `reauth` ja `reauth_user`. **Ajapiirang:** 60 sekundit.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReauthCredentials'
            example:
              password: correct horse battery
              code: '123456'
      responses:
        '200':
          description: Ekspordidokument manusena.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            Content-Disposition:
              description: '`attachment; filename="scacelith-account-<username>.json"`. Kasutajanimes muutuvad kõik märgid peale tähtede, numbrite, `_`, `.` ja `-` märgiks `_`.'
              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` (ainult Google''iga sisse logiv konto), `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` (ebaõnnestunud uuesti autentimised või sellel kontol on proovitud liiga palju koode) või `rate_limited` (piirang `account_export`, `reauth` või `reauth_user`, konto kvoot, paroolide räsimise järjekord).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (andmebaas jäi lukustatuks, koos päisega `Retry-After: 1`), `server_busy` (paroolide räsimise järjekord), `timeout` (60 sekundit). Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /account/delete:
    post:
      operationId: deleteAccount
      tags:
        - account-deletion
      summary: Kustuta konto
      description: |-
        Kustutab konto; seda ei saa tagasi võtta.

        - Kõik seansid tühistatakse kohe: tokenile vastatakse sellest hetkest alates 401 `invalid_token`.
        - Kasutajanimeks saab `deleted#<id>`, nii kontos kui ka igas partiikirjes.
        - Kustutatakse: e-posti aadress, parooli räsi, kaheastmelise autentimise salajane võti ja taastekoodid, seansid ja lingitokenid, Google'i seos, sohitõrje usaldusväärsuse kirje ning turvasündmustega salvestatud IP-aadressid.
        - Reitingud ja partiid säilitatakse. Partiid jäävad anonüümse nime all loetavaks ja `GET /players/{username}` vastab endise nime korral 404.

        **Uuesti autentimine:** parool ja kaheastmelise autentimise korral autentimisrakenduse kood või taastekood. **Piirangud:** `reauth`, seejärel `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: Konto on kustutatud.
          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` (ainult Google''iga sisse logiv konto), `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` (ebaõnnestunud uuesti autentimised või sellel kontol on proovitud liiga palju koode) või `rate_limited` (piirang `reauth` või `reauth_user`, konto kvoot, paroolide räsimise järjekord).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (paroolide räsimise järjekord või andmebaas jäi lukustatuks), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /account/games:
    get:
      operationId: listAccountGames
      tags:
        - game-history
      summary: Loetle mängija partiid filtreeritult ja lehekülgedena
      description: |-
        Sisselogitud mängija partiid, uusimad esimesena, filtreeritult ja lehekülgedena, koos filtrile vastavate partiide arvuga. Kõik päringuparameetrid on valikulised ja tühi väärtus loetakse puuduvaks.

        Lehekülgede kaupa liikumine: andke eelmise lehekülje `next` parameetrina `before`; viimasel leheküljel on `next` väärtus `null`. `total` loendab filtrile vastavad partiid kõigil lehekülgedel kokku.

        **Piirang:** `account_games`.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/BeforeQuery'
        - $ref: '#/components/parameters/LimitQuery'
        - name: category
          in: query
          required: false
          description: Ametliku kategooria ID (`3+2` või `3%2B2`) või `custom` kõigi muu ajakontrolliga partiide jaoks.
          schema:
            type: string
            pattern: '^\s*([0-9]+[+ ][0-9]+|custom)\s*$'
          example: '3+2'
        - name: rated
          in: query
          required: false
          description: Ainult reitinguga (`true`) või reitinguta (`false`) partiid.
          schema:
            type: string
            enum:
              - 'true'
              - 'false'
        - name: result
          in: query
          required: false
          description: Ainult võidetud, kaotatud või viigiga lõppenud partiid mängija vaatenurgast. Katkestatud partiid on näha ainult ilma selle filtrita.
          schema:
            type: string
            enum:
              - win
              - loss
              - draw
      responses:
        '200':
          description: Üks lehekülg ajaloost.
          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` pole partii ID), `invalid_limit` või `invalid_filter` (`category`, `rated` või `result`), igaüks koos väljaga `field`, mis nimetab parameetri.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: piirang `account_games` või konto kvoot.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (andmebaas jäi lukustatuks; kehas `retryAfter: 1`, päist `Retry-After` pole), `server_busy` (seansi otsimisel oli andmebaas lukus), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /games/{id}:
    get:
      operationId: getGame
      tags:
        - games
      summary: Too partiikirje koos käikude ja kelladega
      description: |-
        Üks partiikirje koos käikude ja kelladega. Ilma tokenita või sellise mängija tokeniga, kes partiid ei mänginud, on vastus avalik. Kui tokeni mängija mängis seda partiid, lisatakse vastusele `you` ja `reportable`.

        **Piirang:** `public_read` (tokeniga mängija kohta, ilma tokenita kliendi kohta).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
      responses:
        '200':
          description: Partiikirje.
          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`: pole positiivne täisarv, millel on kuni 16 numbrit (alla 2^53); `invalid_request`: pole kehtiv URL-kodeering.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: saadeti token, mis ei kehti.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: sellist partiid pole.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: piirang `public_read` või konto kvoot.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (andmebaas jäi lukustatuks; kehas `retryAfter: 1`, päist `Retry-After` pole), `server_busy` (seansi otsimisel oli andmebaas lukus), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /games/{id}/pgn:
    get:
      operationId: getGamePgn
      tags:
        - games
      summary: Laadi partii alla PGN-failina
      description: |-
        Sama partii PGN-failina: üks partii, reavahetused `\n` ja käikude tekst alla 80 veeru pikkustel ridadel. Vastus on sama nii tokeniga kui ka ilma.

        Märgendid selles järjekorras: `Event` (`<SERVER_NAME> rated <category>` või `<SERVER_NAME> casual <category>`), `Site` (`SERVER_PUBLIC_HOST`), `Date` (alguskuupäev UTC järgi), `Round`, `White`, `Black`, `Result` (katkestatud partii puhul `*`), `UTCDate` ja `UTCTime` (algus), `WhiteElo` ja `BlackElo` (reitingud alguses või `-`), `WhiteRatingDiff` ja `BlackRatingDiff` (muutused, näiteks `+10` ja `-10`, igas reitinguga partiis; `+0`, kui reitingureeglid jätavad reitingu samaks; reitinguta, kohandatud või katkestatud partiis pole kumbagi), `TimeControl` (sekundites), `Termination` (PGN-standardi väärtus: `normal`; `time forfeit` – lipu langemine, ka siis, kui partii lõpeb viigiga; `abandoned`; `rules infraction` – teine reeglivastane käik või ausa mängu rikkumise eest määratud loobumiskaotus; `unterminated` – katkestatud partii), `PlyCount`, `ScacelithGameId` (ID kümnendarvuna).

        Iga käigu juures on `{[%clk h:mm:ss.f] [%emt h:mm:ss.f]}`: käigu teinud mängija kell pärast käiku ja selle käigu eest arvestatud aeg, sekundi kümnendikes (kärbitud). Väärtus jäetakse välja, kui kirjes seda pole. Pärast viimast käiku tuleb sõnadega lõpu põhjus, seejärel tulemus.

        **Piirang:** `public_read` (tokeniga mängija kohta, ilma tokenita kliendi kohta).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
      responses:
        '200':
          description: PGN-fail.
          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-tekst UTF-8 kodeeringus (`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`: pole positiivne täisarv, millel on kuni 16 numbrit (alla 2^53); `invalid_request`: pole kehtiv URL-kodeering.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: saadeti token, mis ei kehti.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: sellist partiid pole.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: piirang `public_read` või konto kvoot.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`internal_error`: muu hulgas siis, kui salvestatud käike ei õnnestu läbi mängida kuni salvestatud lõpuni (server logib selle).'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (andmebaas jäi lukustatuks; kehas `retryAfter: 1`, päist `Retry-After` pole), `server_busy` (seansi otsimisel oli andmebaas lukus), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /games/{id}/gif:
    get:
      operationId: getGameGif
      tags:
        - gifs
      summary: Laadi selle serveri partii alla animeeritud GIF-ina
      description: |-
        Partii animeeritud GIF-ina. Nimed ja reitingud on võetud partiikirjest (reitingud alguses, kustutatud konto kujul `deleted#<id>`), samuti tulemus ja lõpp (`Resignation`, `Loss on time`…).

        Kontrollid toimuvad selles järjekorras: `gif_disabled`, pildi valikud (`size`, `orientation`, `delay`, `coords`), partii ID, partii, selle pikkus.

        **Seanss on nõutav:** kvoote arvestatakse konto kaupa. **Piirangud:** `gif` igal päringul; `gif_user_min`, `gif_user_hour`, `gif_ip_min` ja `gif_ip_hour` ainult siis, kui GIF tuleb valmistada (vt jaotise kirjeldust). **Ajapiirang:** `GIF_QUEUE_TIMEOUT_MS` + `GIF_RENDER_TIMEOUT_MS` + 5 sekundit (vaikimisi 45 sekundit).
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
        - name: size
          in: query
          required: false
          description: 'Pildi suurus: `small` (32 px väljad), `medium` (48 px) või `large` (72 px).'
          schema:
            type: string
            enum:
              - small
              - medium
              - large
            default: medium
        - name: orientation
          in: query
          required: false
          description: Pool, mis on laua alumises servas.
          schema:
            type: string
            enum:
              - white
              - black
            default: white
        - name: delay
          in: query
          required: false
          description: Millisekundeid käigu kohta (1 kuni 6 kümnendnumbrit).
          schema:
            type: integer
            minimum: 100
            maximum: 3000
            default: 500
        - name: coords
          in: query
          required: false
          description: Kas vertikaalide tähed ja horisontaalide numbrid joonistatakse laua ümber (`1`) või mitte (`0`).
          schema:
            type: string
            enum:
              - '1'
              - '0'
            default: '1'
      responses:
        '200':
          $ref: '#/components/responses/GifFile'
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_option` koos väljaga `field` (`size`, `orientation`, `delay` või `coords`): väärtus pole lubatud väärtuste hulgas. `invalid_game_id`. `invalid_request`: pole kehtiv URL-kodeering.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: sellist partiid pole. `gif_disabled`: server on GIF-id välja lülitanud (`GIF_ENABLED=false`; piirangu `gif` žetoon antakse tagasi).'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '422':
          $ref: '#/components/responses/UnprocessableContent'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: piirang `gif`, renderduspiirang või konto kvoot.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`render_failed`: GIF-i ei õnnestunud valmistada (server logib põhjuse). `internal_error`.'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` koos väljaga `retryAfter` (3 kuni 10 sekundit) ja päisega `Retry-After`: renderdusjärjekord on täis või GIF ootas vaba lõime `GIF_QUEUE_TIMEOUT_MS` (10 sekundit); kõik piirangute žetoonid, mille päring võttis, antakse tagasi. `busy`: andmebaas jäi lukustatuks (`retryAfter: 1` ainult kehas). `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /gif:
    post:
      operationId: renderPgnGif
      tags:
        - gifs
      summary: Tee animeeritud GIF mis tahes PGN-ina saadetud partiist
      description: |-
        Sama pilt nagu `GET /games/{id}/gif` mis tahes partii kohta, mis on saadetud PGN-tekstina: mängus salvestatud partii, mõne teise saidi eksport, käsitsi sisestatud partii. Kasutatakse ainult teksti esimest partiid.

        - PGN-i lugeja võtab vastu sama, mida mängu enda lugeja: iga PGN-i, mille see server kirjutab, ja teiste saitide tavalised ekspordid (kommentaarid, variandid, NAG-id ja kellaannotatsioonid jäetakse vahele; käigunumbreid ja SAN-i loetakse leebelt; märgend `FEN` annab algseisu, välja arvatud siis, kui `SetUp` on `"0"`).
        - Nimed ja reitingud tulevad märgenditest `White`, `Black`, `WhiteElo` ja `BlackElo`. Diakriitikuga tähtedelt eemaldatakse diakriitikud, muud märgid väljaspool prinditavat ASCII-d muutuvad märgiks `?` ja pikad nimed lõigatakse lühemaks (48 märki). Tulemus tuleb märgendist `Result`, muidu käikude teksti lõpust. Märgendit `Termination` näidatakse, välja arvatud siis, kui see on `normal`: siis räägib lõppseis ise, mis juhtus (matt, patt).
        - See otspunkt kontrollib oma keha ise: tundmatu väli või puuduv või mittestringiline `pgn` annab vastuseks 400 `invalid_request` koos väljaga `field`; vale valik annab vastuseks 400 `invalid_option` (`delayMs` peab olema JSON-arv, `coords` tõeväärtus; `null` lükatakse tagasi).

        **Seanss on nõutav:** kvoote arvestatakse konto kaupa. **Piirangud:** nagu `GET /games/{id}/gif` puhul. **Keha suuruse piirang:** 135 168 baiti, olenemata väärtusest `HTTP_BODY_LIMIT` (PGN JSON-stringina koos paojadadega). **Ajapiirang:** vaikimisi 45 sekundit.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GifRequest'
            examples:
              short:
                summary: Lühike käsitsi sisestatud partii, ilma koordinaatideta
                value:
                  pgn: 1. f3 e5 2. g4 Qh4# 0-1
                  coords: false
              options:
                summary: PGN-fail kõigi valikutega
                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` koos väljaga `field` (tundmatu väli või `pgn` puudub või pole string; ilma väljata `field`, kui keha pole objekt); `invalid_json`; `invalid_option` koos väljaga `field` (`size`, `orientation`, `delayMs` või `coords`); `invalid_pgn` koos väljadega `line` ja `column` (alates 1-st, veerud märkides) ning lugeja sõnumiga `message`: reeglivastane või mitmetähenduslik käik, vigane märgend, tundmatu malevariant, üle 65 536 baidi.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`gif_disabled`: server on GIF-id välja lülitanud (`GIF_ENABLED=false`; piirangu `gif` žetoon antakse tagasi).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
          description: '`payload_too_large`: keha on suurem kui 135 168 baiti.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '422':
          $ref: '#/components/responses/UnprocessableContent'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: piirang `gif`, renderduspiirang või konto kvoot.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`render_failed`: GIF-i ei õnnestunud valmistada (server logib põhjuse). `internal_error`.'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` koos väljaga `retryAfter` (3 kuni 10 sekundit) ja päisega `Retry-After`: renderdusjärjekord on täis või GIF ootas vaba lõime liiga kaua; kõik piirangute žetoonid, mille päring võttis, antakse tagasi. `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /players/{username}:
    get:
      operationId: getPlayer
      tags:
        - players
      summary: Too mängija avalik profiil
      description: |-
        Mängija avalik profiil: reitingud ametlikes kategooriates (serveri järjekorras) ja partiide arvud. `games.total` loendab kõik salvestatud partiid, kaasa arvatud reitinguta ja katkestatud partiid; `games.rated`, `wins`, `draws` ja `losses` summeeritakse üle reitingukirjete, seega ainult reitinguga partiid. Vastus on sama nii tokeniga kui ka ilma.

        **Piirang:** `public_read` (tokeniga mängija kohta, ilma tokenita kliendi kohta).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Username'
      responses:
        '200':
          description: Profiil.
          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`: pole 2 kuni 24 märki hulgast `[A-Za-z0-9_.-]`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: saadeti token, mis ei kehti.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: sellist mängijat pole või konto on kustutatud.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: piirang `public_read` või konto kvoot.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (andmebaas jäi lukustatuks; kehas `retryAfter: 1`, päist `Retry-After` pole), `server_busy` (seansi otsimisel oli andmebaas lukus), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /players/{username}/games:
    get:
      operationId: listPlayerGames
      tags:
        - players
      summary: Loetle mängija viimased partiid
      description: |-
        Mängija viimased partiid, uusimad esimesena, lehekülgedena nagu ajalugu, kuid ilma filtrite ja koguarvuta. `color` on selle mängija pool. `next` on viimase partii ID alati, kui lehekülg on täis, nii et järgmine lehekülg võib olla tühi. Kõigepealt otsitakse mängija: tundmatu mängija annab 404, olenemata päringu parameetritest.

        **Piirang:** `public_read` (tokeniga mängija kohta, ilma tokenita kliendi kohta).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Username'
        - $ref: '#/components/parameters/BeforeQuery'
        - $ref: '#/components/parameters/LimitQuery'
      responses:
        '200':
          description: Üks lehekülg mängija partiidest.
          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` pole partii ID), `invalid_limit` (need kaks ilma väljata `field`).'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: saadeti token, mis ei kehti.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: sellist mängijat pole või konto on kustutatud.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: piirang `public_read` või konto kvoot.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (andmebaas jäi lukustatuks; kehas `retryAfter: 1`, päist `Retry-After` pole), `server_busy` (seansi otsimisel oli andmebaas lukus), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /leaderboard:
    get:
      operationId: getLeaderboard
      tags:
        - leaderboard
      summary: Too kategooria parimad mängijad
      description: |-
        Ametliku kategooria 100 parimat reitingukirjet, millel on vähemalt `minGames` (`PROVISIONAL_GAMES`) arvesse läinud partiid; kustutatud kontod ja kinnitatud sohitegijad jäetakse välja. Server arvutab iga edetabeli uuesti mitte sagedamini kui iga 10 sekundi järel; `updatedAt` ütleb, millal. Kategooria kohta käib korraga ainult üks arvutus: selle ajal saavad teised päringud eelmise loendi või ootavad arvutuse lõppu, kui loendit veel pole.

        **Piirangud:** ainult aadressipõhine kiht.
      security: []
      parameters:
        - name: category
          in: query
          required: true
          description: Ametliku kategooria ID (`3+2` või `3%2B2`).
          schema:
            type: string
            pattern: '^\s*[0-9]+[+ ][0-9]+\s*$'
          example: '3+2'
        - name: limit
          in: query
          required: false
          description: Mitu mängijat, 1 kuni 100. Suurem, kuni 3-kohaline arv loetakse võrdseks 100-ga; tühi väärtus loetakse puuduvaks.
          schema:
            type: integer
            minimum: 1
            maximum: 999
            default: 100
      responses:
        '200':
          description: Edetabel.
          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` (puudub või pole ametlik kategooria), `invalid_limit`.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: aadressipõhine kiht.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (andmebaas jäi lukustatuks; kehas `retryAfter: 1`, päist `Retry-After` pole), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /reports:
    post:
      operationId: reportPlayer
      tags:
        - reports
      summary: Teata hiljutise partii vastasest
      description: |-
        Teatab mängija enda partii vastasest, kui partii lõppes viimase 7 päeva jooksul. Teade ei muuda kunagi iseenesest reitingut, sanktsiooni ega usaldusväärsuse taset. See tõstab läbivaatamise prioriteeti, mida moderaatorid näevad, ja (välja arvatud `abuse` puhul) tellib partii mootorianalüüsi.

        Teade samast vastasest sama partii kohta saab sama vastuse ega muuda midagi; vastus ei ütle kunagi midagi konto kohta, millest teatati. `GET /games/{id}` ütleb partii mängijatele ette, kas teade võetaks vastu (`reportable`).

        See otspunkt kontrollib oma keha ise, järjekorras `gameId`, `reported`, `category`, `comment`: viga annab vastuseks 400 `invalid_request` ilma väljata `field`. Välju, mida see ei tunne, eiratakse.

        **Piirangud:** `reports` (mängija kohta), seejärel `REPORTS_PER_DAY` (5) teadet mängija kohta 24 tunni jooksul (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: Teade on vastu võetud (või oli juba esitatud).
          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` (ilma väljata `field`), `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`report_not_allowed`: pole teataja vastane partiis, mis lõppes viimase 7 päeva jooksul.'
        '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`, koos päisega `Retry-After`): viimase 24 tunni jooksul on tehtud `REPORTS_PER_DAY` teadet. `rate_limited`: piirang `reports` või konto kvoot.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (seansi otsimisel oli andmebaas lukus), `timeout`. Samuti `server_busy` väärtusega `retryAfter` 2 kuni 5, kui server on tööga kaugele maha jäänud (vt sissejuhatust): päringut ei täidetud.'
  /verify-email:
    servers:
      - url: https://caissa.scacelith.com
        description: Ametlik server (lehed asuvad juurtasemel, väljaspool `/api/v1`).
      - url: https://{host}:{port}
        description: Mis tahes Scacelithi server (lehed asuvad juurtasemel, väljaspool `/api/v1`).
        variables:
          host:
            default: caissa.scacelith.com
            description: Serveri avalik hostinimi (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Avalik API port (`PUBLIC_API_PORT`, muidu `API_PORT`).
    get:
      operationId: showVerifyEmailPage
      tags:
        - pages
      summary: Näita e-posti kinnitamise lehte
      description: |-
        E-posti kinnituslingi leht (registreerumine või uuesti saadetud kinnituskiri). See näitab ainult nuppu „Confirm my e-mail address“ (kinnita minu e-posti aadress), et linki avav e-kirjade skanner seda ära ei kasutaks; nupp saadab vormi otspunktile `POST /verify-email`.

        **Piirang:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Leht koos kinnitusnupuga.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Link on kehtetu või aegunud (HTML-leht).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Piirang `page` (HTML-leht) või aadressipõhine kiht (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitVerifyEmailPage
      tags:
        - pages
      summary: Kinnita e-posti aadress
      description: |-
        Kinnituslehe vorm. See kinnitab aadressi; uue registreerumise korral loob see siis konto ja mängija saab sisse logida.

        **Piirang:** `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: Aadress on kinnitatud (ja uue registreerumise konto loodud).
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Link on kehtetu, kasutatud või aegunud või vorm on vigane (HTML-leht).
        '408':
          $ref: '#/components/responses/HtmlRequestTimeout'
        '409':
          $ref: '#/components/responses/HtmlConflict'
          description: Uue registreerumise link, mille kasutajanime või aadressi on vahepeal võtnud mõni teine konto; kontot ei looda (HTML-leht).
        '413':
          $ref: '#/components/responses/HtmlPayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/HtmlUnsupportedMediaType'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Piirang `auth` (HTML-leht) või aadressipõhine kiht (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Andmebaas jäi lukustatuks (`Retry-After: 1`) või server on tööga kaugele maha jäänud (`Retry-After` 2 kuni 5, vt sissejuhatust): midagi ei muutunud ja link töötab endiselt. Samuti käsitleja ajalõpp.'
  /reset-password:
    servers:
      - url: https://caissa.scacelith.com
        description: Ametlik server (lehed asuvad juurtasemel, väljaspool `/api/v1`).
      - url: https://{host}:{port}
        description: Mis tahes Scacelithi server (lehed asuvad juurtasemel, väljaspool `/api/v1`).
        variables:
          host:
            default: caissa.scacelith.com
            description: Serveri avalik hostinimi (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Avalik API port (`PUBLIC_API_PORT`, muidu `API_PORT`).
    get:
      operationId: showResetPasswordPage
      tags:
        - pages
      summary: Näita parooli lähtestamise vormi
      description: |-
        Parooli lähtestamislingi leht: uue parooli vorm (parool kaks korda). Vorm saadetakse otspunktile `POST /reset-password`.

        **Piirang:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Uue parooli vorm.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Link on kehtetu (ka siis, kui see saadeti aadressile, mida kontol enam pole).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Piirang `page` (HTML-leht) või aadressipõhine kiht (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitResetPasswordPage
      tags:
        - pages
      summary: Määra lähtestamisvormi kaudu uus parool
      description: |-
        Lähtestamislehe vorm; see teeb sama, mida `POST /auth/password/reset`, ühes transaktsioonis: kõik seadmed logitakse välja, ootel e-posti aadressi muutmine tühistatakse, teised lähtestamislingid lakkavad töötamast, aadress loetakse kinnitatuks ja omanik saab e-kirja.

        Kõigepealt kontrollitakse linki, seejärel seda, et mõlemad paroolid on samad, ja siis paroolireegleid. Kui server on hõivatud, tuleb vorm tagasi koos päisega `Retry-After` ja link jääb kehtima.

        **Piirangud:** `auth`, seejärel `auth_reset` (ühine otspunktiga `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: Parool on muudetud ja kõik seadmed on välja logitud.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Vorm uuesti koos veaga (paroolid erinevad, nõrk parool), link on kehtetu või vorm on vigane (HTML-leht).
        '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: Piirang `auth` või `auth_reset` (HTML-leht), vorm uuesti koos päisega `Retry-After`, kui sellel kliendil ootab liiga palju paroolide räsimisi (link jääb kehtima), või aadressipõhine kiht (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Vorm uuesti koos päisega `Retry-After`, kui server on hõivatud (paroolide räsimise järjekord või andmebaas jäi lukustatuks); link jääb kehtima. Kui server on tööga kaugele maha jäänud (vt sissejuhatust), siis selle asemel leht „Server error“ (serveri viga) koos päisega `Retry-After` (2 kuni 5): midagi ei täidetud ja link jääb kehtima. Samuti käsitleja ajalõpp.'
  /confirm-email-change:
    servers:
      - url: https://caissa.scacelith.com
        description: Ametlik server (lehed asuvad juurtasemel, väljaspool `/api/v1`).
      - url: https://{host}:{port}
        description: Mis tahes Scacelithi server (lehed asuvad juurtasemel, väljaspool `/api/v1`).
        variables:
          host:
            default: caissa.scacelith.com
            description: Serveri avalik hostinimi (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Avalik API port (`PUBLIC_API_PORT`, muidu `API_PORT`).
    get:
      operationId: showConfirmEmailChangePage
      tags:
        - pages
      summary: Näita e-posti aadressi muutmise kinnituslehte
      description: |-
        E-posti aadressi muutmise lingi leht: see näitab uut aadressi ja konto nime ning nuppu „Use this e-mail address“ (kasuta seda e-posti aadressi), mis saadab vormi otspunktile `POST /confirm-email-change`.

        **Piirang:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Leht uue aadressi ja selle kinnitusnupuga.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Link on kehtetu või aegunud (ka siis, kui konto aadress on pärast päringut muutunud).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Piirang `page` (HTML-leht) või aadressipõhine kiht (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitConfirmEmailChangePage
      tags:
        - pages
      summary: Kinnita uus e-posti aadress
      description: |-
        E-posti aadressi muutmise lehe vorm. Aadress muutub ja loetakse kinnitatuks; seadmed jäävad sisse logituks; varem saadetud lingid (kinnitus, parooli lähtestamine, muud muutmised) lakkavad töötamast; endist aadressi teavitatakse, kusjuures uus aadress on varjatud.

        **Piirang:** `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: Aadress on muudetud.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Link on kehtetu, kasutatud või aegunud või vorm on vigane (HTML-leht).
        '408':
          $ref: '#/components/responses/HtmlRequestTimeout'
        '409':
          $ref: '#/components/responses/HtmlConflict'
          description: Mõni teine konto võttis aadressi vahepeal endale (HTML-leht).
        '413':
          $ref: '#/components/responses/HtmlPayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/HtmlUnsupportedMediaType'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Piirang `auth` (HTML-leht) või aadressipõhine kiht (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Andmebaas jäi lukustatuks (`Retry-After: 1`) või server on tööga kaugele maha jäänud (`Retry-After` 2 kuni 5, vt sissejuhatust): midagi ei muutunud ja link töötab endiselt. Samuti käsitleja ajalõpp.'
  /healthz:
    servers:
      - url: https://caissa.scacelith.com
        description: Ametlik server, juurtasemel.
      - url: https://caissa.scacelith.com/api/v1
        description: Ametlik server, tee `/api/v1` all.
      - url: https://{host}:{port}
        description: Mis tahes Scacelithi server, juurtasemel.
        variables:
          host:
            default: caissa.scacelith.com
            description: Serveri avalik hostinimi (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Avalik API port (`PUBLIC_API_PORT`, muidu `API_PORT`).
      - url: https://{host}:{port}/api/v1
        description: Mis tahes Scacelithi server, tee `/api/v1` all.
        variables:
          host:
            default: caissa.scacelith.com
            description: Serveri avalik hostinimi (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Avalik API port (`PUBLIC_API_PORT`, muidu `API_PORT`).
    get:
      operationId: getLiveness
      tags:
        - health
      summary: Kontrolli, et protsess töötab
      description: |-
        Vastab 200 `ok`, kuni protsess töötab. Töötab ka `HEAD`. Iga muu meetod saab vastuseks 405 `method_not_allowed` koos päisega `Allow: GET, HEAD` (ka `OPTIONS`).

        **Piirangud:** ainult aadressipõhine kiht.
      security: []
      responses:
        '200':
          description: Protsess töötab.
          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`: aadressipõhine kiht.'
  /readyz:
    servers:
      - url: https://caissa.scacelith.com
        description: Ametlik server, juurtasemel.
      - url: https://caissa.scacelith.com/api/v1
        description: Ametlik server, tee `/api/v1` all.
      - url: https://{host}:{port}
        description: Mis tahes Scacelithi server, juurtasemel.
        variables:
          host:
            default: caissa.scacelith.com
            description: Serveri avalik hostinimi (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Avalik API port (`PUBLIC_API_PORT`, muidu `API_PORT`).
      - url: https://{host}:{port}/api/v1
        description: Mis tahes Scacelithi server, tee `/api/v1` all.
        variables:
          host:
            default: caissa.scacelith.com
            description: Serveri avalik hostinimi (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Avalik API port (`PUBLIC_API_PORT`, muidu `API_PORT`).
    get:
      operationId: getReadiness
      tags:
        - health
      summary: Kontrolli, et server võtab mängijaid vastu
      description: |-
        Vastab 200 `ready`, kui server võtab mängijaid vastu, ja 503 `not_ready`, kui see käivitub või seiskub. Töötab ka `HEAD`. Iga muu meetod saab vastuseks 405 `method_not_allowed` koos päisega `Allow: GET, HEAD` (ka `OPTIONS`).

        **Piirangud:** ainult aadressipõhine kiht.
      security: []
      responses:
        '200':
          description: Server võtab mängijaid vastu.
          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`: aadressipõhine kiht.'
        '503':
          description: Server käivitub või seiskub.
          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: |-
        Seansitoken (`sct_`, millele järgneb 43 base64url-märki) sisselogimisvastusest, päises `Authorization`: `Authorization: Bearer <token>`. Eesliide on täpselt `Bearer` (tõstutundlik) ja üks tühik; väärtus, millel seda pole, või token, mis ei koosne 1 kuni 512 prinditavast ASCII-märgist, saab vastuseks 401 `invalid_token` nagu iga teine kehtetu token. Tühi päis loetakse päise puudumiseks.
  parameters:
    GameId:
      name: id
      in: path
      required: true
      description: 'Partii ID: positiivne kümnendsüsteemis täisarv, kuni 16 numbrit, ilma eesnullideta, alla 2^53.'
      schema:
        type: integer
        format: int64
        minimum: 1
        maximum: 9007199254740991
      example: 4100000000001
    SessionId:
      name: id
      in: path
      required: true
      description: Seansi ID, nagu `GET /auth/sessions` selle annab, kirjutatuna ilma eesnullideta (`03` pole seanss 3).
      schema:
        type: integer
        format: int64
        minimum: 1
      example: 3
    Username:
      name: username
      in: path
      required: true
      description: Mängija kasutajanimi, tõstust arvestamata. Server võtab vastu iga nime, mis koosneb 2 kuni 24 märgist hulgast `[A-Za-z0-9_.-]` (kõige laiem reegel, mida server on kunagi lubanud).
      schema:
        type: string
        pattern: '^[A-Za-z0-9_.-]{2,24}$'
      example: alice
    BeforeQuery:
      name: before
      in: query
      required: false
      description: Partii ID; loetletakse ainult vanemad partiid. Andke eelmise lehekülje `next`. Tühi väärtus loetakse puuduvaks.
      schema:
        type: integer
        format: int64
        minimum: 1
        maximum: 9007199254740991
      example: 4100000000002
    LimitQuery:
      name: limit
      in: query
      required: false
      description: Lehekülje suurus, 1 kuni 50. Suurem, kuni 3-kohaline arv loetakse võrdseks 50-ga; tühi väärtus loetakse puuduvaks.
      schema:
        type: integer
        minimum: 1
        maximum: 999
        default: 20
    LinkTokenQuery:
      name: token
      in: query
      required: false
      description: E-posti lingi token (43 base64url-märki). Puuduva või vale tokeni korral näidatakse lehte „link invalid or expired“ (link on kehtetu või aegunud).
      schema:
        type: string
        pattern: '^[A-Za-z0-9_-]{43}$'
      example: asfioO4P0r_Zn5DxWzcQuF6Drgxu7wXGjCyS-UFbTaw
  headers:
    CacheControl:
      description: Serveri iga vastus keelab vahemällu salvestamise.
      schema:
        type: string
        const: no-store
    RetryAfter:
      description: Sekundid, mida oodata enne uut katset; sama väärtus mis `retryAfter` kehas.
      schema:
        type: integer
        minimum: 1
      example: 30
    WwwAuthenticate:
      description: Bearer-väljakutse, kehtetu tokeni korral koos parameetriga `error="invalid_token"`.
      schema:
        type: string
        enum:
          - Bearer realm="scacelith"
          - Bearer realm="scacelith", error="invalid_token"
    ConnectionClose:
      description: Server sulgeb ühenduse pärast seda vastust.
      schema:
        type: string
        const: close
  responses:
    BadRequest:
      description: '`invalid_request` (koos väljaga `field`, kui viga on keha mõnes väljas) või `invalid_json`.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalidRequest:
              summary: Väli rikub skeemi
              value:
                error: invalid_request
                message: '"password" is required'
                field: password
            invalidJson:
              summary: Keha pole JSON
              value:
                error: invalid_json
                message: The body is not valid JSON.
            weakPassword:
              summary: Nõrk parool
              value:
                error: weak_password
                message: The password must have at least 10 characters.
                reason: too_short
            invalidPgn:
              summary: PGN, mida ei saa lugeda
              value:
                error: invalid_pgn
                message: illegal move 'Ke3'
                line: 1
                column: 13
    Unauthorized:
      description: '`unauthorized` (päist `Authorization` pole) või `invalid_token` (token on vigane, aegunud, tühistatud või kuulub kustutatud kontole).'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        WWW-Authenticate:
          $ref: '#/components/headers/WwwAuthenticate'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            unauthorized:
              summary: Seanss puudub
              value:
                error: unauthorized
                message: Log in first.
            invalidToken:
              summary: Kehtetu token
              value:
                error: invalid_token
                message: The session is invalid or has expired; log in again.
    Forbidden:
      description: Päring lükatakse tagasi.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalidPassword:
              summary: Vale parool uuesti autentimisel
              value:
                error: invalid_password
                message: Wrong password.
            banned:
              summary: Peatatud konto
              value:
                error: banned
                message: This account is banned.
                until: 1791487639708
    NotFound:
      description: '`not_found` või funktsioon, mille see server on välja lülitanud.'
      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`: keha ei saabunud 10 sekundi jooksul. Server sulgeb ühenduse.'
      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: Päring on vastuolus serveri olekuga.
      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`: Google''iga sisselogimise katse või pilet on tundmatu, kasutatud või aegunud; alustage sisselogimist uuesti.'
      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`: keha ületab väärtuse `HTTP_BODY_LIMIT` (vaikimisi 16 384 baiti). Server sulgeb ühenduse.'
      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`: päringu sihtmärk on pikem kui 4096 märki.'
      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`: keha pole `application/json` või selle märgistik pole 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`: partiis on rohkem kui `GIF_MAX_PLIES` (600) poolkäiku.'
      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`: lahendage väljakutse `pow` ja saatke sama päring uuesti koos väljaga `pow: { challenge, nonce }`. `reason` ütleb põhjuse: `required`, `malformed`, `signature`, `endpoint`, `network`, `expired`, `bits`, `work` või `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` (päringute piirang, konto kvoot või paroolide räsimise järjekord) või `too_many_attempts` (konto pidurdus), koos väljaga `retryAfter` ja päisega `Retry-After`.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            rateLimited:
              summary: Päringute piirang
              value:
                error: rate_limited
                message: Too many requests; try again later.
                retryAfter: 30
            tooManyAttempts:
              summary: Sellel kontol on liiga palju ebaõnnestumisi
              value:
                error: too_many_attempts
                message: Too many attempts; wait before trying again.
                retryAfter: 4
    InternalError:
      description: '`internal_error`: ootamatu tõrge. Server logib selle.'
      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`: vale `state` või `iss` või Google lükkas koodi tagasi või saatis ID-tokeni, mis ei läbi kontrolli (sealhulgas teise kliendi – mängu või veebisaidi – kood või ID-token). See ei sisalda kunagi Google''i teksti.'
      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` või `timeout`: proovige uuesti pärast `retryAfter` sekundit, kui see on antud.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            serverBusy:
              summary: Server on hõivatud
              value:
                error: server_busy
                message: The server is busy; try again in a few seconds.
                retryAfter: 9
            busy:
              summary: Andmebaas jäi lukustatuks (lugemine)
              value:
                error: busy
                message: Try again shortly.
                retryAfter: 1
            timeout:
              summary: Serveril kulus liiga kaua aega
              value:
                error: timeout
                message: The server took too long to answer; try again.
    GifFile:
      description: Animeeritud GIF manusena.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Content-Disposition:
          description: '`attachment; filename="scacelith-<id>.gif"` selle serveri partii puhul, `attachment; filename="scacelith-game.gif"` otspunktiga `POST /gif` saadetud PGN-i puhul.'
          schema:
            type: string
            pattern: '^attachment; filename="scacelith-([1-9][0-9]{0,15}|game)\.gif"$'
          example: attachment; filename="scacelith-4100000000001.gif"
        Content-Length:
          description: Faili suurus baitides.
          schema:
            type: integer
            minimum: 1
      content:
        image/gif:
          schema:
            type: string
            format: binary
    HtmlPage:
      description: HTML-leht.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlBadRequest:
      description: HTML-leht, mis selgitab tagasilükkamist.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlRequestTimeout:
      description: Vorm ei saabunud 10 sekundi jooksul (HTML-leht). Server sulgeb ühenduse.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Connection:
          $ref: '#/components/headers/ConnectionClose'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlConflict:
      description: HTML-leht, mis selgitab konflikti.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlPayloadTooLarge:
      description: Vorm ületab väärtuse `HTTP_BODY_LIMIT` (HTML-leht). Server sulgeb ühenduse.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Connection:
          $ref: '#/components/headers/ConnectionClose'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlUnsupportedMediaType:
      description: Keha pole ei vorm (`application/x-www-form-urlencoded`) ega JSON või selle märgistik pole UTF-8 (HTML-leht).
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlTooManyRequests:
      description: Päringute piirang (HTML-leht), koos päisega `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: Ootamatu tõrge (HTML-leht pealkirjaga „Server error“). Server logib selle.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlServiceUnavailable:
      description: Server on hõivatud (andmebaas jäi lukustatuks või server on tööga kaugele maha jäänud; koos päisega `Retry-After`) või sellel kulus liiga kaua aega (HTML-leht pealkirjaga „Server error“).
      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: JSON API iga veavastuse ainus kuju.
      required:
        - error
        - message
      properties:
        error:
          type: string
          pattern: '^[a-z][a-z0-9_]*$'
          description: Veakood kujul `snake_case`. Klient valib selle põhjal, mida näidata.
          examples:
            - rate_limited
        message:
          type: string
          description: Ingliskeelne lause, mõeldud logide jaoks ja varulahenduseks.
        retryAfter:
          type: integer
          minimum: 1
          description: Sekundid, mida oodata enne uut katset; on olemas ainult tagasilükkamistel, mis aja möödudes lõpevad. Siis on vastuses ka sama väärtusega päis `Retry-After`, välja arvatud ajaloo, partii, mängija ja edetabeli lugemise 503 `busy` korral.
        field:
          type: string
          description: Väli või päringuparameeter, milles on viga (`invalid_request`, `invalid_option` ning otspunkti `GET /account/games` `invalid_cursor`, `invalid_limit` ja `invalid_filter`). Pesastatud väli kirjutatakse punktidega (`pow.nonce`).
        reason:
          type: string
          description: 'Põhjus: reegel, mida `weak_password` rikub, või miks töötõendit nõuti või see tagasi lükati (`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: 'Ainult `banned`: peatamise lõpp (Unixi aeg, ms), alalise peatamise korral `null`.'
        line:
          type: integer
          minimum: 1
          description: 'Ainult `invalid_pgn`: vea rida, alates 1-st.'
        column:
          type: integer
          minimum: 1
          description: 'Ainult `invalid_pgn`: vea veerg, alates 1-st, märkides.'
    PowChallenge:
      type: object
      description: Töötõendi väljakutse (vastuse 428 `pow_required` väli `pow`).
      required:
        - challenge
        - bits
        - expiresAt
      properties:
        challenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{16,400}\.[A-Za-z0-9_-]{43}$'
          description: Allkirjastatud väljakutse, mis tuleb saata tagasi muutmata kujul.
        bits:
          type: integer
          minimum: 1
          maximum: 26
          description: Nullbittide arv, millega `SHA-256(challenge + ":" + nonce)` peab algama.
        expiresAt:
          type: integer
          format: int64
          description: 'Millal väljakutse aegub (Unixi aeg, ms): 2 minutit pärast selle väljaandmist.'
    PowAnswer:
      type: object
      description: Vastus töötõendi väljakutsele.
      additionalProperties: false
      required:
        - challenge
        - nonce
      properties:
        challenge:
          type: string
          minLength: 16
          maxLength: 512
          description: Vastuse 428 `challenge` samal kujul, nagu see anti.
        nonce:
          type: string
          pattern: '^[0-9]{1,20}$'
          description: Kuni 20 numbrist koosnev kümnendstring, mille korral `SHA-256(challenge + ":" + nonce)` algab `bits` nullbitiga.
    ServerInfo:
      type: object
      description: Mida klient vajab enne sisselogimist või ühendumist.
      required:
        - name
        - serverId
        - motd
        - protocol
        - wsPort
        - wsPath
        - registration
        - emailVerification
        - sso
        - mfa
        - pow
        - categories
        - limits
      properties:
        name:
          type: string
          description: Serveri nimi (`SERVER_NAME`).
        serverId:
          type:
            - string
            - 'null'
          format: uuid
          description: UUID, mille andmebaas saab esimesel käivitamisel. See jääb taaskäivitustel samaks. See on `null`, kui andmebaas ei saa seda anda.
        motd:
          type: string
          description: Päevasõnum (`SERVER_MOTD`), võib olla tühi.
        protocol:
          type: object
          description: WebSocketi protokoll (`PROTOCOL.md`).
          required:
            - min
            - max
            - schema
            - subprotocol
          properties:
            min:
              type: integer
              minimum: 1
              description: Vanim protokolliversioon, mida server toetab.
            max:
              type: integer
              minimum: 1
              description: Uusim protokolliversioon, mida server toetab.
            schema:
              type: integer
              format: int64
              minimum: 0
              maximum: 4294967295
              description: 'Protokolli skeemi sõrmejälg (selle kanoonilise kuju SHA-256 esimesed 4 baiti märgita täisarvuna: `0xc649140f`, 3326678031, protokolli alamversiooni 2 puhul); informatiivne, seda ei võrrelda kunagi.'
            subprotocol:
              type: string
              description: WebSocketi alamprotokolli tunnus (`Sec-WebSocket-Protocol`).
        wsPort:
          type: integer
          minimum: 0
          maximum: 65535
          description: WebSocketi port, mida mängijad kasutavad (`PUBLIC_WS_PORT`, muidu `WS_PORT`, muidu `API_PORT`).
        wsPath:
          type: string
          const: /ws
          description: WebSocketi tee.
        registration:
          type: string
          enum:
            - open
            - closed
          description: Kas uusi kontosid saab luua.
        emailVerification:
          type: boolean
          description: Kas uued kontod kinnitavad oma aadressi (`REQUIRE_EMAIL_VERIFICATION`).
        sso:
          type: object
          required:
            - google
            - googleWeb
          properties:
            google:
              type: boolean
              description: Kas mängule pakutakse Google'iga sisselogimist.
            googleWeb:
              type: boolean
              description: Kas Google'iga sisselogimist pakutakse ka veebisaidile (`POST /auth/sso/google/web/start`).
        mfa:
          type: boolean
          const: true
          description: Kaheastmeline autentimine on alati saadaval.
        pow:
          type: object
          required:
            - register
          properties:
            register:
              type: integer
              minimum: 0
              maximum: 26
              description: Töötõendi bitid, mida registreerumine nõuab (0, kui ei nõua).
        categories:
          type: array
          description: Ametlikud (reitinguga) ajakontrollid (`RATED_CATEGORIES`) serveri järjekorras. Iga muu ajakontroll on `custom`.
          items:
            $ref: '#/components/schemas/Category'
        limits:
          type: object
          description: Reeglid, mida klient saab kontrollida enne vormi saatmist.
          required:
            - usernameMin
            - usernameMax
            - usernamePattern
            - passwordMinLength
            - passwordMaxBytes
            - customTimeControls
            - reportsPerDay
            - wsMaxMessageBytes
          properties:
            usernameMin:
              type: integer
              description: Kasutajanime vähim pikkus (`USERNAME_MIN`).
            usernameMax:
              type: integer
              description: Kasutajanime suurim pikkus (`USERNAME_MAX`).
            usernamePattern:
              type: string
              description: Regulaaravaldis, millele uus kasutajanimi peab vastama.
            passwordMinLength:
              type: integer
              description: Parooli vähim pikkus märkides (`PASSWORD_MIN_LENGTH`).
            passwordMaxBytes:
              type: integer
              description: Parooli suurim pikkus UTF-8 baitides.
            customTimeControls:
              type: boolean
              description: Kas väljakutsetes ja privaatpartiides võib kasutada kohandatud ajakontrolle.
            reportsPerDay:
              type: integer
              description: Mitu teadet võib mängija 24 tunni jooksul esitada (`REPORTS_PER_DAY`).
            wsMaxMessageBytes:
              type: integer
              description: Suurim WebSocketi sõnum, mida klient võib saata, baitides.
      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: Ametlik ajakontroll.
      required:
        - id
        - baseSec
        - incSec
      properties:
        id:
          type: string
          pattern: '^[0-9]+\+[0-9]+$'
          description: 'Kategooria ID: minutid ja lisaaja sekundid (`3+2`).'
        baseSec:
          type: number
          minimum: 0
          description: Põhiaeg sekundites.
        incSec:
          type: number
          minimum: 0
          description: Lisaaeg käigu kohta sekundites.
    AccountView:
      type: object
      description: Konto nii, nagu selle mängija seda näeb (sisselogimisvastuste ja otspunkti `GET /account/me` väli `user`).
      required:
        - id
        - username
        - email
        - emailVerified
        - mfaEnabled
        - googleLinked
        - hasPassword
        - acceptChallenges
        - createdAt
        - lastLoginAt
        - pendingEmail
      properties:
        id:
          type: integer
          format: int64
          description: Konto ID.
        username:
          type: string
          description: Kasutajanimi.
        email:
          type: string
          format: email
          description: E-posti aadress.
        emailVerified:
          type: boolean
          description: Kas aadress on kinnitatud.
        mfaEnabled:
          type: boolean
          description: Kas kaheastmeline autentimine on sisse lülitatud.
        googleLinked:
          type: boolean
          description: Kas Google'i konto on seotud.
        hasPassword:
          type: boolean
          description: '`false` Google''i kaudu loodud konto puhul, millele pole veel parooli määratud.'
        acceptChallenges:
          type: string
          enum:
            - all
            - none
          description: Kas otsesed väljakutsed nime järgi võetakse vastu (vt `PUT /account/preferences`).
        createdAt:
          type: integer
          format: int64
          description: Millal konto loodi (Unixi aeg, ms).
        lastLoginAt:
          type:
            - integer
            - 'null'
          format: int64
          description: Viimane sisselogimine (Unixi aeg, ms) või `null`.
        pendingEmail:
          type:
            - string
            - 'null'
          format: email
          description: Lingi kaudu kinnitamist ootava e-posti aadressi muutmise uus aadress või `null`.
    SessionAnswer:
      type: object
      description: Uus seanss.
      required:
        - token
        - expiresAt
        - user
      properties:
        token:
          type: string
          pattern: '^sct_[A-Za-z0-9_-]{43}$'
          description: Seansitoken päise `Authorization` ja WebSocketi jaoks.
        expiresAt:
          type: integer
          format: int64
          description: Seansi absoluutne lõpp (Unixi aeg, ms); jõudeoleku piirang võib selle varem lõpetada.
        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: Kaheastmelise autentimisega sisselogimise teine samm; jätkake otspunktiga `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: Selle sammu token otspunkti `POST /auth/login/mfa` jaoks.
        expiresIn:
          type: integer
          const: 300
          description: Sammu lõpetamiseks jäänud sekundid.
    LoginAnswer:
      description: Seanss või kaheastmelise autentimisega sisselogimise teine samm.
      oneOf:
        - $ref: '#/components/schemas/SessionAnswer'
        - $ref: '#/components/schemas/MfaChallenge'
    SsoNeedsUsername:
      type: object
      description: Esimene Google'iga sisselogimine; jätkake otspunktiga `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: Pilet otspunkti `POST /auth/sso/complete` jaoks, kehtib 10 minutit.
        suggestedUsername:
          type: string
          description: Google'i nimest või aadressist tehtud kasutajanimi või `""`, kui miski ei sobi.
    SsoNeedsPassword:
      type: object
      description: Aadressi kasutab parooliga konto; jätkake otspunktiga `POST /auth/sso/google/link`. Midagi pole veel seotud.
      required:
        - needsPassword
        - linkTicket
        - username
        - expiresIn
      properties:
        needsPassword:
          type: boolean
          const: true
        linkTicket:
          type: string
          pattern: '^sso_[A-Za-z0-9_-]{43}$'
          description: Pilet otspunkti `POST /auth/sso/google/link` jaoks.
        username:
          type: string
          description: Selle konto kasutajanimi (seda näeb ainult see, kes on Google'ile aadressi omamist tõendanud).
        expiresIn:
          type: integer
          const: 600
          description: Mitu sekundit pilet kehtib.
    SsoFinishAnswer:
      description: Otspunkti `POST /auth/sso/google/finish` vastus.
      oneOf:
        - $ref: '#/components/schemas/SessionAnswer'
        - $ref: '#/components/schemas/MfaChallenge'
        - $ref: '#/components/schemas/SsoNeedsUsername'
        - $ref: '#/components/schemas/SsoNeedsPassword'
    SsoStartAnswer:
      type: object
      description: Google'iga sisselogimise katse.
      required:
        - attemptId
        - authUrl
        - state
        - expiresIn
      properties:
        attemptId:
          type: string
          pattern: '^sso_[A-Za-z0-9_-]{43}$'
          description: Katse otspunkti `POST /auth/sso/google/finish` jaoks.
        authUrl:
          type: string
          format: uri
          description: Google'i URL, mis tuleb pärast kontrollimist avada süsteemi brauseris.
        state:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: '`state`, mille Google peab tagasi saatma.'
        expiresIn:
          type: integer
          const: 600
          description: Mitu sekundit katse kehtib.
    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: Kasutajanimi; seejärel kehtivad serveri reeglid (vt kirjeldust).
        email:
          type: string
          minLength: 1
          maxLength: 254
          description: E-posti aadress. Algus- ja lõputühikud eemaldatakse ning see salvestatakse väiketähtedega.
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Parool; seejärel kehtivad paroolireeglid.
        pow:
          $ref: '#/components/schemas/PowAnswer'
    LoginRequest:
      type: object
      additionalProperties: false
      required:
        - login
        - password
      properties:
        login:
          type: string
          minLength: 1
          maxLength: 254
          description: Kasutajanimi või e-posti aadress (iga tekst, milles on `@`).
        password:
          type: string
          minLength: 1
          maxLength: 1024
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
        pow:
          $ref: '#/components/schemas/PowAnswer'
    ClientLabel:
      type: string
      maxLength: 64
      description: Valikuline. Näidatakse sisselogitud seadmete loendis (näiteks `Scacelith 1.4 (Windows)`).
    MfaLoginRequest:
      type: object
      additionalProperties: false
      required:
        - mfaToken
      properties:
        mfaToken:
          type: string
          minLength: 1
          maxLength: 64
          description: Sisselogimisvastuse (või Google'iga sisselogimise vastuse) `mfaToken`.
        code:
          type: string
          maxLength: 32
          description: Valikuline. 6-kohaline autentimisrakenduse kood või taastekood.
        recoveryCode:
          type: string
          maxLength: 32
          description: Valikuline. Taastekood. Vaja on üht väljadest `code` ja `recoveryCode`.
    EmailRequest:
      type: object
      additionalProperties: false
      required:
        - email
      properties:
        email:
          type: string
          minLength: 1
          maxLength: 254
          description: E-posti aadress.
    PasswordResetRequest:
      type: object
      additionalProperties: false
      required:
        - token
        - newPassword
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: Lähtestamislingi parameeter `token`.
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Uus parool; kehtivad registreerumise paroolireeglid.
    SsoStartRequest:
      type: object
      additionalProperties: false
      required:
        - codeChallenge
        - redirectPort
      properties:
        codeChallenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: Kliendi `codeVerifier` S256 PKCE väljakutse (`BASE64URL(SHA-256(codeVerifier))`).
        redirectPort:
          type: integer
          minimum: 1024
          maximum: 65535
          description: Kliendi kuulaja port aadressil `127.0.0.1`.
    SsoWebStartRequest:
      type: object
      additionalProperties: false
      required:
        - codeChallenge
      properties:
        codeChallenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: Lehe `codeVerifier` S256 PKCE väljakutse (`BASE64URL(SHA-256(codeVerifier))`).
    SsoFinishRequest:
      type: object
      additionalProperties: false
      required:
        - attemptId
        - codeVerifier
        - state
        - code
      properties:
        attemptId:
          type: string
          minLength: 1
          maxLength: 64
          description: '`attemptId` otspunkti `POST /auth/sso/google/start` vastusest.'
        codeVerifier:
          type: string
          pattern: '^[A-Za-z0-9._~-]{43,128}$'
          description: PKCE verifitseerija; selle SHA-256 peab vastama sammule `start` antud väljakutsele.
        state:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: '`state` sellisena, nagu Google selle tagasi saatis.'
        code:
          type: string
          minLength: 1
          maxLength: 2048
          pattern: '^[\x21-\x7E]+$'
          description: '`code` sellisena, nagu Google selle tagasi saatis (prinditav ASCII ilma tühikuteta).'
        iss:
          type: string
          minLength: 1
          maxLength: 256
          description: Valikuline. `iss` sellisena, nagu Google selle tagasi saatis, kui ta selle saatis.
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
    SsoLinkRequest:
      type: object
      additionalProperties: false
      required:
        - linkTicket
        - password
      properties:
        linkTicket:
          type: string
          minLength: 1
          maxLength: 64
          description: '`linkTicket` sammu `finish` vastusest, kehtib 10 minutit.'
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Konto parool.
        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` sammu `finish` vastusest, kehtib 10 minutit.'
        username:
          type: string
          minLength: 1
          maxLength: 64
          description: Kasutajanimi; kehtivad registreerumise reeglid.
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
    SessionList:
      type: object
      required:
        - sessions
      properties:
        sessions:
          type: array
          description: Aktiivsed seansid, viimati kasutatud esimesena.
          items:
            $ref: '#/components/schemas/SessionEntry'
    SessionEntry:
      type: object
      description: Aktiivne seanss (sisselogitud seade).
      required:
        - id
        - createdAt
        - lastSeenAt
        - expiresAt
        - clientLabel
        - current
      properties:
        id:
          type: integer
          format: int64
          description: Seansi ID otspunkti `DELETE /auth/sessions/{id}` jaoks.
        createdAt:
          type: integer
          format: int64
          description: Sisselogimine (Unixi aeg, ms).
        lastSeenAt:
          type: integer
          format: int64
          description: Viimane kasutus (Unixi aeg, ms), uuendatakse mitte sagedamini kui iga 5 minuti järel (hiljem, kui server on tööga kaugele maha jäänud).
        expiresAt:
          type: integer
          format: int64
          description: Absoluutne lõpp (Unixi aeg, ms); jõudeoleku piirang võib seansi varem lõpetada.
        clientLabel:
          type:
            - string
            - 'null'
          description: Sisselogimisel saadetud `clientLabel` või `null`, kui seda ei saadetud.
        current:
          type: boolean
          description: Kas see on seanss, mis päringu teeb.
    AccountMe:
      type: object
      required:
        - user
        - ratings
        - sanctions
        - ban
      properties:
        user:
          $ref: '#/components/schemas/AccountView'
        ratings:
          type: array
          description: Üks kirje iga kategooria kohta, milles mängija on reitinguga partiisid mänginud.
          items:
            $ref: '#/components/schemas/RatingSummary'
        sanctions:
          type: array
          description: Aktiivsed sanktsioonid.
          items:
            $ref: '#/components/schemas/ActiveSanction'
        ban:
          description: 'Peatamise ajal `{ until }`, muidu `null`.'
          oneOf:
            - $ref: '#/components/schemas/Ban'
            - type: 'null'
    RatingSummary:
      type: object
      description: Ühe kategooria reitingukirje.
      required:
        - category
        - rating
        - games
        - wins
        - draws
        - losses
        - peak
        - provisional
      properties:
        category:
          type: string
          description: Kategooria ID.
        rating:
          type: integer
          description: Reiting.
        games:
          type: integer
          minimum: 0
          description: Kategoorias mängitud partiid (arvesse läinud või mitte).
        wins:
          type: integer
          minimum: 0
        draws:
          type: integer
          minimum: 0
        losses:
          type: integer
          minimum: 0
        peak:
          type: integer
          description: Kõrgeim saavutatud reiting.
        provisional:
          type: boolean
          description: '`true`, kuni reiting on reitinguta faasis või sellel on vähem kui `PROVISIONAL_GAMES` arvesse läinud partiid (mäng näitab seda kujul `1510?`).'
    ActiveSanction:
      type: object
      required:
        - kind
        - reason
        - startsAt
        - endsAt
      properties:
        kind:
          type: string
          enum:
            - ban
            - mm_block
            - warning
          description: Sanktsiooni liik.
        reason:
          type:
            - string
            - 'null'
          description: Antud põhjus, kui see on olemas.
        startsAt:
          type: integer
          format: int64
          description: Algus (Unixi aeg, ms).
        endsAt:
          type:
            - integer
            - 'null'
          format: int64
          description: Lõpp (Unixi aeg, ms), alalise sanktsiooni korral `null`.
    Ban:
      type: object
      required:
        - until
      properties:
        until:
          type:
            - integer
            - 'null'
          format: int64
          description: Peatamise lõpp (Unixi aeg, ms), alalise peatamise korral `null`.
    Preferences:
      type: object
      additionalProperties: false
      required:
        - acceptChallenges
      properties:
        acceptChallenges:
          type: string
          enum:
            - all
            - none
          description: '`all` otseste väljakutsete vastuvõtmiseks nime järgi, `none` nende tagasilükkamiseks.'
    PasswordChangeRequest:
      type: object
      additionalProperties: false
      required:
        - currentPassword
        - newPassword
      properties:
        currentPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Praegune parool.
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Uus parool; kehtivad registreerumise paroolireeglid.
    PasswordOnlyRequest:
      type: object
      additionalProperties: false
      required:
        - password
      properties:
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Konto parool.
    TotpEnableRequest:
      type: object
      additionalProperties: false
      required:
        - code
      properties:
        code:
          type: string
          pattern: '^[0-9]{6}$'
          description: Ootel salajase võtme kood, täpselt 6 numbrit.
    ReauthCredentials:
      type: object
      additionalProperties: false
      required:
        - password
      properties:
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Konto parool.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: Valikuline; vajalik kaheastmelise autentimise korral (või `recoveryCode`). Autentimisrakenduse kood või taastekood.
        recoveryCode:
          type: string
          minLength: 1
          maxLength: 32
          description: Valikuline. Taastekood (`xxxx-xxxx-xx`; tõstul, tühikutel ja sidekriipsudel pole tähtsust).
    RecoveryCodesRequest:
      type: object
      additionalProperties: false
      required:
        - password
        - code
      properties:
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Konto parool.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: Autentimisrakenduse kood (taastekood lükatakse tagasi).
    EmailChangeRequest:
      type: object
      additionalProperties: false
      required:
        - newEmail
        - password
      properties:
        newEmail:
          type: string
          minLength: 1
          maxLength: 254
          description: Uus aadress; algus- ja lõputühikud eemaldatakse ja see teisendatakse väiketähtedeks, seejärel kontrollitakse seda nagu registreerumisel antud aadressi.
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Konto parool.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: Valikuline; vajalik kaheastmelise autentimise korral (või `recoveryCode`). Autentimisrakenduse kood või taastekood.
        recoveryCode:
          type: string
          minLength: 1
          maxLength: 32
          description: Valikuline. Taastekood.
    TotpSetup:
      type: object
      description: Ootel salajane võti autentimisrakenduse jaoks.
      required:
        - secret
        - uri
        - algorithm
        - digits
        - period
      properties:
        secret:
          type: string
          pattern: '^[A-Z2-7]+$'
          description: Salajane võti base32 kujul, rakendusse sisestamiseks.
        uri:
          type: string
          format: uri
          description: URI `otpauth://totp/...`, mida näidata QR-koodina.
        algorithm:
          type: string
          const: SHA1
        digits:
          type: integer
          const: 6
        period:
          type: integer
          const: 30
          description: Sekundeid sammu kohta.
    RecoveryCodeList:
      type: array
      description: 10 taastekoodi, mida näidatakse ainult see üks kord. Iga kood töötab ühe korra.
      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: Partii üks pool.
      required:
        - name
        - rating
        - ratingAfter
        - ratingDiff
      properties:
        name:
          type: string
          description: Nimi partiikirjes (kustutatud konto puhul `deleted#<id>`).
        rating:
          type:
            - integer
            - 'null'
          description: Reiting alguses, `null`, kui see pole teada.
        ratingAfter:
          type:
            - integer
            - 'null'
          description: Reiting pärast partiid. Reitinguga partiil on see alati olemas; `null` ainult partii puhul, mis reitingutes arvesse ei lähe (reitinguta, kohandatud, katkestatud).
        ratingDiff:
          type:
            - integer
            - 'null'
          description: Reitingu muutus; `0`, kui reitingureeglid jätavad reitingu samaks (FIDE nullitulemus, reitinguta vastane, reitinguta mängija esimesed partiid); `null` samadel juhtudel kui `ratingAfter`.
    GameSummary:
      type: object
      description: Salvestatud partii kokkuvõte.
      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: Partii ID.
        category:
          type: string
          description: Ametliku kategooria ID (`3+2`) või `custom`.
        rated:
          type: boolean
          description: Kas partii läheb reitingutes arvesse.
        timeControl:
          type: string
          pattern: '^[0-9]+\+[0-9]+$'
          description: Ajakontroll sekundites (`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` (vt partiide koode).'
        reason:
          type: integer
          minimum: 0
          maximum: 255
          description: Lõpu põhjuse kood (vt partiide koode).
        result:
          type: string
          enum:
            - 1-0
            - 0-1
            - 1/2-1/2
            - '*'
          description: Tulemus, katkestatud partii puhul `*`.
        termination:
          $ref: '#/components/schemas/Termination'
        plies:
          type: integer
          minimum: 0
          description: Poolkäikude arv.
        startedAt:
          type: integer
          format: int64
          description: Algus (Unixi aeg, ms).
        endedAt:
          type: integer
          format: int64
          description: Lõpp (Unixi aeg, ms).
    Termination:
      type: string
      description: Lõpu põhjuse nimi (vt partiide koode); `Unknown` koodi puhul, mida protokoll ei tunne.
      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: Partii kokkuvõte ühe mängija vaatenurgast.
      allOf:
        - $ref: '#/components/schemas/GameSummary'
        - type: object
          required:
            - color
          properties:
            color:
              type: string
              enum:
                - white
                - black
              description: Mängija pool.
    HistoryGameSummary:
      description: Partii sisselogitud mängija ajaloost.
      allOf:
        - $ref: '#/components/schemas/PlayerGameSummary'
        - type: object
          required:
            - baseMs
            - incMs
            - outcome
          properties:
            baseMs:
              type: integer
              format: int64
              minimum: 0
              description: Põhiaeg millisekundites.
            incMs:
              type: integer
              format: int64
              minimum: 0
              description: Lisaaeg millisekundites.
            outcome:
              type: string
              enum:
                - win
                - loss
                - draw
                - aborted
              description: Tulemus mängija vaatenurgast.
    HistoryPage:
      type: object
      required:
        - games
        - next
        - total
      properties:
        games:
          type: array
          description: Lehekülje partiid, uusimad esimesena.
          items:
            $ref: '#/components/schemas/HistoryGameSummary'
        next:
          type:
            - integer
            - 'null'
          format: int64
          description: ID, mis tuleb järgmise lehekülje jaoks anda parameetrina `before`, või viimasel leheküljel `null`.
        total:
          type: integer
          minimum: 0
          description: Filtrile vastavate partiide arv kõigil lehekülgedel kokku.
    MoveRecord:
      type: object
      description: Partii üks poolkäik.
      required:
        - uci
        - spentMs
        - clockMs
      properties:
        uci:
          type: string
          pattern: '^[a-h][1-8][a-h][1-8][nbrq]?$'
          description: Käik UCI-noteeringus; muundumise korral lisatakse `n`, `b`, `r` või `q`.
        spentMs:
          type:
            - integer
            - 'null'
          minimum: 0
          description: Käigu eest arvestatud aeg (ms), `null`, kui kirjes seda pole.
        clockMs:
          type:
            - integer
            - 'null'
          minimum: 0
          description: Käigu teinud mängija kell pärast käiku (ms), `null`, kui kirjes seda pole.
    PgnTagsView:
      type: object
      description: Peamised PGN-märgendid kuvamiseks. `Termination` on siin lõpu põhjuse nimi; PGN-fail kasutab PGN-standardi väärtusi.
      required:
        - Event
        - Site
        - Date
        - Round
        - White
        - Black
        - Result
        - WhiteElo
        - BlackElo
        - TimeControl
        - Termination
        - PlyCount
      properties:
        Event:
          type: string
          description: '`<SERVER_NAME> rated <category>` või `<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: Alguskuupäev UTC järgi.
        Round:
          type: string
          const: '-'
        White:
          type: string
        Black:
          type: string
        Result:
          type: string
          enum:
            - 1-0
            - 0-1
            - 1/2-1/2
            - '*'
        WhiteElo:
          description: Reiting alguses või `"-"`.
          oneOf:
            - type: integer
            - type: string
              const: '-'
        BlackElo:
          description: Reiting alguses või `"-"`.
          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: Partiikirje koos käikude ja kelladega. Sellel on partii kokkuvõtte väljad, ilma väljadeta `color` ja `outcome`.
      allOf:
        - $ref: '#/components/schemas/GameSummary'
        - type: object
          required:
            - baseMs
            - incMs
            - statusName
            - rematchOf
            - moves
            - pgn
          properties:
            baseMs:
              type: integer
              format: int64
              minimum: 0
              description: Põhiaeg millisekundites.
            incMs:
              type: integer
              format: int64
              minimum: 0
              description: Lisaaeg millisekundites.
            statusName:
              type: string
              enum:
                - WhiteWins
                - BlackWins
                - Draw
                - Aborted
              description: Väärtuse `status` nimi.
            rematchOf:
              type:
                - integer
                - 'null'
              format: int64
              description: Selle partii ID, mille revanš see partii on, või `null`.
            moves:
              type: array
              description: Üks kirje poolkäigu kohta.
              items:
                $ref: '#/components/schemas/MoveRecord'
            pgn:
              $ref: '#/components/schemas/PgnTagsView'
            you:
              type: string
              enum:
                - white
                - black
              description: Ainult siis, kui tokeni mängija mängis seda partiid; tema pool.
            reportable:
              type: boolean
              description: Ainult siis, kui tokeni mängija mängis seda partiid; `true`, kui `POST /reports` võtaks praegu vastu teate selle partii vastase kohta (partii lõppes 7 päeva jooksul, päevakvoot pole ära kasutatud ja vastasest pole selle partii kohta juba teatatud).
    PlayerProfile:
      type: object
      required:
        - username
        - createdAt
        - ratings
        - games
      properties:
        username:
          type: string
          description: Kasutajanimi sellisena, nagu mängija selle kirjutas.
        createdAt:
          type: integer
          format: int64
          description: Millal konto loodi (Unixi aeg, ms).
        ratings:
          type: array
          description: Ainult ametlikud kategooriad, serveri järjekorras.
          items:
            $ref: '#/components/schemas/RatingSummary'
        games:
          type: object
          required:
            - total
            - rated
            - wins
            - draws
            - losses
          properties:
            total:
              type: integer
              minimum: 0
              description: Kõik salvestatud partiid, kaasa arvatud reitinguta ja katkestatud partiid.
            rated:
              type: integer
              minimum: 0
              description: Reitinguga partiid, summeeritud üle reitingukirjete.
            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: Kasutajanimi sellisena, nagu mängija selle kirjutas.
        games:
          type: array
          description: Lehekülje partiid, uusimad esimesena.
          items:
            $ref: '#/components/schemas/PlayerGameSummary'
        next:
          type:
            - integer
            - 'null'
          format: int64
          description: Viimase partii ID alati, kui lehekülg on täis (järgmine lehekülg võib siis olla tühi), muidu `null`.
    Leaderboard:
      type: object
      required:
        - category
        - minGames
        - updatedAt
        - players
      properties:
        category:
          type: string
          description: Kategooria ID.
        minGames:
          type: integer
          minimum: 0
          description: Arvesse läinud partiide arv, mida kirje vajab loendisse pääsemiseks (`PROVISIONAL_GAMES`).
        updatedAt:
          type: integer
          format: int64
          description: Millal edetabel arvutati (Unixi aeg, 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-tekst, kuni 65 536 baiti UTF-8 kodeeringus. Kasutatakse ainult esimest partiid.
        size:
          type: string
          enum:
            - small
            - medium
            - large
          default: medium
          description: Valikuline. Pildi suurus.
        orientation:
          type: string
          enum:
            - white
            - black
          default: white
          description: Valikuline. Pool, mis on laua alumises servas.
        delayMs:
          type: integer
          minimum: 100
          maximum: 3000
          default: 500
          description: Valikuline. Millisekundeid käigu kohta, täisarvulise väärtusega JSON-arv.
        coords:
          type: boolean
          default: true
          description: Valikuline. Kas vertikaalide tähed ja horisontaalide numbrid joonistatakse laua ümber.
    ReportRequest:
      type: object
      required:
        - gameId
        - reported
        - category
      properties:
        gameId:
          description: Partii, täisarvuna või 1 kuni 16 numbrist koosneva stringina.
          oneOf:
            - type: integer
              format: int64
              minimum: 1
              maximum: 9007199254740991
            - type: string
              pattern: '^[0-9]{1,16}$'
        reported:
          type: string
          minLength: 1
          maxLength: 24
          description: Vastase kasutajanimi, nagu see on partiikirjes või nagu see on praegu, tõstust arvestamata. See ei tohi olla tühi.
        category:
          type: string
          enum:
            - cheating
            - abuse
            - other
          description: Mida teade puudutab.
        comment:
          type:
            - string
            - 'null'
          description: Valikuline. Kuni 500 märki pärast seda, kui juhtmärgid (v.a tabeldusmärk ja reavahetus) on eemaldatud ning tekstilt on eemaldatud algus- ja lõputühikud.
    AccountExport:
      type: object
      description: Kõik, mida server konto kohta hoiab (ajad Unixi ajana, 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: Millal eksport tehti.
        server:
          type: object
          required:
            - name
            - host
          properties:
            name:
              type: string
              description: '`SERVER_NAME`.'
            host:
              type: string
              description: '`SERVER_PUBLIC_HOST`.'
        notes:
          type: array
          description: Mida fail sisaldab ja mis on sellest välja jäetud, lihtsas inglise keeles mängija jaoks.
          items:
            type: string
        account:
          description: Otspunkti `GET /account/me` konto vaade koos väljaga `googleEmail`.
          allOf:
            - $ref: '#/components/schemas/AccountView'
            - type: object
              required:
                - googleEmail
              properties:
                googleEmail:
                  type:
                    - string
                    - 'null'
                  description: Seotud Google'i konto aadress või `null`.
        ratings:
          type: array
          description: Täielikud reitingukirjed.
          items:
            $ref: '#/components/schemas/ExportRating'
        ratingRefunds:
          type: array
          description: Reitingupunktid, mis tagastati pärast seda, kui vastane tabati sohitegemiselt, kokku liidetud UTC päeva ja kategooria kaupa, uusimad esimesena. Ei partiisid ega sohitegijaid nimetata.
          items:
            $ref: '#/components/schemas/RatingRefund'
        sessions:
          type: array
          description: Kõik salvestatud seansid, uusimad esimesena, ilma ühegi tokenita. Säilitusaja puhastus kustutab aegunud seansi ning välja logitud seansi üks päev pärast väljalogimist.
          items:
            $ref: '#/components/schemas/ExportSession'
        securityEvents:
          type: array
          description: Turvasündmused, uusimad esimesena; säilitusaeg `RETENTION_SECURITY_DAYS`.
          items:
            $ref: '#/components/schemas/SecurityEvent'
        sanctions:
          type: array
          description: Kõik sanktsioonid, kaasa arvatud tühistatud. Moderaatori nime ei lisata kunagi.
          items:
            $ref: '#/components/schemas/ExportSanction'
        conduct:
          type: array
          description: Käitumissündmused, mis on salvestatud mängija nende partiide kohta, mis lõppesid partiist lahkumise, katkestamise või tegemata esimese käiguga; säilitatakse 30 päeva.
          items:
            $ref: '#/components/schemas/ConductEvent'
        reportsFiled:
          type: array
          description: Mängija esitatud teated.
          items:
            $ref: '#/components/schemas/FiledReport'
        games:
          type: object
          required:
            - total
            - list
          properties:
            total:
              type: integer
              minimum: 0
              description: Partiide arv.
            list:
              type: array
              description: Kõik partiid, uusimad esimesena, otspunkti `GET /account/games` kokkuvõtetena. Käigud on saadaval otspunktis `GET /games/{id}` ja PGN otspunktis `GET /games/{id}/pgn`.
              items:
                $ref: '#/components/schemas/HistoryGameSummary'
    ExportRating:
      description: Täielik reitingukirje.
      allOf:
        - $ref: '#/components/schemas/RatingSummary'
        - type: object
          required:
            - rated
            - countedGames
            - updatedAt
          properties:
            rated:
              type: boolean
              description: Kas mängija on reitinguta faasist väljunud.
            countedGames:
              type: integer
              minimum: 0
              description: Partiid, mis läksid reitingus arvesse.
            updatedAt:
              type: integer
              format: int64
              description: Millal kirje viimati muutus.
    RatingRefund:
      type: object
      required:
        - day
        - category
        - points
      properties:
        day:
          type: integer
          format: int64
          description: Päeva kell 00:00 UTC (Unixi aeg, ms).
        category:
          type: string
        points:
          type: integer
          description: Sel päeval selles kategoorias tagastatud punktid.
    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: Millal seanss välja logiti, või `null`.
        clientLabel:
          type:
            - string
            - 'null'
        ip:
          type:
            - string
            - 'null'
          description: Sisselogimise aadress, kustutatakse pärast aega `RETENTION_IP_DAYS`.
    SecurityEvent:
      type: object
      description: |-
        Turvasündmus. `ip` antakse ainult toimingute puhul, mis tehti sisselogituna, konto parooli (ja teise teguriga) või konto aadressile saadetud lingiga: `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` ja `account_exported`; kõigil teistel liikidel on `ip: null` ning `ip` kustutatakse pärast aega `RETENTION_IP_DAYS`.

        `detail` säilitab ainult need väljad: `login`: `method`; `sso_login`, `sso_account_created`: `provider`; `sso_linked`: `provider` ja `method` (`password` või `password+totp`); `login_failed`: `failures`; `login_lockout`: `retryAfterMs`; `mfa_failed`: `attempts`; `recovery_code_used`: `remaining`; `reauth_failed`: `factor`; `session_revoked` ja `sessions_revoked_all`: `reason`; `email_change_refused`: `reason`; `sanction_auto`: `kind`, `gameId`, `until`. Sündmus `moderator_action` säilitab ainult `{ action }` toimingute `ban`, `unban`, `reset_mfa`, `verify_email` ja `revoke_sessions` puhul (teised moderaatori toimingud jäetakse välja). Kõigil teistel liikidel on `detail: null`. Sündmused `rating_refund` jäetakse välja: nende punktid on väljal `ratingRefunds`.
      required:
        - kind
        - at
        - ip
        - detail
      properties:
        kind:
          type: string
          description: Sündmuse liik (`login`, `password_changed`…).
        at:
          type: integer
          format: int64
          description: Millal see juhtus.
        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: Mängija, kellest teatati, praegune avalik nimi.
        category:
          type: string
          enum:
            - cheating
            - abuse
            - other
        comment:
          type:
            - string
            - 'null'
        createdAt:
          type: integer
          format: int64
        status:
          type: string
          enum:
            - open
            - closed
          description: Kas teade on veel avatud (kas mängijat, kellest teatati, sanktsioneeriti, ei öelda).
    LinkTokenForm:
      type: object
      additionalProperties: false
      required:
        - token
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: E-posti lingi token (lehe vormi peidetud väli).
    ResetPasswordForm:
      type: object
      additionalProperties: false
      required:
        - token
        - newPassword
        - confirmPassword
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: Lähtestamislingi token (vormi peidetud väli).
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Uus parool; kehtivad registreerumise paroolireeglid.
        confirmPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Uus parool uuesti; see peab olema sama.
    HtmlDocument:
      type: string
      contentMediaType: text/html
      description: HTML-leht UTF-8 kodeeringus (`text/html; charset=utf-8`), ilma JavaScripti ja väliste ressurssideta.
