openapi: 3.1.1
info:
  title: HTTPS API servera Scacelith
  version: "1.1.0"
  summary: HTTPS API každého servera Scacelith, oficiálneho aj komunitných serverov.
  description: |-
    Toto HTTPS API poskytuje každý server Scacelith, oficiálny `caissa.scacelith.com` aj ktorýkoľvek komunitný server. Hra ho používa na všetko mimo samotnej partie: registráciu a prihlásenie (vrátane dvojfaktorového overenia a prihlásenia cez Google), stránku účtu, históriu partií, sťahovanie PGN a animovaných GIF-ov partií, prihlásené zariadenia, stiahnutie údajov, odstránenie účtu a nahlásenia.

    Hra v reálnom čase (hľadanie súpera, výzvy, ťahy, hodiny) prebieha cez WebSocket toho istého servera (`wss://<host>/ws`), ktorý sa otvára s tokenom relácie z tohto API; popisuje ho `PROTOCOL.md`, nie táto stránka.

    Predvolené hodnoty uvedené nižšie platia pre server s nezmenenou konfiguráciou; komunitný server ich môže zmeniť (`CONFIG.md` uvádza každé nastavenie).

    ## Základná URL, port a transport

    - Oficiálny server: `https://caissa.scacelith.com/api/v1`, port TCP 443.
    - Komunitný server: `https://<SERVER_PUBLIC_HOST>[:<port>]/api/v1`. `API_PORT` je predvolene 443; za NAT alebo proxy je port, ktorý používajú hráči, `PUBLIC_API_PORT`.
    - WebSocket hry je predvolene na tom istom porte (`WS_PORT` ho presúva inam; `GET /info` oznámi klientovi, kde je).
    - HTTP/1.1 cez TLS. Pri `TLS_MODE=proxy` ukončuje TLS reverzná proxy pred serverom. `TLS_MODE=off` (čisté HTTP) je určené iba na lokálny vývoj.
    - Stránky HTML otvárané z odkazov v e-mailoch (`/verify-email`, `/reset-password`, `/confirm-email-change`) sú v koreni servera, mimo `/api/v1`. Endpointy kontroly stavu odpovedajú v koreni aj pod `/api/v1`. Prihlásenie cez Google nemá na serveri žiadnu stránku: Google pošle prehliadač späť priamo do hry, na `127.0.0.1`, alebo na stránku webu, ktorý prihlásenie začal (`GOOGLE_WEB_REDIRECT_URI`).
    - Koncová lomka sa ignoruje (`/api/v1/info/` je `/api/v1/info`) a parametre cesty sa dekódujú z kódovania URL (na parameter, ktorý nie je platne kódovaný pre URL, server odpovie 400 `invalid_request`).

    ## Požiadavky

    - **Telá JSON.** Požiadavky `POST`, `PUT` a `DELETE` nesú objekt JSON s `Content-Type: application/json`. Parameter `charset` s inou hodnotou ako UTF-8 sa odmieta, rovnako ako akýkoľvek iný typ obsahu (415 `unsupported_media_type`). Prázdne telo sa počíta ako `{}`, čo je presne to, čo prijímajú endpointy bez parametrov (`POST /auth/logout`, `POST /auth/logout-all`, `DELETE /auth/sessions/{id}`).
    - **Veľkosť a čas.** Telo je obmedzené na `HTTP_BODY_LIMIT` bajtov (predvolene 16 384; `POST /gif` má vlastný limit 135 168 bajtov): 413 `payload_too_large`. Musí doraziť do 10 sekúnd: 408 `request_timeout`. Po ktorejkoľvek z týchto chýb server zatvorí spojenie.
    - **Prísne schémy.** Pole, ktoré endpoint nepozná, sa odmietne, pole neoznačené ako nepovinné je povinné a kontrolujú sa typy a dĺžky. Pri ktoromkoľvek z týchto zlyhaní server odpovie 400 `invalid_request` a `field` uvedie názov poľa (pri vnorenom poli v zápise s bodkami, napríklad `pow.nonce`). Reťazce nesmú obsahovať riadiace znaky. Dĺžky sa počítajú v kódových jednotkách UTF-16, takže znak mimo základnej viacjazyčnej roviny (Basic Multilingual Plane), napríklad emoji, sa počíta dvakrát. Na telo, ktoré nie je JSON, server odpovie 400 `invalid_json`. `POST /gif` a `POST /reports` kontrolujú svoje telá samy (pozri jednotlivé endpointy).
    - **Reťazce dopytu.** Platí prvý výskyt parametra, neznáme parametre sa ignorujú a `+` sa dekóduje na medzeru: endpointy, ktoré prijímajú kategóriu tempa hry, akceptujú `3%2B2` aj `3+2`.
    - **Metódy.** `HEAD` je `GET` bez tela. Na `OPTIONS` na existujúcej ceste server odpovie 204 s hlavičkou `Allow` (a s hlavičkami odpovede na predbežnú požiadavku CORS, pozri nižšie). Na akúkoľvek inú metódu, ktorú cesta nemá, odpovie 405 `method_not_allowed` s `Allow`.
    - **Cieľ požiadavky.** Nesmie presiahnuť 4096 znakov (414 `uri_too_long`). Požiadavka, ktorú vôbec nemožno analyzovať alebo ktorej hlavičková časť je príliš veľká či prichádza príliš pomaly, dostane prázdnu odpoveď 400, 431 alebo 408 a spojenie sa zatvorí.
    - **CORS: zoznam povolených, predvolene vypnutý.** API slúži hre, ktorá neposiela `Origin`. Keď je `CORS_ORIGINS` prázdne (predvolené nastavenie), nikdy sa neposiela žiadna hlavička `Access-Control-*`, takže žiadna webová stránka nemôže odpoveď prečítať. `CORS_ORIGINS` uvádza webové pôvody, ktorých stránky môžu volať API z prehliadača, každý presne tak, ako ho prehliadače posielajú v `Origin`: `https://host` alebo `https://host:port`, malými písmenami, bez cesty, koncovej lomky a predvoleného portu; `http://` iba pre `localhost`, `127.0.0.1` a `[::1]` (lokálny vývoj); bez zástupných znakov. Oficiálny server nastavuje pre svoj web `CORS_ORIGINS=https://scacelith.com,https://www.scacelith.com`. Potom:
      - Každá odpoveď pre cestu pod `/api/v1` okrem endpointov kontroly stavu, úspešná aj chybová (vrátane 429 vrstvy podľa adresy), nesie `Vary: Origin`. Keď sa `Origin` požiadavky bajt po bajte zhoduje s jedným z uvedených pôvodov, nesie aj `Access-Control-Allow-Origin: <that origin>` a, okrem odpovede na predbežnú požiadavku, `Access-Control-Expose-Headers: Retry-After, Content-Disposition`, aby stránka mohla prečítať kódy chýb, `Retry-After` a názvy súborov pri sťahovaní PGN a GIF.
      - Na predbežnú požiadavku (`OPTIONS` s `Access-Control-Request-Method`) z uvedeného pôvodu na existujúcu cestu server odpovie 204, bez tela, s `Allow`, `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods` (metódy z `Allow`), `Access-Control-Allow-Headers: Authorization, Content-Type`, `Access-Control-Max-Age: 600` a `Vary: Origin`. Nepotrebuje token a nezapočítava sa do kvóty účtu ani do žiadneho limitu endpointu; ako každá požiadavka vezme jeden žetón vrstvy podľa adresy. Prehliadač opätovne využíva predbežnú požiadavku 10 minút, ale iba pre presne tú istú URL vrátane reťazca dopytu. Požiadavka bez `Authorization` a bez tela JSON, napríklad `GET /info` alebo `GET /leaderboard`, predbežnú požiadavku nepotrebuje.
      - Akákoľvek iná odpoveď na predbežnú požiadavku znamená neúspešnú predbežnú požiadavku, aj keď nesie hlavičky CORS: 404 pre cestu, ktorá neexistuje, alebo 429 vrstvy podľa adresy. Prehliadač potom požiadavku neodošle a stránka uvidí sieťovú chybu bez odpovede aj bez jej `Retry-After`.
      - `Access-Control-Allow-Credentials` sa nikdy neposiela, ani `Access-Control-Allow-Origin: *`: stránka posiela svoj token relácie v `Authorization: Bearer`, rovnako ako hra, a API nenastavuje žiadne cookie. Stránky HTML, endpointy kontroly stavu a prázdne odpovede na nečitateľné požiadavky nenesú žiadnu hlavičku CORS. WebSocket CORS nepoužíva: `WS_ALLOWED_ORIGINS` uvádza stránky, ktoré ho môžu otvoriť.
      - Stránka z neuvedeného pôvodu nič neprečíta a jej zápisy z iného webu (cross-site) sa nikdy neodošlú: telá JSON a `Authorization` vyžadujú predbežnú požiadavku, ktorej odpoveď nemá pre tento pôvod `Access-Control-Allow-Origin`. `Cross-Origin-Resource-Policy: same-origin` zostáva na každej odpovedi: vzťahuje sa iba na požiadavky `no-cors` (iný web vkladá odpoveď do svojej stránky), nikdy na požiadavky `cors` stránky z uvedeného pôvodu.

    ## Odpovede a chyby

    - Odpovede sú JSON v UTF-8, okrem sťahovania PGN (`application/x-chess-pgn`), animovaných GIF-ov (`image/gif`) a stránok HTML. Časy sú v milisekundách od 1970-01-01 UTC. ID sú celé čísla. ID partií majú až 16 číslic, ale zostávajú pod 2^53, takže ich číslo JSON (double) vyjadrí presne.
    - Každá odpoveď API nesie `Cache-Control: no-store`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`, `X-Frame-Options: DENY`, `Cross-Origin-Resource-Policy: same-origin` a `Content-Security-Policy: default-src 'none'; frame-ancestors 'none'` (stránky HTML majú vlastnú politiku). Pri `TLS_MODE=native` nesie aj `Strict-Transport-Security: max-age=31536000`. Odpovede, ktoré zapisuje sám poslucháč ešte pred API (`GET` a `HEAD` endpointov kontroly stavu, 429 vrstvy podľa adresy a 500 `internal_error`, keď API úplne zlyhá), nesú tie isté hlavičky okrem `Cross-Origin-Resource-Policy` a ich `Content-Type` je `application/json` bez parametra charset.
    - Odpoveď musí byť prečítaná do 60 sekúnd od chvíle, keď ju má server pripravenú, na čom záleží iba pri veľkých odpovediach (GIF, export údajov, dlhé PGN). Čas, ktorý server potrebuje na prípravu odpovede, sa nezapočítava ani do tohto limitu, ani do 30 sekúnd bez prijatého či odoslaného bajtu, po ktorých sa spojenie zatvorí.

    Chyby majú jeden tvar (schéma `Error`):

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

    `message` je určené pre logy a ako záložný text; klient vyberá, čo zobrazí, podľa `error`. `retryAfter` (sekundy) je prítomné iba pri odmietnutiach, ktoré po čase pominú, a odpoveď potom nesie aj hlavičku `Retry-After` s rovnakou hodnotou; jedinou výnimkou je chyba 503 `busy` pri čítaní histórie, partií, hráčov a rebríčka, ktorá má iba toto pole. Niektoré chyby pridávajú polia: `field` (neplatný vstup), `reason` (`weak_password`, `pow_required`), `pow` (`pow_required`), `until` (`banned`), `line` a `column` (`invalid_pgn`).

    Chyby, ktoré môže vrátiť ktorýkoľvek endpoint:

    | Stav | `error` | Kedy |
    |---|---|---|
    | 400 | `invalid_request` | Telo porušuje schému endpointu (`field` uvádza kde), cieľ požiadavky alebo `Content-Length` má chybný formát, parameter cesty nie je platne kódovaný pre URL alebo bolo telo odrezané. |
    | 400 | `invalid_json` | Telo nie je JSON. |
    | 401 | `unauthorized` | Chýba hlavička `Authorization` na endpointe, ktorý vyžaduje reláciu. |
    | 401 | `invalid_token` | Token má chybný formát, vypršala jeho platnosť, bol odvolaný alebo patrí odstránenému účtu; aj na endpointoch, kde je relácia nepovinná. |
    | 404 | `not_found` | Takýto endpoint neexistuje. Používajú ho aj niektoré endpointy: takáto partia, hráč alebo relácia neexistuje. |
    | 405 | `method_not_allowed` | Cesta existuje pre iné metódy (pozri `Allow`). |
    | 408 | `request_timeout` | Telo nedorazilo do 10 sekúnd. |
    | 413 | `payload_too_large` | Telo presahuje `HTTP_BODY_LIMIT` (pre `POST /gif` 135 168 bajtov). |
    | 414 | `uri_too_long` | Cieľ požiadavky presahuje 4096 znakov. |
    | 415 | `unsupported_media_type` | Telo nie je `application/json` alebo jeho znaková sada nie je UTF-8. |
    | 429 | `rate_limited` | Limit počtu požiadaviek (pozri nižšie): `retryAfter` a k tomu hlavička `Retry-After`. |
    | 500 | `internal_error` | Neočakávané zlyhanie. Server ho zapíše do logu. |
    | 503 | `timeout` | Server neodpovedal do 30 sekúnd (pri exporte do 60 sekúnd, pri GIF-och s predvolenými nastaveniami do 45 sekúnd). |
    | 503 | `server_busy` | Vyhľadanie relácie alebo zmena účtu narazili na zamknutú databázu (`retryAfter` 1) alebo je front na hashovanie hesiel plný (pozri nižšie). Tiež s `retryAfter` 2 až 5, keď server výrazne zaostáva: pri `POST`, `PUT`, `PATCH` alebo `DELETE` čaká v databáze príliš veľa zápisov; pri akejkoľvek požiadavke stále beží príliš veľa skorších požiadaviek po uplynutí ich časového limitu alebo token relácie nebolo možné overiť do 5 sekúnd. Požiadavka nebola vykonaná a z jej limitov počtu požiadaviek sa nič neodpočítalo. |

    Endpointy na čítanie (história partií, partie, hráči, rebríček) a export odpovedajú 503 `busy` s `retryAfter: 1`, keď databáza zostala zamknutá.

    Endpointy, ktoré overujú alebo hashujú heslo, môžu odpovedať aj jednou z týchto chýb, obe s náhodným `retryAfter` 5 až 15 sekúnd:

    - 503 `server_busy`: front na hashovanie hesiel servera (`PASSWORD_HASH_QUEUE_MAX`) je plný alebo vypršal čas čakania.
    - 429 `rate_limited`: front je zaplnený aspoň do polovice a tento klient (adresa IPv4 alebo IPv6 /48) už v ňom má `PASSWORD_HASH_WAITERS_PER_SOURCE` čakajúcich hashov. Táto odpoveď 429 vráti žetóny limitov počtu požiadaviek, ktoré požiadavka spotrebovala.

    Nič sa nezmenilo a nezapočítal sa žiadny neúspešný pokus (okrem `POST /auth/sso/google/link`, pri ktorom sa pokus tiketu a zlyhanie účtu započítali ešte pred hashovaním) a odkaz na resetovanie hesla zostáva platný.

    Stránky HTML vracajú svoje chyby ako stránky HTML s rovnakými stavovými kódmi.

    ## Autentifikácia

    Endpointy, ktoré vyžadujú reláciu, prijímajú token typu bearer v hlavičke `Authorization` (schéma `bearerAuth`):

    ```
    Authorization: Bearer sct_L_8GDd7uzfQ3QQWtqrsWXDTsFWzRwIvJcwIGHhjWPS8
    ```

    - **Získanie tokenu.** Token (`sct_`, za ktorým nasleduje 43 znakov base64url) vydáva `POST /auth/login`, potom `POST /auth/login/mfa`, keď je zapnuté dvojfaktorové overenie, ďalej `POST /auth/sso/google/finish` a `POST /auth/sso/google/link` (prihlásenie cez Google) a `POST /auth/sso/complete`. Každý z nich odpovedá `{ token, expiresAt, user }`. Server ukladá iba SHA-256 tokenu.
    - **Povinná relácia.** Na chýbajúcu hlavičku server odpovie 401 `unauthorized` s `WWW-Authenticate: Bearer realm="scacelith"`. Na neplatný token odpovie 401 `invalid_token` s `WWW-Authenticate: Bearer realm="scacelith", error="invalid_token"`. Klient by mal token, ktorý dostane `invalid_token`, zabudnúť a znova sa prihlásiť.
    - **Nepovinná relácia.** Pri `GET /games/{id}`, `GET /games/{id}/pgn`, `GET /players/{username}` a `GET /players/{username}/games` je relácia nepovinná. Bez hlavičky vracajú verejný pohľad; ak sa hlavička pošle, jej token musí byť platný.
    - **Zaneprázdnené úložisko.** Keď databáza počas overovania tokenu zostane zamknutá, endpoint, ktorý reláciu vyžaduje alebo prijíma, odpovie 503 `server_busy` s `retryAfter: 1` (a `Retry-After`); keď overenie trvá viac ako 5 sekúnd, odpovie 503 `server_busy` s `retryAfter` 2 až 5. V oboch prípadoch o tokene nebolo rozhodnuté: ponechajte si ho a pošlite požiadavku znova.
    - **Platnosť.** Relácia sa skončí pri tom, čo nastane skôr: `SESSION_MAX_DAYS` (90) dní po prihlásení (to je `expiresAt`) alebo `SESSION_IDLE_DAYS` (30) dní bez použitia. Každé použitie posunie limit nečinnosti; server zapisuje novú hodnotu najviac raz za 5 minút, na pozadí. Kým v jeho databáze čaká veľa zápisov, odkladá tento zápis na neskoršiu požiadavku, okrem poslednej hodiny pred limitom nečinnosti, takže používaná relácia nevyprší. Účet si ponecháva najviac `MAX_SESSIONS_PER_USER` (10) relácií: nové prihlásenie odvolá najstaršie relácie nad tento počet.
    - **Odvolanie.** Odhlásenie (`POST /auth/logout`, `POST /auth/logout-all`, `DELETE /auth/sessions/{id}`) odvoláva relácie; rovnako aj zmena hesla (ostatné relácie), resetovanie hesla a odstránenie účtu (všetky relácie) a administrátor. Odvolanie cez API platí okamžite a WebSocket otvorený s odvolanou reláciou sa zatvorí. Zmena alebo resetovanie hesla a odstránenie účtu odvolávajú relácie v tej istej transakcii ako samotná zmena: keď niektorá z nich zlyhá, žiadna relácia nebola odvolaná a nič iné sa tiež nezmenilo. Server uchováva výsledky vyhľadania relácií vo vyrovnávacej pamäti 30 sekúnd, takže odvolanie administrátorským príkazom, ktorý beží ako samostatný proces, sa prejaví do 30 sekúnd.
    - **Rozsah.** Token patrí jednému serveru a otvára aj jeho WebSocket. Nikdy ho neposielajte inému serveru.

    ## Limity počtu požiadaviek a ďalšie obmedzenia

    Limity sa vzťahujú na jeden z dvoch rozsahov. **Klient:** adresa IPv4 alebo IPv6 /64 (v režime proxy pochádza adresa z `X-Forwarded-For`, ktorú poslala adresa z `TRUSTED_PROXIES`); niektoré limity počítajú aj každú IPv6 /48 ako celok, navyše ku každej z jej sietí /64. **Hráč:** prihlásený účet bez ohľadu na adresu; limit počítaný na hráča na endpointe, kde je relácia nepovinná, sa pri požiadavke bez tokenu počíta na klienta.

    Každý limit je vedro žetónov (token bucket), ktoré pojme `limit` požiadaviek a priebežne sa dopĺňa rýchlosťou `limit / window`; `retryAfter` je čas do ďalšieho žetónu. Limity označené ako *zdieľané* sa navyše počítajú v posuvnom okne rovnakej dĺžky, aby žiadne okno neobsahovalo o veľa viac ako `limit` požiadaviek. Každý limit sa počíta pre celý server. Keď jeden z limitov endpointu požiadavku odmietne, žetóny, ktoré pre túto požiadavku vzali jeho ostatné limity, sa vrátia. Na odmietnutie server odpovie 429 `rate_limited` s `retryAfter` a `Retry-After`.

    - **Vrstva podľa adresy.** Každá požiadavka (akákoľvek cesta a metóda vrátane endpointov kontroly stavu a prechodov na WebSocket) najprv vezme jeden žetón z kvóty svojho klienta: `HTTP_RATE_PER_IP` (600) za minútu s nárazovou kapacitou (burst) na pol minúty a `HTTP_RATE_PER_PREFIX` (4 x `HTTP_RATE_PER_IP`) pre IPv6 /48. Klient môže mať navyše najviac `IP_MAX_INFLIGHT` (32 x `WORKERS`) rozpracovaných požiadaviek (nad tento počet `retryAfter` 1). Klient, ktorý pokračuje aj po odmietnutiach, sa zablokuje: `ABUSE_BLOCK_REFUSALS_PER_MIN` (600) odmietnutí za jednu minútu ho zablokuje na 1 minútu, potom na 4, 16 a 60 minút pri každom ďalšom zablokovaní v priebehu 6 hodín. Odmietnutie limitmi `auth`, `auth_*` a `reauth` sa počíta za 5; odmietnutia limitmi počítanými na hráča sa nepočítajú nikdy. Adresy v `ABUSE_EXEMPT` túto vrstvu obchádzajú, ale nie kvótu účtu ani limity endpointov.
    - **Kvóta účtu.** Každá požiadavka, ktorá nesie platný token relácie, sa započítava aj do kvóty svojho účtu: `USER_RATE_PER_MIN` (120) za minútu pre všetky endpointy spolu, bez ohľadu na adresu, s nárazovou kapacitou (burst) na pol minúty.
    - **Limity endpointov:**

    | Limit | Predvolená hodnota | Počíta sa | Endpointy |
    |---|---|---|---|
    | `auth` | `AUTH_RATE_PER_IP` (20) / 10 min, zdieľaný | na klienta a `AUTH_RATE_PER_PREFIX` (5 x `AUTH_RATE_PER_IP`) na IPv6 /48 | `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) / h, zdieľaný | na klienta, trojnásobok na IPv6 /48 | `POST /auth/register` |
    | `auth_mail` | `AUTH_MAIL_PER_HOUR` (10) / h, zdieľaný | na klienta, trojnásobok na IPv6 /48 | `POST /auth/verify-email/resend` |
    | `auth_forgot` | `AUTH_FORGOT_PER_HOUR` (3) / h, zdieľaný | na klienta, trojnásobok na IPv6 /48 | `POST /auth/password/forgot` |
    | `auth_forgot_day` | `AUTH_FORGOT_PER_DAY` (10) / 24 h, zdieľaný | na klienta, trojnásobok na IPv6 /48 | `POST /auth/password/forgot` |
    | `auth_reset` | `AUTH_RESET_PER_HOUR` (10) / h, zdieľaný | na klienta, trojnásobok na IPv6 /48 | `POST /auth/password/reset`, `POST /reset-password` |
    | `reauth` | hodnoty limitu `auth`, vlastné vedro, zdieľaný | na klienta a IPv6 /48 | `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, zdieľaný | na hráča | tie isté endpointy ako `reauth` |
    | `account` | 60 / min | na hráča | `GET /account/me`, `PUT /account/preferences` |
    | `account_games` | 60 / min | na hráča | `GET /account/games` |
    | `account_export` | 5 / h, zdieľaný | na hráča | `POST /account/export` (kontroluje sa pred `reauth`; počíta sa každý pokus) |
    | `sessions` | 60 / min | na hráča | `POST /auth/logout`, `/auth/logout-all`, `GET /auth/sessions`, `DELETE /auth/sessions/{id}` |
    | `public_read` | 60 / min | na hráča (na klienta bez tokenu) | `GET /players/{username}`, `/players/{username}/games`, `/games/{id}`, `/games/{id}/pgn` (jedno vedro pre všetky štyri) |
    | `gif` | 30 / min | na hráča | `GET /games/{id}/gif`, `POST /gif` (jedno vedro pre oba) |
    | `gif_user_min`, `gif_user_hour` | `GIF_USER_RENDERS_PER_MIN` (4) / min a `GIF_USER_RENDERS_PER_HOUR` (30) / h, zdieľané | na hráča | tie isté dva, iba keď treba GIF vytvoriť (nie z vyrovnávacej pamäte) |
    | `gif_ip_min`, `gif_ip_hour` | `GIF_IP_RENDERS_PER_MIN` (12) / min a `GIF_IP_RENDERS_PER_HOUR` (120) / h, zdieľané | na klienta (všetky jeho účty spolu), trojnásobok na IPv6 /48 | tie isté, ako vyššie |
    | `reports` | 30 / h | na hráča | `POST /reports` |
    | `sso_start` | 30 / 10 min, zdieľaný | na klienta a 90 na IPv6 /48 | `POST /auth/sso/google/start`, `POST /auth/sso/google/web/start` (spolu) |
    | `sso_finish` | 30 / min | na klienta | `POST /auth/sso/google/finish` |
    | `page` | 60 / min | na klienta | `GET /verify-email`, `/reset-password`, `/confirm-email-change` |

    `GET /info` a `GET /leaderboard` nemajú vlastný limit: iba vrstvu podľa adresy. Endpoint s viacerými limitmi ich kontroluje v poradí uvedenom v jeho popise.

    Server spracúva požiadavku v tomto poradí: vrstva podľa adresy, potom endpointy kontroly stavu; priradenie endpointu; kontroly, či server výrazne nezaostáva (503 `server_busy`): príliš veľa skorších požiadaviek, ktoré stále bežia po uplynutí časového limitu, a pri `POST`, `PUT`, `PATCH` alebo `DELETE` nevybavené zápisy databázy; autentifikácia, do 5 sekúnd (inak 503 `server_busy`); kvóta účtu, keď požiadavka nesie reláciu; limity endpointu; telo; samotný endpoint (endpointy GIF uplatňujú svoje limity vykresľovania, iba keď majú GIF vytvoriť). Požiadavka odmietnutá pre svoj token teda nespotrebuje žiadne žetóny endpointu, zatiaľ čo požiadavka s neplatným telom ich spotrebuje.

    Ďalšie obmedzenia, ktoré uplatňujú samotné endpointy:

    - **Neúspešné prihlásenia na jedno prihlasovacie meno** (používateľské meno alebo e-mail): od `AUTH_FAILURES_PER_ACCOUNT` (5) zlyhaní musí každý pokus čakať dvakrát dlhšie ako predchádzajúci (2 s, 4 s atď., najviac 15 minút): 429 `too_many_attempts` s `retryAfter`. Počítadlo sa vynuluje po hodine bez zlyhaní.
    - **Neúspešné overenia druhého faktora pri prihlásení:** rovnaké pravidlo od 5. nesprávneho kódu účtu. Jeden krok prihlásenia prijme najviac 5 nesprávnych kódov.
    - **Kódy druhého faktora jedného účtu:** najviac `AUTH_MFA_PER_ACCOUNT` (10) kódov (kódov z overovacej aplikácie alebo kódov na obnovenie, správnych či nesprávnych) za 15 minút, z akejkoľvek adresy, pri prihlásení aj pri opätovných autentifikáciách; nad tento počet 429 `too_many_attempts` ešte pred overením kódu, takže kód na obnovenie sa nespotrebuje.
    - **Neúspešné opätovné autentifikácie účtu** (nesprávne heslo alebo kód): rovnaké pravidlo (429 `too_many_attempts`), spoločné pre všetky endpointy, ktoré vyžadujú opätovnú autentifikáciu.
    - **E-maily:** jeden potvrdzovací e-mail alebo e-mail na resetovanie hesla na adresu každých 5 minút (odpoveď zostáva rovnaká); jedno upozornenie „niekto sa pokúsil použiť vašu adresu“ na adresu za hodinu.
    - **Nahlásenia:** `REPORTS_PER_DAY` (5) na hráča za 24 hodín: 429 `report_limit`.

    ## Dôkaz práce

    `POST /auth/register` vždy vyžaduje dôkaz práce, keď je `POW_REGISTER_BITS` väčšie ako 0 (predvolene 18; `GET /info` ho uvádza ako `pow.register`). `POST /auth/login` a `POST /auth/sso/google/link` (jeden druh výzvy pre oba) ho vyžadujú iba počas 5 minút po tom, čo server zaznamená vlnu neúspešných prihlásení (`POW_LOGIN_TRIGGER_PER_MIN`, potom `POW_LOGIN_BITS`); klient sa to dozvie z odpovede.

    1. Na požiadavku bez dôkazu (alebo s odmietnutým dôkazom) server odpovie 428 `pow_required` s `reason` a výzvou `pow` `{ challenge, bits, expiresAt }`.
    2. Nájdite nonce: desiatkový reťazec s najviac 20 číslicami, pre ktorý `SHA-256(challenge + ":" + nonce)` začína `bits` nulovými bitmi (najvýznamnejší bit prvého bajtu ako prvý). 18 bitov si vyžaduje v priemere približne 260 000 hashov.
    3. Pošlite tú istú požiadavku znova s `"pow": { "challenge": "...", "nonce": "123456" }` v tele.

    Výzva platí 2 minúty a iba raz, pre jeden endpoint a jednu sieť klienta (adresu IPv4 alebo IPv6 /64). Je podpísaná, takže server si o nej nič neuchováva, kým sa nevráti. `reason` uvádza, prečo bol dôkaz odmietnutý: `required`, `malformed`, `signature`, `endpoint`, `network`, `expired`, `bits`, `work` alebo `replayed`.

    ## Opätovná autentifikácia

    Zmeny účtu znova vyžadujú heslo a, keď je zapnuté dvojfaktorové overenie, aj druhý faktor:

    - iba heslo: `POST /account/password` a `POST /account/mfa/totp/setup`;
    - heslo a kód z overovacej aplikácie, pričom kód na obnovenie sa odmieta: `POST /account/mfa/recovery-codes`;
    - heslo a kód z overovacej aplikácie alebo kód na obnovenie: `POST /account/mfa/totp/disable`, `POST /account/email`, `POST /account/export` a `POST /account/delete`.

    V telách obsahuje `code` 6-ciferný kód z overovacej aplikácie a `recoveryCode` kód na obnovenie (`xxxx-xxxx-xx`; na veľkosti písmen, medzerách a pomlčkách nezáleží). Kód na obnovenie možno poslať aj v `code` tam, kde sa kódy na obnovenie prijímajú. Každý kód funguje raz: použitý kód z overovacej aplikácie sa odmieta až do nasledujúceho 30-sekundového kroku a kód na obnovenie po použití zaniká.

    | Stav | `error` | Kedy |
    |---|---|---|
    | 403 | `invalid_password` | Nesprávne heslo alebo bolo heslo počas vybavovania požiadavky resetované či zmenené (nič sa nezmenilo). |
    | 403 | `mfa_code_required` | Dvojfaktorové overenie je zapnuté a nebol poslaný ani `code`, ani `recoveryCode`. |
    | 403 | `invalid_code` | Nesprávny alebo už použitý kód. |
    | 400 | `password_not_set` | Účet len s prihlásením cez Google zatiaľ nemá heslo (nastaví ho „Zabudnuté heslo?“). |
    | 429 | `too_many_attempts` | Príliš veľa zlyhaní alebo príliš veľa vyskúšaných kódov na tomto účte. |
    | 503 / 429 | `server_busy` / `rate_limited` | Front na hashovanie hesiel je preťažený. |

    Nesprávne heslá a kódy sa započítavajú do počítadla zlyhaní účtu a zaznamenávajú sa ako bezpečnostné udalosti.

    ## Port metrík

    Okrem portu API server odpovedá cez čisté HTTP aj na porte metrík (`METRICS_PORT`, 9464, viazaný na `METRICS_BIND`, predvolene 127.0.0.1; ponechajte ho neverejný). Nie je súčasťou tohto API: `GET /healthz` odpovedá `ok`; `GET /readyz` odpovedá `ready` od dokončenia spustenia (každý shard znova prehral svoj žurnál a poslucháči sú naviazaní) až do začiatku vypínania, inak 503 `not ready`; `GET /metrics` poskytuje metriky Prometheus a pri nastavenom `METRICS_TOKEN` vyžaduje `Authorization: Bearer <token>` s presne týmto tokenom.
  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: Zdrojový kód servera Scacelith a jeho referenčná dokumentácia (API.md, PROTOCOL.md, CONFIG.md).
  url: https://github.com/DarkCenobyte/scacelith-chess-server
servers:
  - url: https://caissa.scacelith.com/api/v1
    description: Oficiálny server.
  - url: https://{host}:{port}/api/v1
    description: Ktorýkoľvek server Scacelith, napríklad komunitný server.
    variables:
      host:
        default: caissa.scacelith.com
        description: Verejný názov hostiteľa servera (`SERVER_PUBLIC_HOST`).
      port:
        default: "443"
        description: Verejný port API (`PUBLIC_API_PORT`, inak `API_PORT`).
tags:
  - name: server-info
    x-displayName: Informácie o serveri
    description: Čo klient potrebuje vedieť predtým, ako sa prihlási alebo pripojí.
  - name: health
    x-displayName: Kontrola stavu
    description: |-
      Dostupnosť a pripravenosť servera na účely monitorovania. Tieto endpointy odpovedajú na porte API ešte pred autentifikáciou a pred všetkými limitmi endpointov, ale ako každá požiadavka vezmú jeden žetón vrstvy podľa adresy; monitorovací hostiteľ môže byť uvedený v `ABUSE_EXEMPT`. Pre každý endpoint fungujú obe cesty: v koreni servera aj pod `/api/v1`.

      Port metrík má vlastné endpointy kontroly stavu, opísané v úvode.
  - name: auth
    x-displayName: Registrácia a prihlásenie
    description: |-
      Vytvorenie účtu, prihlásenie heslom (a druhým faktorom), potvrdzovacie e-maily a e-maily na resetovanie hesla.

      `POST /auth/login`, `POST /auth/login/mfa`, `POST /auth/sso/google/finish`, `POST /auth/sso/google/link` a `POST /auth/sso/complete` odpovedajú reláciou `{ token, expiresAt, user }` (alebo, pri prvých z nich, druhým krokom).
  - name: google-sign-in
    x-displayName: Prihlásenie cez Google
    description: |-
      K dispozícii, keď `GET /info` uvádza `sso.google: true`; inak každý endpoint nižšie odpovie 404 `sso_disabled`. Web sa prihlasuje rovnakým spôsobom s vlastným klientom, keď má `sso.googleWeb` hodnotu true (nižšie, **Na webe**). Hra sa prihlasuje cez systémový prehliadač tokom pre nainštalované aplikácie podľa RFC 8252 (klient typu „Desktop app“): Google pošle prehliadač späť na poslucháča hry na `127.0.0.1`, nikdy nie na tento server, a hra nikdy neuvidí prihlasovacie údaje Google.

      1. Klient načúva na `127.0.0.1:0` (port vyberie systém) a vytvorí pár PKCE: `codeVerifier` so 43 až 128 znakmi `[A-Za-z0-9._~-]` a `codeChallenge = BASE64URL(SHA-256(codeVerifier))`, ktorý má 43 znakov a žiadne zarovnanie (padding).
      2. `POST /auth/sso/google/start` s výzvou a portom vráti URL Google a `state` pokusu. Klient URL skontroluje (pozri nižšie) a otvorí ju v prehliadači.
      3. Google pošle prehliadač na `http://127.0.0.1:<port>/oauth2/google/<tag>?code=...&state=...`. Klient prijme iba `state` z odpovede na spustenie pokusu a po presmerovaní s `error=` nič neposiela.
      4. `POST /auth/sso/google/finish` s ID pokusu, `codeVerifier`, `state` a `code` (a s `iss`, ak ho Google poslal).
      5. Odpoveďou je relácia, krok dvojfaktorového overenia (pokračujte cez `POST /auth/login/mfa`), `needsUsername` pre nového hráča (pokračujte cez `POST /auth/sso/complete`) alebo `needsPassword`, keď adresu používa účet s heslom (pokračujte cez `POST /auth/sso/google/link`).

      **Značka pôvodu.** URI presmerovania nesie značku servera, ktorý hráč pridal, aby hra odmietla URL, ktorú iný server získal pre svojich hráčov. Pôvod je hostiteľ malými písmenami (literál IPv6 v hranatých zátvorkách), `:` a desiatkový port API, vždy uvedený, vrátane 443: server berie `SERVER_PUBLIC_HOST` a svoj verejný port API, hra adresu, ku ktorej je pripojená. Značka je prvých 22 znakov base64url (bez zarovnania) zo SHA-256(UTF-8 `"scacelith-sso-origin-v1\n"` + pôvod). URI presmerovania je `"http://127.0.0.1:" + port + "/oauth2/google/" + tag`: vždy literál IPv4 a port poslucháča hry (1024 až 65535). Server nikdy neprijíma od klienta URI, hostiteľa ani cestu.

      | Pôvod | Značka |
      |---|---|
      | `play.scacelith.example:443` | `IhcScoV7eDOzTEcSnqPUPt` |
      | `localhost:8443` | `TFGx7zQ_8QlGZW5zpqznCr` |
      | `[::1]:8443` | `XToJm0DG5PjciEVmZa9Cho` |
      | `127.0.0.1:50443` | `3r653wM5ZjYsHcAJljmCwY` |

      Prihlásenie cez Google preto funguje iba pre hráčov, ktorí pridali server presne pod `SERVER_PUBLIC_HOST` a s jeho verejným portom API.

      **Čo hra skontroluje pred otvorením prehliadača.** `authUrl` začína presne `https://accounts.google.com/o/oauth2/v2/auth?`, obsahuje iba tlačiteľné znaky ASCII a má menej ako 4096 znakov a jej dopyt obsahuje presne jeden `response_type=code`, presne jeden `redirect_uri` rovný URI, ktoré hra vypočíta zo svojho portu a svojej značky pôvodu, presne jeden `state` rovný `state` z odpovede, `code_challenge_method=S256` a 43-znakový `code_challenge`. Inak hra zastaví svojho poslucháča a nič neotvorí. `finish` a `link` posiela iba serveru, ktorý odpovedal na `start`.

      **Na webe.** Web (oficiálny: <https://scacelith.com/>) sa prihlasuje klientom typu „Web application“ toho istého projektu Google (`GOOGLE_WEB_CLIENT_ID`). Google pošle prehliadač späť na jednu stránku webu, ktorú nakonfiguroval prevádzkovateľ (`GOOGLE_WEB_REDIRECT_URI`, uvedená v zozname „Authorized redirect URIs“ klienta), a táto stránka pošle kód na `finish`; ani tu server neprijíma od klienta žiadne URI presmerovania.

      1. Stránka vytvorí pár PKCE ako vyššie, uchová overovací reťazec (verifier) v karte (`sessionStorage`) a pošle `POST /auth/sso/google/web/start` s výzvou.
      2. Skontroluje `authUrl` rovnako ako hra, okrem toho, že `redirect_uri` musí byť stránka z jej vlastného pôvodu, a potom na túto URL prejde. Nenačítava sa žiadny skript Google.
      3. Google pošle prehliadač na `GOOGLE_WEB_REDIRECT_URI?code=...&state=...`. Stránka prijme iba `state` z odpovede na spustenie pokusu a po presmerovaní s `error=` nič neposiela.
      4. `finish`, `link` a `complete` nasledujú ako pri hre. Pokus si pamätá, že ho začal web: kód sa vymení s klientom a stránkou webu a ID token musí uvádzať tohto klienta (kód alebo ID token klienta hry zlyhá s 502 `sso_failed`, a naopak).

      Účet Google vedie k tomu istému účtu Scacelith z hry aj z webu: prepojenie uchováva subjekt Google (`sub`), ktorý identifikuje účet Google bez ohľadu na klienta. Pôvod webu musí byť v `CORS_ORIGINS` (inak `check-config` zobrazí varovanie).

      **Ku ktorému účtu vedie prihlásenie cez Google:** k účtu, ktorý je s týmto účtom Google už prepojený; inak k aktívnemu účtu s adresou, ktorú Google potvrdil (ak má heslo, `finish` odpovie `needsPassword` a prepojenie sa uloží až vtedy, keď prejde overenie jeho hesla a potom jeho druhého faktora, ak je zapnutý; ak heslo nemá, 409 `sso_account_exists`); inak k novému účtu (`needsUsername`). Účet Google sa nikdy neprepojí s existujúcim účtom iba na základe adresy. Na adresu účtu príde e-mail, keď prihlásenie cez Google vytvorí účet a keď sa pridá k existujúcemu účtu.
  - name: sessions
    x-displayName: Relácie
    description: Prihlásené zariadenia účtu a odhlásenie.
  - name: account
    x-displayName: Účet
    description: Prehľad účtu, jeho predvoľby a heslo.
  - name: two-step-verification
    x-displayName: Dvojfaktorové overenie
    description: Overovacie aplikácie (TOTP, RFC 6238) so SHA-1, 6 číslicami, 30-sekundovými krokmi a toleranciou jedného kroku oboma smermi, plus jednorazové kódy na obnovenie.
  - name: email-change
    x-displayName: Zmena e-mailovej adresy
    description: Zmena adresy účtu, potvrdená odkazom poslaným na novú adresu.
  - name: data-export
    x-displayName: Export údajov
    description: Všetko, čo server o účte uchováva, v jednom súbore JSON.
  - name: account-deletion
    x-displayName: Odstránenie účtu
    description: Trvalé odstránenie účtu.
  - name: game-history
    x-displayName: História partií
    description: Vlastné partie prihláseného hráča, filtrované a stránkované.
  - name: games
    x-displayName: Partie a PGN
    description: |-
      Záznamy partií s ich ťahmi a hodinami a ich súbory PGN.

      **Kódy partií.** `status`: 1 `WhiteWins`, 2 `BlackWins`, 3 `Draw`, 4 `Aborted` (ukladajú sa iba skončené partie). `result`: `1-0`, `0-1`, `1/2-1/2` alebo `*` (zrušená). `reason` s názvom (`termination`) a slovami, ktorými končí text ťahov v súbore PGN (v súbore po anglicky, nižšie s prekladom); kódy 7, 14 a 21 sú remízy (hráč, ktorému vypršal čas, ktorý sa vzdal alebo ktorý partiu opustil, mal súpera, ktorý nemohol dať mat: FIDE 6.9 a 5.1.2; pravidlo uvádza `DESIGN.md` 6.3) a server nikdy neukončí partiu kódmi 4 a 13 (patria do spoločného zoznamu dôvodov):

      | Kód | `termination` | Slová v PGN |
      |---|---|---|
      | 1 | `Checkmate` | Checkmate (mat) |
      | 2 | `Resignation` | Resignation (vzdanie sa) |
      | 3 | `Timeout` | Loss on time (prehra na čas) |
      | 4 | `IllegalMoves` | Second illegal move (forfeit) (druhý nemožný ťah, prehra) |
      | 5 | `Stalemate` | Stalemate (pat) |
      | 6 | `InsufficientMaterial` | Dead position (insufficient material) (mŕtva pozícia, nedostatočný materiál) |
      | 7 | `TimeoutVsInsufficient` | Flag fall, but the opponent cannot checkmate (čas vypršal, ale súper nemôže dať mat) |
      | 8 | `FivefoldRepetition` | Fivefold repetition (päťnásobné opakovanie pozície) |
      | 9 | `SeventyFiveMoves` | 75-move rule (pravidlo 75 ťahov) |
      | 10 | `ThreefoldClaim` | Threefold repetition (claimed) (trojnásobné opakovanie pozície, na žiadosť) |
      | 11 | `FiftyMoveClaim` | 50-move rule (claimed) (pravidlo 50 ťahov, na žiadosť) |
      | 12 | `Agreement` | Draw by agreement (remíza dohodou) |
      | 13 | `IllegalMovesVsInsufficient` | Second illegal move, but the opponent cannot checkmate (druhý nemožný ťah, ale súper nemôže dať mat) |
      | 14 | `ResignationVsInsufficient` | Resignation, but the opponent cannot checkmate (vzdanie sa, ale súper nemôže dať mat) |
      | 20 | `Abandonment` | Abandoned (disconnected for too long) (opustenie partie, príliš dlhé odpojenie) |
      | 21 | `AbandonmentVsInsufficient` | Abandoned, but the opponent cannot checkmate (opustenie partie, ale súper nemôže dať mat) |
      | 22 | `Aborted` | Game aborted (partia zrušená) |
      | 23 | `NoShow` | Aborted: first move not played in time (zrušená: prvý ťah nebol zahraný včas) |
      | 24 | `Forfeit` | Forfeit (fair play violation) (kontumácia za porušenie pravidiel férovej hry) |
      | 25 | `ServerAborted` | Aborted by the server (zrušené serverom) |
      | 26 | `BothDisconnected` | Aborted: both players disconnected (zrušená: obaja hráči sa odpojili) |

      Kód 14 pribudol v protokole s vedľajšou verziou (minor) 1: klient napísaný pred touto verziou by mal neznámy kód zobraziť ako bežný koniec partie s jej `result`.
  - name: gifs
    x-displayName: Animované GIF-y
    description: |-
      Partia ako animovaný GIF na uchovanie alebo zdieľanie: šachovnica pri pohľade zhora, jedna snímka na každú pozíciu od začiatočnej po záverečnú, mená a ratingy hráčov nad šachovnicou, posledný ťah pod ňou a na poslednej snímke výsledok a spôsob, akým partia skončila.

      **Náklady, vyrovnávacia pamäť a kvóty.** GIF sa vytvára vo vlastnom vykresľovacom vlákne, nikdy nie vo vláknach, v ktorých bežia partie, a s najnižšou prioritou CPU: `GIF_THREADS` vlákien (predvolene `WORKERS`), ktoré sa spustia s prvým GIF-om a zastavia sa po minúte bez GIF-u. Na vlákno čaká až `GIF_QUEUE_MAX` (4 x `WORKERS`) GIF-ov, každý najviac `GIF_QUEUE_TIMEOUT_MS` (10 s); vykreslenie môže trvať `GIF_RENDER_TIMEOUT_MS` (30 s). Server uchováva vytvorené GIF-y vo vyrovnávacej pamäti s veľkosťou `GIF_CACHE_MB` MB (32 x `WORKERS`), pričom ako prvé z nej odchádzajú najdlhšie nepoužité; GIF z vyrovnávacej pamäte alebo GIF, ktorý sa práve vytvára pre inú požiadavku, nestojí žiadne vykreslenie. Kľúč vyrovnávacej pamäte zahŕňa všetko, čo mení obrázok, vrátane mien.

      Každá požiadavka sa započítava do `gif` (30 za minútu na hráča pre oba endpointy). GIF, ktorý treba vytvoriť, sa započítava aj do limitov vykresľovania: na hráča `GIF_USER_RENDERS_PER_MIN` (4) za minútu a `GIF_USER_RENDERS_PER_HOUR` (30) za hodinu; na klienta, so všetkými jeho účtami spolu, `GIF_IP_RENDERS_PER_MIN` (12) za minútu a `GIF_IP_RENDERS_PER_HOUR` (120) za hodinu a trojnásobok na IPv6 /48. Klient by si mal stiahnutý súbor ponechať a nežiadať ho znova a po 429 alebo 503 počkať `retryAfter`.

      Veľkosti: `small` (polia 32 px: 284 x 350 pixelov), `medium` (48 px: 424 x 515), `large` (72 px: 628 x 762); bez súradníc 268 x 342, 400 x 503 a 600 x 748. Začiatočná pozícia zostane zobrazená najmenej 1 s (alebo počas oneskorenia, ak je dlhšie), záverečná pozícia 3 s a GIF sa prehráva dookola; po prvej snímke sa ukladá iba časť obrázka, ktorá sa mení. Partia so 40 ťahmi zaberá približne 135, 205 a 325 KiB (small, medium, large), partia so 150 ťahmi 0,5, 0,8 a 1,2 MiB.
  - name: players
    x-displayName: Hráči
    description: Verejné profily a nedávne partie. Iba verejné údaje, nikdy e-mailová adresa, relácia, sankcia ani úroveň integrity. Odstránený účet nemá profil.
  - name: leaderboard
    x-displayName: Rebríček
    description: Najlepší hráči každej oficiálnej kategórie.
  - name: reports
    x-displayName: Nahlásenia
    description: Nahlásenie súpera z nedávnej partie moderátorom.
  - name: pages
    x-displayName: Stránky HTML
    description: |-
      Stránky pre prehliadač, otvárané z odkazov v e-mailoch. Ich odkazy smerujú na `https://<SERVER_PUBLIC_HOST>` (s `:<PUBLIC_API_PORT>`, ak to nie je 443), mimo `/api/v1`.

      - Stránky nespúšťajú žiadny JavaScript a nenačítavajú žiadne externé zdroje. Posielajú sa s `Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'`.
      - `GET` iba zobrazí tlačidlo alebo formulár, aby odkaz nespotreboval skener e-mailov, ktorý ho otvorí. Zmena nastane pri `POST`: formulár odoslaný ako `application/x-www-form-urlencoded` (prijíma sa aj telo JSON) s tokenom odkazu v skrytom poli. Pole zadané dvakrát sa odmietne.
      - Chyby (limity počtu požiadaviek, neplatné polia) sú tiež stránky HTML s nadpisom „Request refused“ (požiadavka odmietnutá) alebo, od 500 vyššie, „Server error“ (chyba servera). V JSON odpovedajú iba zlyhania zistené pred priradením stránky (príliš dlhý alebo chybne utvorený cieľ požiadavky, vrstva podľa adresy).
x-tagGroups:
  - name: server
    x-displayName: Server
    tags:
      - server-info
      - health
  - name: accounts
    x-displayName: Účty
    tags:
      - auth
      - google-sign-in
      - sessions
      - account
      - two-step-verification
      - email-change
      - data-export
      - account-deletion
  - name: games
    x-displayName: Partie
    tags:
      - game-history
      - games
      - gifs
  - name: community
    x-displayName: Hráči a nahlásenia
    tags:
      - players
      - leaderboard
      - reports
  - name: browser-pages
    x-displayName: Stránky pre prehliadač
    tags:
      - pages
paths:
  /info:
    get:
      operationId: getServerInfo
      tags:
        - server-info
      summary: Získať názov servera, verzie, porty a pravidlá registrácie
      description: |-
        Čo klient potrebuje predtým, ako sa prihlási alebo pripojí: názov a ID servera, verzie protokolu WebSocket a umiestnenie WebSocketu, pravidlá registrácie, oficiálne tempá hry a limity, ktoré môže klient skontrolovať pred odoslaním formulára.

        **Limity:** iba vrstva podľa adresy.
      security: []
      responses:
        '200':
          description: Popis servera.
          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`: vrstva podľa adresy.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /auth/register:
    post:
      operationId: registerAccount
      tags:
        - auth
      summary: Vytvoriť účet
      description: |-
        Vytvorí účet: po potvrdení jeho e-mailovej adresy odkazom, ktorý sa na ňu pošle, alebo okamžite, ak sa e-mail nepotvrdzuje.

        **S potvrdením e-mailu** (`REQUIRE_EMAIL_VERIFICATION`, predvolené): 202 `verification_sent`. Účet ešte neexistuje: registrácia čaká 24 hodín a medzitým si rezervuje svoje používateľské meno. Na adresu príde odkaz platný počas týchto 24 hodín, najviac jeden na adresu každých 5 minút (`POST /auth/verify-email/resend` pošle nový); účet sa vytvorí s potvrdenou adresou, keď sa odkaz použije (tlačidlo na stránke `/verify-email`), a hráč sa potom môže prihlásiť. Dovtedy server na prihlásenie s týmto používateľským menom odpovedá `invalid_credentials`, ako pri neznámom účte, a verejný profil neexistuje. Nová registrácia s rovnakou adresou nahradí čakajúcu. Odpoveď je rovnaká, keď adresu už používa iný účet: odkaz sa vtedy nepošle, jeho vlastník namiesto toho dostane upozornenie (najviac jedno za hodinu) a používateľské meno sa rezervuje rovnako, aby nič neprezradilo, či adresa má účet. Registrácia, ktorej odkaz nebol použitý, sa po 24 hodinách zahodí a jej používateľské meno je opäť voľné.

        **Bez potvrdenia e-mailu** (`REQUIRE_EMAIL_VERIFICATION=false`): 201 `ready`, účet sa vytvorí okamžite a môže sa prihlásiť.

        **Pravidlá.** `username`: `USERNAME_MIN` až `USERNAME_MAX` znakov (3 až 20), písmená, číslice, `_` a `-`, na začiatku písmeno alebo číslica (`GET /info` ich uvádza v `limits`); rezervované mená (`admin`, `moderator`, `deleted`...) a niektoré predpony sa odmietajú; jedinečné bez ohľadu na veľkosť písmen. `email`: bez medzier na začiatku a na konci, uložený malými písmenami, čisté ASCII s doménou s bodkou. `password`: aspoň `PASSWORD_MIN_LENGTH` (10) znakov a najviac 256 bajtov v UTF-8; nesmie obsahovať používateľské meno ani lokálnu časť e-mailu a nesmie byť bežne používaným heslom.

        **Chyby** v poradí kontroly: 403 `registration_closed`; 400 `invalid_username`; 400 `invalid_email`; 400 `weak_password`; 409 `username_taken`; 428 `pow_required`; chyby frontu na hashovanie hesiel; 409 `email_taken` (iba bez potvrdenia e-mailu).

        **Limity:** `auth`, potom `auth_register` (10 registrácií za hodinu na klienta). **Dôkaz práce:** vždy, keď je `POW_REGISTER_BITS` väčšie ako 0.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
            examples:
              first:
                summary: Prvý pokus, bez dôkazu práce
                value:
                  username: alice
                  email: alice@example.org
                  password: correct horse battery
              withPow:
                summary: Opätovne odoslané s dôkazom práce
                value:
                  username: alice
                  email: alice@example.org
                  password: correct horse battery
                  pow:
                    challenge: eyJ2IjoxLCJlIjoicmVnaXN0ZXIiLCJiIjoxOCwieCI6MTc5MDg4Mjk5MTIwMH0.6Ku_D7kw3S69OYxj-9KxsXszRCwz6ECnOQ-B6-v2erM
                    nonce: '123456'
      responses:
        '201':
          description: Účet je vytvorený a môže sa prihlásiť (tento server nepotvrdzuje e-maily).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    const: ready
              example:
                status: ready
        '202':
          description: Registrácia čaká na svoj potvrdzovací odkaz (alebo adresa už má účet; odpoveď to neprezrádza).
          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` (s `field`), `invalid_json`, `invalid_username`, `invalid_email` alebo `weak_password` s `reason`: `too_short`, `too_long`, `contains_username`, `contains_email` alebo `too_common`.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`registration_closed`: registrácia je na tomto serveri uzavretá (`GET /info` uvádza `registration: closed`).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`username_taken`: používateľské meno má niektorý účet alebo si ho rezervuje čakajúca registrácia s inou adresou. `email_taken`: adresu používa iný účet (iba bez potvrdenia e-mailu).'
        '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`: limit `auth` alebo `auth_register`, alebo front na hashovanie hesiel.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (front na hashovanie hesiel alebo databáza zostala zamknutá), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /auth/login:
    post:
      operationId: logIn
      tags:
        - auth
      summary: Prihlásiť sa heslom
      description: |-
        Prihlási hráča pomocou používateľského mena alebo e-mailovej adresy a hesla. Možné sú dve odpovede, obe so stavom 200: relácia alebo, keď je zapnuté dvojfaktorové overenie, druhý krok, ktorý treba dokončiť do 5 minút cez `POST /auth/login/mfa`.

        Neznámy účet, nesprávne heslo a účet bez hesla dostanú rovnakú odpoveď po rovnako dlhom čase: 401 `invalid_credentials`. Kontroly účtu (`banned`, `email_unverified`) prichádzajú až po správnom hesle. Od `AUTH_FAILURES_PER_ACCOUNT` (5) zlyhaní na jedno prihlasovacie meno musí každý pokus čakať (429 `too_many_attempts`).

        **Limit:** `auth`. **Dôkaz práce:** iba počas vlny neúspešných prihlásení (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: Relácia alebo druhý krok prihlásenia s dvojfaktorovým overením.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginAnswer'
              examples:
                session:
                  summary: Prihlásenie prebehlo
                  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: Dvojfaktorové overenie je zapnuté
                  value:
                    mfaRequired: true
                    mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                    expiresIn: 300
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_credentials`: neznámy účet, nesprávne heslo alebo účet bez hesla.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: 'Iba po správnom hesle: `banned` s `until` (ms od epochy Unixu, `null` pri trvalom pozastavení) alebo `email_unverified` (starší účet, ktorého adresa ešte nie je potvrdená).'
        '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` (oneskorenie po neúspešných pokusoch pre toto prihlasovacie meno) alebo `rate_limited` (limit `auth`, front na hashovanie hesiel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (front na hashovanie hesiel alebo databáza zostala zamknutá), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /auth/login/mfa:
    post:
      operationId: logInWithSecondFactor
      tags:
        - auth
      summary: Dokončiť prihlásenie druhým faktorom
      description: |-
        Druhý krok prihlásenia s dvojfaktorovým overením, keď `POST /auth/login` alebo prihlásenie cez Google (`POST /auth/sso/google/finish` alebo `POST /auth/sso/google/link`) odpovedali `mfaRequired`. Pošlite `code` (6-ciferný kód z overovacej aplikácie alebo kód na obnovenie) alebo `recoveryCode`. Kód na obnovenie použitý tu zaniká.

        Jeden krok prijme najviac 5 nesprávnych kódov; krok sa skončí aj vtedy, keď sa heslo resetuje alebo zmení. Po kroku s heslom pri prepájaní s Google sa prepojenie uloží až vtedy, keď tu kód prejde.

        **Limity:** `auth` a najviac `AUTH_MFA_PER_ACCOUNT` (10) kódov za 15 minút pre účet, z akejkoľvek adresy; nad tento počet sa kód neoveruje, takže sa kód na obnovenie nespotrebuje.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MfaLoginRequest'
            examples:
              authenticator:
                summary: Kód z overovacej aplikácie
                value:
                  mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                  code: '123456'
              recovery:
                summary: Kód na obnovenie
                value:
                  mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                  recoveryCode: j7v5-3ezx-zn
      responses:
        '200':
          description: Prihlásenie prebehlo.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionAnswer'
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_request`: nebol poslaný ani `code`, ani `recoveryCode`, alebo telo porušuje schému; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_mfa_token`: platnosť kroku vypršala, krok bol použitý alebo sa skončil po 5 nesprávnych kódoch, alebo bolo heslo od prvého kroku resetované či zmenené (prihláste sa znova). `invalid_code`: nesprávny alebo už použitý kód.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`banned` (s `until`), `email_unverified`.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_already_linked`: iba po kroku s heslom pri prepájaní s Google, keď bol účet Google medzitým prepojený s iným účtom.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: iba po kroku s heslom pri prepájaní s Google, keď sa účet medzitým zmenil; začnite prihlásenie znova.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts`: oneskorenie po neúspešných pokusoch na účte alebo vyčerpanie jeho kódov `AUTH_MFA_PER_ACCOUNT` za posledných 15 minút. `rate_limited`: limit `auth`.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` s `retryAfter: 1`: databáza zostala zamknutá (po kroku prepájania s Google sa nič neprepojilo: začnite prihlásenie znova). `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /auth/logout:
    post:
      operationId: logOut
      tags:
        - sessions
      summary: Odhlásiť túto reláciu
      description: |-
        Odvolá reláciu použitého tokenu. WebSocket otvorený s touto reláciou sa zatvorí. Bez tela (alebo `{}`).

        **Limit:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Odhlásenie prebehlo.
          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`: iné telo ako `{}`; `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`: limit `sessions` alebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databáza zostala zamknutá), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /auth/logout-all:
    post:
      operationId: logOutEverywhere
      tags:
        - sessions
      summary: Odhlásiť všetky relácie
      description: |-
        Odvolá všetky relácie účtu vrátane tejto. WebSockety otvorené s nimi sa zatvoria. Bez tela (alebo `{}`).

        **Limit:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Všetky relácie sú odhlásené.
          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`: iné telo ako `{}`; `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`: limit `sessions` alebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databáza zostala zamknutá), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /auth/verify-email/resend:
    post:
      operationId: resendVerificationEmail
      tags:
        - auth
      summary: Znova poslať potvrdzovací odkaz
      description: |-
        Znova pošle odkaz na potvrdenie e-mailu. Odpoveď je 202 `accepted` bez ohľadu na adresu, aby nikdy neprezradila, či adresu používa účet alebo registrácia.

        Požiadavka sa prejaví najviac raz za 5 minút na adresu. Čakajúca registrácia s touto adresou dostane znova svojich 24 hodín bez ohľadu na to, či adresu používa iný účet, aby jej používateľské meno zostalo v oboch prípadoch rezervované rovnako dlho. Odkaz sa pošle iba pre túto registráciu, keď adresa nemá účet (nový odkaz s platnosťou 24 hodín nahradí predchádzajúci), alebo pre aktívny nepotvrdený účet s touto adresou.

        **Limity:** `auth`, potom `auth_mail` (10 za hodinu na klienta).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmailRequest'
            example:
              email: alice@example.org
      responses:
        '202':
          description: Prijaté (bez ohľadu na adresu).
          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`: limit `auth` alebo `auth_mail` (bez ohľadu na adresu).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` s `retryAfter: 1`: databáza zostala zamknutá pri vyhľadávaní účtu alebo pri ukladaní odkazu nepotvrdeného účtu (predĺženie čakajúcej registrácie je bez záruky a nikdy nespôsobí zlyhanie požiadavky); 5 minút pre adresu sa napriek tomu začne počítať, takže nová požiadavka počas nich nič neurobí. `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /auth/password/forgot:
    post:
      operationId: requestPasswordReset
      tags:
        - auth
      summary: Poslať odkaz na resetovanie hesla
      description: |-
        Pošle odkaz na resetovanie hesla s platnosťou jednu hodinu, ktorý otvorí stránku `/reset-password`. Odpoveď je 202 `accepted` bez ohľadu na adresu. Odkaz sa pošle iba aktívnemu účtu, najviac raz za 5 minút na adresu. Účet len s prihlásením cez Google si týmto spôsobom nastaví svoje prvé heslo.

        Obnova zabudnutého hesla má najprísnejšie limity v celom API, všetky počítané pre celý server: 3 požiadavky za hodinu (`AUTH_FORGOT_PER_HOUR`) a 10 za 24 hodín (`AUTH_FORGOT_PER_DAY`) na klienta (adresu IPv4 alebo IPv6 /64), trojnásobok týchto počtov na IPv6 /48, navyše k limitu `auth` (20 za 10 minút) a k jednému e-mailu na adresu každých 5 minút. Ani 202, ani 429 neprezradí, či adresu používa nejaký účet. Nastavenie nového hesla má vlastný limit (`auth_reset`).

        **Limity:** `auth`, `auth_forgot`, potom `auth_forgot_day`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmailRequest'
            example:
              email: alice@example.org
      responses:
        '202':
          description: Prijaté (bez ohľadu na adresu).
          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`: limit `auth`, `auth_forgot` alebo `auth_forgot_day` (bez ohľadu na adresu).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` s `retryAfter: 1`: databáza zostala zamknutá pri vyhľadávaní účtu alebo pri ukladaní odkazu; 5 minút pre adresu sa napriek tomu začne počítať, takže nová požiadavka počas nich nepošle žiadny odkaz. `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /auth/password/reset:
    post:
      operationId: resetPassword
      tags:
        - auth
      summary: Nastaviť nové heslo pomocou tokenu z odkazu na resetovanie hesla
      description: |-
        Nastaví nové heslo pomocou tokenu z odkazu na resetovanie hesla (stránka `/reset-password` robí to isté). Nové heslo sa riadi pravidlami registrácie.

        Resetovanie má tieto účinky v jednej transakcii (všetky, alebo žiadny): odvolá všetky relácie a zruší čakajúcu zmenu e-mailu; ostatné odkazy účtu na resetovanie hesla prestanú fungovať; adresa sa považuje za potvrdenú (odkaz to dokázal); vlastník dostane e-mail. Dvojfaktorové overenie zostáva nedotknuté. Po chybe frontu na hashovanie hesiel, po 503 alebo 500 sa nič nezmenilo: staré heslo aj relácie zostávajú a odkaz zostáva platný.

        **Limity:** `auth`, potom `auth_reset` (10 za hodinu na klienta, 30 na IPv6 /48, spoločný so stránkou: každý pokus hashuje heslo).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasswordResetRequest'
            example:
              token: A0ATfmXCDKFC8UkSxLJCD85K7au5nMIrZ2P3K-1VWQg
              newPassword: a much better passphrase
      responses:
        '200':
          description: Heslo je zmenené a všetky relácie sú odhlásené.
          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`: odkaz je neplatný, použitý alebo mu vypršala platnosť, alebo bol poslaný na adresu, ktorú účet už nemá. `weak_password` (s `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`: limit `auth` alebo `auth_reset`, alebo front na hashovanie hesiel.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: front na hashovanie hesiel alebo databáza zostala zamknutá (`retryAfter: 1`, nič sa nezmenilo). `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /auth/sso/google/start:
    post:
      operationId: startGoogleSignIn
      tags:
        - google-sign-in
      summary: Začať prihlásenie cez Google
      description: |-
        Začne pokus o prihlásenie cez Google pre výzvu PKCE a port poslucháča hry na `127.0.0.1`. Odpoveď uvádza URL Google, ktorú treba otvoriť v systémovom prehliadači, a `state` pokusu; pokus platí 10 minút.

        `authUrl` obsahuje `client_id`, `redirect_uri` (zostavené z `redirectPort` a značky pôvodu servera), `response_type=code`, `scope=openid email profile`, `state`, `nonce`, `code_challenge` (S256 vlastného overovacieho reťazca servera pre Google) s `code_challenge_method=S256` a `prompt=select_account`. Hra ju pred otvorením skontroluje (pozri popis sekcie).

        **Limit:** `sso_start`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SsoStartRequest'
            example:
              codeChallenge: 6e7diXEYxG7OTYw7STfNOltEeLxAilPthC_txzaE0xA
              redirectPort: 51234
      responses:
        '200':
          description: Pokus.
          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`: prihlásenie cez Google nie je na tomto serveri k dispozícii.'
        '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`: limit `sso_start`.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databáza zostala zamknutá), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /auth/sso/google/web/start:
    post:
      operationId: startGoogleWebSignIn
      tags:
        - google-sign-in
      summary: Začať prihlásenie cez Google na webe
      description: |-
        Začne pokus webu o prihlásenie cez Google pre výzvu PKCE jeho stránky. Odpoveď je rovnaká ako pre hru: URL Google, na ktorú treba prejsť, a `state` pokusu; pokus platí 10 minút. K dispozícii, keď `GET /info` uvádza `sso.googleWeb: true`.

        `authUrl` obsahuje `client_id` webu a `redirect_uri=GOOGLE_WEB_REDIRECT_URI` s rovnakými ostatnými parametrami ako pre hru. Stránka ju skontroluje predtým, ako na ňu prejde (pozri **Na webe** v popise sekcie).

        **Limit:** `sso_start`, spoločný so spustením pre hru.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SsoWebStartRequest'
            example:
              codeChallenge: 6e7diXEYxG7OTYw7STfNOltEeLxAilPthC_txzaE0xA
      responses:
        '200':
          description: Pokus.
          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`: chýbajúci alebo chybne utvorený `codeChallenge` alebo akékoľvek iné pole (vrátane `redirectPort`).'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: prihlásenie cez Google nie je na tomto serveri k dispozícii alebo nie je k dispozícii pre weby.'
        '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`: limit `sso_start` (spoločný so spustením pre hru).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databáza zostala zamknutá), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /auth/sso/google/finish:
    post:
      operationId: finishGoogleSignIn
      tags:
        - google-sign-in
      summary: Odovzdať serveru odpoveď od Google
      description: |-
        Odovzdá serveru `code` a `state`, ktoré Google poslal poslucháčovi hry alebo stránke webu, spolu s ID pokusu a overovacím reťazcom PKCE (verifier). ID pokusu je bez overovacieho reťazca nepoužiteľné a kód je nepoužiteľný bez vlastného overovacieho reťazca PKCE servera a tajného kľúča klienta.

        Server skontroluje pokus (neznámy, použitý alebo s vypršanou platnosťou: 410), potom overovací reťazec (nesprávny ponechá pokus použiteľný), potom pokus jednorazovo použije, skontroluje `state` a `iss`, vymení kód s Google a overí ID token. Odpoveď (200) je jedna z týchto:

        - `{ token, expiresAt, user }`: prihlásenie do prepojeného účtu prebehlo;
        - `{ mfaRequired, mfaToken, expiresIn }`: pokračujte cez `POST /auth/login/mfa`;
        - `{ needsUsername, ssoTicket, suggestedUsername }`: nový účet; pokračujte cez `POST /auth/sso/complete` do 10 minút. `suggestedUsername` pochádza z mena v Google alebo z adresy a je `""`, keď sa nič nehodí;
        - `{ needsPassword, linkTicket, username, expiresIn }`: účet s touto adresou má heslo; pokračujte cez `POST /auth/sso/google/link`. Zatiaľ nie je nič prepojené.

        **Limit:** `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: Relácia, druhý krok alebo ďalší krok prvého prihlásenia cez Google.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SsoFinishAnswer'
              examples:
                session:
                  summary: Prihlásenie do prepojeného účtu prebehlo
                  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: Dvojfaktorové overenie je zapnuté
                  value:
                    mfaRequired: true
                    mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                    expiresIn: 300
                needsUsername:
                  summary: Nový hráč
                  value:
                    needsUsername: true
                    ssoTicket: sso_gYIKKhwnz1AqWfFxOC3R-ZvCACw06q74fEMtGEOJQWU
                    suggestedUsername: alice
                needsPassword:
                  summary: Adresu používa účet s heslom
                  value:
                    needsPassword: true
                    linkTicket: sso_ZfEI6iZjePTHbnbevupaD9O7GjBCzcL-GJu2XGOdFGc
                    username: alice
                    expiresIn: 600
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_verifier`: overovací reťazec nezodpovedá výzve pokusu (pokus zostáva použiteľný). `sso_email_unverified`: Google adresu nepotvrdil. `registration_closed`. `account_disabled`. Iba pre prepojený účet: `banned` (s `until`), `email_unverified`.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: prihlásenie cez Google nie je na tomto serveri k dispozícii.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_account_exists`: adresu používa aktívny účet bez hesla (odpoveď ho neuvádza).'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: pokus je neznámy, už použitý alebo mu vypršala platnosť.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: limit `sso_finish`.'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/BadGateway'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databáza zostala zamknutá), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /auth/sso/google/link:
    post:
      operationId: linkGoogleAccount
      tags:
        - google-sign-in
      summary: Prepojiť Google s existujúcim účtom pomocou jeho hesla
      description: |-
        Keď `finish` odpovedal `needsPassword`: prepojí Google s existujúcim účtom pomocou hesla tohto účtu zadaného v hre. Odpoveďou (200) je relácia (prepojenie sa uloží a hráč sa prihlási) alebo, keď je zapnuté dvojfaktorové overenie, `{ mfaRequired, mfaToken, expiresIn }`: pokračujte cez `POST /auth/login/mfa` a prepojenie sa uloží až vtedy, keď tam kód prejde.

        Nesprávne heslo ponechá tiket použiteľný, celkovo na 5 pokusov. Pokus sa odpočíta tesne pred overením hesla: ani 429 `too_many_attempts`, ani 428 `pow_required` žiadny pokus neodpočítajú, zatiaľ čo odmietnutie frontom na hashovanie hesiel (503 `server_busy`, 429 `rate_limited`) už jeden odpočítalo a počíta sa ako zlyhanie účtu. Oneskorenie po neúspešných pokusoch používa to isté počítadlo ako `POST /auth/login`.

        **Limit:** `auth` (vrátane počítania na IPv6 /48). **Dôkaz práce:** ako pri `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: Relácia alebo druhý krok prihlásenia.
          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`: nesprávne heslo; tiket zostáva použiteľný, celkovo na 5 pokusov.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`banned` (s `until`), iba po zadaní správneho hesla.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: tento server neponúka prihlásenie cez Google.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_already_linked`: účet Google bol medzitým prepojený s iným účtom.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: neznámy alebo už použitý tiket či tiket s vypršanou platnosťou; 5. nesprávne heslo; účet, ktorého stav alebo adresa sa od `finish` zmenili; alebo účet, ktorého stav, adresa, heslo alebo dvojfaktorové overenie sa zmenili počas ukladania prepojenia. Začnite prihlásenie znova.'
        '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` (oneskorenie po neúspešných pokusoch na účte) alebo `rate_limited` (limit `auth`, front na hashovanie hesiel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: front na hashovanie hesiel alebo databáza zostala zamknutá (`retryAfter: 1`, nič sa neprepojilo). `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /auth/sso/complete:
    post:
      operationId: completeGoogleSignUp
      tags:
        - google-sign-in
      summary: Vytvoriť účet pri prvom prihlásení cez Google
      description: |-
        Keď `finish` vrátil `needsUsername`: vytvorí účet so zvoleným používateľským menom (platia pravidlá registrácie) a prihlási ho. Účet nemá heslo: `hasPassword` je `false` a heslo mu nastaví „Zabudnuté heslo?“ (`POST /auth/password/forgot`).

        **Limit:** `auth` (vrátane počítania na IPv6 /48).
      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: Účet je vytvorený a prihlásený.
          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`: tento server neponúka prihlásenie cez Google.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`username_taken` (používateľské meno má niektorý účet alebo ho drží čakajúca registrácia s inou adresou), `sso_already_linked`, `email_taken`.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: tiket je neznámy, už použitý alebo mu vypršala platnosť.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: limit `auth`.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databáza zostala zamknutá), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /auth/sessions:
    get:
      operationId: listSessions
      tags:
        - sessions
      summary: Vypísať prihlásené zariadenia
      description: |-
        Aktívne relácie účtu (prihlásené zariadenia), zoradené od naposledy použitej. `current` označuje reláciu, ktorá posiela požiadavku. `lastSeenAt` sa aktualizuje najviac raz za 5 minút (neskôr, kým server výrazne zaostáva); `expiresAt` je absolútny koniec a limit nečinnosti môže reláciu ukončiť skôr.

        **Limit:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Aktívne relácie.
          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`: limit `sessions` alebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databáza zostala zamknutá), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /auth/sessions/{id}:
    delete:
      operationId: revokeSession
      tags:
        - sessions
      summary: Odhlásiť jedno zariadenie
      description: |-
        Odhlási jednu reláciu účtu; odhlásiť možno aj aktuálnu. WebSocket otvorený s touto reláciou sa zatvorí. Bez tela (alebo `{}`).

        **Limit:** `sessions`.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/SessionId'
      responses:
        '200':
          description: Relácia je odhlásená.
          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`: iné telo ako `{}` alebo `id`, ktoré nie je platne kódované pre URL; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: tento účet nemá aktívnu reláciu s týmto ID.'
        '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`: limit `sessions` alebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databáza zostala zamknutá), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /account/me:
    get:
      operationId: getAccount
      tags:
        - account
      summary: Získať účet, jeho ratingy a aktívne sankcie
      description: |-
        Účet tak, ako ho vidí jeho hráč: zobrazenie účtu (aj `user` v každej odpovedi na prihlásenie), jeden záznam ratingu pre každú kategóriu, v ktorej hráč hral hodnotené partie, aktívne sankcie a pozastavenie účtu. Úroveň integrity zo systému proti podvádzaniu sa nikdy nezobrazuje.

        **Limit:** `account`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Účet.
          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` alebo `invalid_token` (aj keď bol účet odstránený).'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: limit `account` alebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databáza zostala zamknutá), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /account/preferences:
    put:
      operationId: updatePreferences
      tags:
        - account
      summary: Prijímať alebo odmietať priame výzvy
      description: |-
        Nastaví, či ostatní hráči môžu tohto hráča vyzvať podľa mena. Pri `none` sa priame výzvy odmietajú: vyzývajúci sa dozvie, že hráč nie je k dispozícii. Bez opätovnej autentifikácie.

        **Limit:** `account`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Preferences'
            example:
              acceptChallenges: none
      responses:
        '200':
          description: Predvoľby, ktoré teraz platia.
          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`: limit `account` alebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databáza zostala zamknutá), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /account/password:
    post:
      operationId: changePassword
      tags:
        - account
      summary: Zmeniť heslo
      description: |-
        Zmení heslo. Vyžaduje sa aktuálne heslo, ale nie druhý faktor, ani pri zapnutom dvojfaktorovom overení. Nové heslo sa riadi pravidlami registrácie (kontrolujú sa po aktuálnom hesle).

        Zmena má tieto účinky v jednej transakcii (všetky, alebo žiadny): odvolá všetky ostatné relácie (táto zostane prihlásená), zruší čakajúcu zmenu e-mailovej adresy, odkazy na resetovanie hesla tohto účtu prestanú fungovať a vlastníkovi príde e-mail.

        **Opätovná autentifikácia:** iba heslo. **Limity:** `reauth`, potom `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: Heslo je zmenené.
          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` (účet len s prihlásením cez Google), `weak_password` (s `reason`), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password`: nesprávne aktuálne heslo alebo resetovanie či zmena hesla, ktoré prebehli počas kontroly požiadavky.'
        '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` (neúspešné opätovné autentifikácie účtu) alebo `rate_limited` (limit `reauth` alebo `reauth_user`, kvóta účtu, front na hashovanie hesiel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: front na hashovanie hesiel alebo databáza zostala zamknutá (`retryAfter: 1`, nič sa nezmenilo). `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /account/mfa/totp/setup:
    post:
      operationId: startTotpSetup
      tags:
        - two-step-verification
      summary: Začať zapínanie dvojfaktorového overenia
      description: |-
        Uloží nový čakajúci tajný kľúč, ktorý nahradí prípadný skorší čakajúci kľúč, a vráti ho pre overovaciu aplikáciu (ako text a ako URI `otpauth://` na zobrazenie v podobe QR kódu). Dvojfaktorové overenie ešte nie je zapnuté: zapne ho `POST /account/mfa/totp/enable` s kódom z tohto kľúča.

        **Opätovná autentifikácia:** iba heslo. **Limity:** `reauth`, potom `reauth_user`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasswordOnlyRequest'
            example:
              password: correct horse battery
      responses:
        '200':
          description: Čakajúci tajný kľúč.
          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` (účet len s prihlásením cez Google), `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` (kontroluje sa pred heslom).'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (neúspešné opätovné autentifikácie účtu) alebo `rate_limited` (limit `reauth` alebo `reauth_user`, kvóta účtu, front na hashovanie hesiel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (front na hashovanie hesiel alebo databáza zostala zamknutá), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /account/mfa/totp/enable:
    post:
      operationId: enableTotp
      tags:
        - two-step-verification
      summary: Dokončiť zapnutie dvojfaktorového overenia a získať kódy na obnovenie
      description: |-
        Zapne dvojfaktorové overenie kódom z čakajúceho tajného kľúča (presne 6 číslic). Heslo sa tu nevyžaduje; bolo zadané pri nastavení. Odpoveď obsahuje 10 kódov na obnovenie, zobrazených iba tento raz.

        **Limity:** `reauth`, potom `reauth_user`; nesprávny kód sa započíta do neúspešných opätovných autentifikácií účtu.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TotpEnableRequest'
            example:
              code: '123456'
      responses:
        '200':
          description: Dvojfaktorové overenie je zapnuté.
          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` nemá presne 6 číslic alebo telo porušuje schému; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_code`: nesprávny kód (skontrolujte čas zariadenia).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`mfa_already_enabled`; `mfa_setup_required`: neexistuje čakajúci tajný kľúč (najprv zavolajte `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` (neúspešné opätovné autentifikácie účtu) alebo `rate_limited` (limit `reauth` alebo `reauth_user`, kvóta účtu).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databáza zostala zamknutá), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /account/mfa/totp/disable:
    post:
      operationId: disableTotp
      tags:
        - two-step-verification
      summary: Vypnúť dvojfaktorové overenie
      description: |-
        Vypne dvojfaktorové overenie. Tajný kľúč a kódy na obnovenie sa odstránia a vlastník dostane e-mail.

        **Opätovná autentifikácia:** heslo a kód z overovacej aplikácie alebo kód na obnovenie (vyžaduje sa jedno z `code` alebo `recoveryCode`). **Limity:** `reauth`, potom `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: Dvojfaktorové overenie je vypnuté.
          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` (účet len s prihlásením cez Google), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`mfa_code_required` (chýba `code` aj `recoveryCode`, kontroluje sa pred heslom), `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` (neúspešné opätovné autentifikácie alebo príliš veľa vyskúšaných kódov na tomto účte) alebo `rate_limited` (limit `reauth` alebo `reauth_user`, kvóta účtu, front na hashovanie hesiel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (front na hashovanie hesiel alebo databáza zostala zamknutá), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /account/mfa/recovery-codes:
    post:
      operationId: regenerateRecoveryCodes
      tags:
        - two-step-verification
      summary: Nahradiť kódy na obnovenie
      description: |-
        Nahradí kódy na obnovenie 10 novými; staré prestanú fungovať.

        **Opätovná autentifikácia:** heslo a kód z overovacej aplikácie v `code` (kód na obnovenie sa odmietne s 403 `invalid_code`). **Limity:** `reauth`, potom `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: Nové kódy na obnovenie, zobrazené iba tento raz.
          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` (účet len s prihlásením cez Google), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password`, `mfa_code_required`, `invalid_code` (aj pri kóde na obnovenie).'
        '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` (neúspešné opätovné autentifikácie alebo príliš veľa vyskúšaných kódov na tomto účte) alebo `rate_limited` (limit `reauth` alebo `reauth_user`, kvóta účtu, front na hashovanie hesiel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (front na hashovanie hesiel alebo databáza zostala zamknutá), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /account/email:
    post:
      operationId: changeEmail
      tags:
        - email-change
      summary: Zmeniť e-mailovú adresu
      description: |-
        Zmení e-mailovú adresu účtu. Z `newEmail` sa odstránia medzery na začiatku a na konci, prevedie sa na malé písmená a potom sa skontroluje ako adresa pri registrácii.

        **S potvrdením e-mailu** (predvolené): 202 `verification_sent`. Na novú adresu príde odkaz platný 24 hodín; otvorí `/confirm-email-change` a adresa sa zmení, až keď hráč stlačí tlačidlo na tejto stránke. Dovtedy `GET /account/me` zobrazuje novú adresu ako `pendingEmail`. Nová požiadavka nahradí čakajúcu; zmena alebo resetovanie hesla ju zruší. Na danú novú adresu príde najviac jeden odkaz za 5 minút bez ohľadu na to, kto oň žiada (požiadavka na zmenu, ktorá už čaká, ponechá skôr odoslaný odkaz, ktorý zostáva platný); odpoveď je rovnaká. Aktuálna adresa dostane upozornenie, že bola vyžiadaná zmena na maskovanú adresu (`a***@example.org`). Odpoveď aj `pendingEmail` sú rovnaké, keď novú adresu už používa iný účet: vtedy sa neodošle žiadny odkaz, takže táto zmena sa nikdy nedokončí, a vlastník tejto adresy namiesto toho dostane upozornenie (najviac jedno za hodinu).

        Po potvrdení odkazu sa adresa zmení a považuje sa za potvrdenú, zariadenia zostanú prihlásené, skôr odoslané odkazy (potvrdenie, resetovanie hesla, iné zmeny) prestanú fungovať a predchádzajúca adresa dostane oznámenie, v ktorom je nová adresa maskovaná.

        **Bez potvrdenia e-mailu** (`REQUIRE_EMAIL_VERIFICATION=false`): adresa sa zmení okamžite (200 `email_changed`) a predchádzajúca adresa dostane oznámenie. Ak adresu používa iný účet, odpoveď je 409 `email_taken` a vlastník tejto adresy dostane upozornenie.

        **Opätovná autentifikácia:** heslo a pri dvojfaktorovom overení aj kód z overovacej aplikácie alebo kód na obnovenie. `invalid_email` a `same_email` sa kontrolujú pred heslom, takže sa nezapočítavajú ako neúspech. **Limity:** `reauth`, potom `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: Adresa sa zmenila okamžite (tento server nepoužíva potvrdenie e-mailu).
          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: Nová adresa v uloženej podobe.
              example:
                status: email_changed
                email: alice.new@example.org
        '202':
          description: Na novú adresu bol odoslaný potvrdzovací odkaz (alebo adresa patrí inému účtu; odpoveď to neprezradí).
          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` (aktuálna adresa účtu), `password_not_set` (účet len s prihlásením cez Google), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password` (aj keď požiadavku predbehla zmena alebo resetovanie hesla: neodošle sa žiadny odkaz), `mfa_code_required`, `invalid_code`.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`email_taken`: iba bez potvrdenia e-mailu.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (neúspešné opätovné autentifikácie alebo príliš veľa vyskúšaných kódov na tomto účte) alebo `rate_limited` (limit `reauth` alebo `reauth_user`, kvóta účtu, front na hashovanie hesiel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: front na hashovanie hesiel alebo databáza zostala zamknutá (`retryAfter: 1`: nič sa nezmenilo a rovnakú požiadavku možno poslať znova). `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /account/export:
    post:
      operationId: exportAccountData
      tags:
        - data-export
      summary: Stiahnuť údaje účtu
      description: |-
        Všetko, čo server o účte uchováva, ako jeden súbor JSON na uloženie (`format` `scacelith-account-export`, `version` 1). Export zaznamená bezpečnostnú udalosť (`account_exported`). Pole `notes` v dokumente hráčovi v jednoduchej angličtine vysvetľuje, čo je vynechané.

        **V exporte nikdy nie je:** hash hesla, tajný kľúč dvojfaktorového overenia ani kódy na obnovenie; žiadny token relácie alebo odkazu ani jeho hash; údaje systému proti podvádzaniu (úroveň a skóre integrity, anomálie, analýza partií, váha nahlásenia); nahlásenia, ktoré na hráča podali iní hráči; totožnosť moderátorov; súkromné údaje iných hráčov (súperi sa zobrazujú pod verejným menom a ratingom, nič neprezrádza, či bol iný hráč sankcionovaný, a nie je zahrnutá žiadna IP adresa, ktorá môže patriť inej osobe).

        **Opätovná autentifikácia:** heslo a pri dvojfaktorovom overení aj kód z overovacej aplikácie alebo kód na obnovenie. **Limity:** `account_export` (5 za hodinu na hráča, pre celý server; počíta sa každý pokus vrátane neúspešných; kontroluje sa ako prvý), potom `reauth` a `reauth_user`. **Časový limit:** 60 sekúnd.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReauthCredentials'
            example:
              password: correct horse battery
              code: '123456'
      responses:
        '200':
          description: Exportovaný dokument ako príloha.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            Content-Disposition:
              description: '`attachment; filename="scacelith-account-<username>.json"`. Znaky používateľského mena okrem písmen, číslic, `_`, `.` a `-` sa nahradia znakom `_`.'
              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` (účet len s prihlásením cez Google), `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` (neúspešné opätovné autentifikácie alebo príliš veľa vyskúšaných kódov na tomto účte) alebo `rate_limited` (limit `account_export`, `reauth` alebo `reauth_user`, kvóta účtu, front na hashovanie hesiel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (databáza zostala zamknutá, s hlavičkou `Retry-After: 1`), `server_busy` (front na hashovanie hesiel), `timeout` (60 sekúnd). Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /account/delete:
    post:
      operationId: deleteAccount
      tags:
        - account-deletion
      summary: Odstrániť účet
      description: |-
        Odstráni účet; túto akciu nemožno vrátiť späť.

        - Všetky relácie sa okamžite odvolajú: token odvtedy dostáva 401 `invalid_token`.
        - Používateľské meno sa zmení na `deleted#<id>`, v účte aj v každom zázname partie.
        - Vymažú sa: e-mailová adresa, hash hesla, tajný kľúč dvojfaktorového overenia a kódy na obnovenie, relácie a tokeny odkazov, prepojenie s účtom Google, záznam integrity zo systému proti podvádzaniu a IP adresy uložené pri bezpečnostných udalostiach.
        - Ratingy a partie sa zachovajú. Partie zostanú čitateľné pod anonymným menom a `GET /players/{username}` pre predchádzajúce meno odpovedá 404.

        **Opätovná autentifikácia:** heslo a pri dvojfaktorovom overení aj kód z overovacej aplikácie alebo kód na obnovenie. **Limity:** `reauth`, potom `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: Účet je odstránený.
          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` (účet len s prihlásením cez Google), `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` (neúspešné opätovné autentifikácie alebo príliš veľa vyskúšaných kódov na tomto účte) alebo `rate_limited` (limit `reauth` alebo `reauth_user`, kvóta účtu, front na hashovanie hesiel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (front na hashovanie hesiel alebo databáza zostala zamknutá), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /account/games:
    get:
      operationId: listAccountGames
      tags:
        - game-history
      summary: Vypísať partie hráča s filtrami a stránkovaním
      description: |-
        Partie prihláseného hráča od najnovšej, filtrované a stránkované, s počtom partií, ktoré zodpovedajú filtru. Každý parameter dopytu je nepovinný a prázdna hodnota sa považuje za chýbajúcu.

        Stránkovanie: `next` z predchádzajúcej strany pošlite ako `before`; na poslednej strane je `next` `null`. `total` počíta partie zodpovedajúce filtru na všetkých stranách.

        **Limit:** `account_games`.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/BeforeQuery'
        - $ref: '#/components/parameters/LimitQuery'
        - name: category
          in: query
          required: false
          description: ID oficiálnej kategórie (`3+2` alebo `3%2B2`) alebo `custom` pre všetky partie s iným tempom hry.
          schema:
            type: string
            pattern: '^\s*([0-9]+[+ ][0-9]+|custom)\s*$'
          example: '3+2'
        - name: rated
          in: query
          required: false
          description: Iba hodnotené (`true`) alebo nehodnotené (`false`) partie.
          schema:
            type: string
            enum:
              - 'true'
              - 'false'
        - name: result
          in: query
          required: false
          description: Iba partie vyhrané, prehrané alebo skončené remízou, z pohľadu hráča. Zrušené partie sa zobrazujú iba bez tohto filtra.
          schema:
            type: string
            enum:
              - win
              - loss
              - draw
      responses:
        '200':
          description: Jedna strana histórie.
          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` nie je ID partie), `invalid_limit` alebo `invalid_filter` (`category`, `rated` alebo `result`), každá s `field`, ktoré uvádza názov parametra.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: limit `account_games` alebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (databáza zostala zamknutá; `retryAfter: 1` v tele, bez hlavičky `Retry-After`), `server_busy` (vyhľadanie relácie narazilo na zamknutú databázu), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /games/{id}:
    get:
      operationId: getGame
      tags:
        - games
      summary: Získať záznam partie s ťahmi a hodinami
      description: |-
        Jeden záznam partie s ťahmi a hodinami. Bez tokenu alebo s tokenom hráča, ktorý partiu nehral, je odpoveď verejná. Keď partiu hral hráč, ktorému token patrí, odpoveď pridá `you` a `reportable`.

        **Limit:** `public_read` (na hráča s tokenom, na klienta bez neho).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
      responses:
        '200':
          description: Záznam partie.
          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`: nie je kladné celé číslo s najviac 16 číslicami (menšie ako 2^53); `invalid_request`: nie je platne kódované pre URL.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: bol poslaný token a nie je platný.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: taká partia neexistuje.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: limit `public_read` alebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (databáza zostala zamknutá; `retryAfter: 1` v tele, bez hlavičky `Retry-After`), `server_busy` (vyhľadanie relácie narazilo na zamknutú databázu), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /games/{id}/pgn:
    get:
      operationId: getGamePgn
      tags:
        - games
      summary: Stiahnuť partiu ako súbor PGN
      description: |-
        Tá istá partia ako súbor PGN: jedna partia s koncami riadkov `\n` a text ťahov v riadkoch kratších ako 80 stĺpcov. Odpoveď je rovnaká s tokenom aj bez neho.

        Značky v tomto poradí: `Event` (`<SERVER_NAME> rated <category>` alebo `<SERVER_NAME> casual <category>`), `Site` (`SERVER_PUBLIC_HOST`), `Date` (dátum začiatku v UTC), `Round`, `White`, `Black`, `Result` (`*` pri zrušenej partii), `UTCDate` a `UTCTime` (začiatok), `WhiteElo` a `BlackElo` (ratingy na začiatku alebo `-`), `WhiteRatingDiff` a `BlackRatingDiff` (zmeny, napríklad `+10` a `-10`, v každej hodnotenej partii, `+0`, keď pravidlá ratingu ponechajú rating bez zmeny; nehodnotená partia, partia s vlastným tempom hry a zrušená partia nemajú ani jednu z nich), `TimeControl` (v sekundách), `Termination` (štandardná hodnota PGN: `normal`; `time forfeit`, prekročenie času, aj keď sa skončí remízou; `abandoned`; `rules infraction`, druhý nemožný ťah alebo kontumácia za porušenie pravidiel férovej hry; `unterminated`, zrušená partia), `PlyCount`, `ScacelithGameId` (desiatkové ID).

        Každý ťah nesie `{[%clk h:mm:ss.f] [%emt h:mm:ss.f]}`: stav hodín hráča, ktorý ťahal, po ťahu a čas započítaný za ťah, v desatinách sekundy (orezané). Hodnota sa vynechá, keď ju záznam nemá. Za posledným ťahom nasleduje slovami dôvod ukončenia a potom výsledok.

        **Limit:** `public_read` (na hráča s tokenom, na klienta bez neho).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
      responses:
        '200':
          description: Súbor PGN.
          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: 'Text PGN v UTF-8 (`Content-Type: application/x-chess-pgn; charset=utf-8`).'
              example: |
                [Event "Scacelith rated 3+2"]
                [Site "caissa.scacelith.com"]
                [Date "2026.09.28"]
                [Round "-"]
                [White "alice"]
                [Black "bob"]
                [Result "1-0"]
                [UTCDate "2026.09.28"]
                [UTCTime "18:30:05"]
                [WhiteElo "1500"]
                [BlackElo "1520"]
                [WhiteRatingDiff "+10"]
                [BlackRatingDiff "-10"]
                [TimeControl "180+2"]
                [Termination "normal"]
                [PlyCount "2"]
                [ScacelithGameId "4100000000001"]

                1. e4 {[%clk 0:03:00.0] [%emt 0:00:00.0]} 1... e5 {[%clk 0:03:00.3]
                [%emt 0:00:01.7]} {Resignation} 1-0
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_game_id`: nie je kladné celé číslo s najviac 16 číslicami (menšie ako 2^53); `invalid_request`: nie je platne kódované pre URL.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: bol poslaný token a nie je platný.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: taká partia neexistuje.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: limit `public_read` alebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`internal_error`: okrem iného, keď uložené ťahy nemožno prehrať až po uložené ukončenie partie (server to zapíše do logu).'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (databáza zostala zamknutá; `retryAfter: 1` v tele, bez hlavičky `Retry-After`), `server_busy` (vyhľadanie relácie narazilo na zamknutú databázu), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /games/{id}/gif:
    get:
      operationId: getGameGif
      tags:
        - gifs
      summary: Stiahnuť partiu z tohto servera ako animovaný GIF
      description: |-
        Partia ako animovaný GIF. Mená a ratingy sú tie zo záznamu partie (ratingy na začiatku, odstránený účet ako `deleted#<id>`), rovnako ako výsledok a spôsob ukončenia (`Resignation`, `Loss on time`...).

        Kontroly prebiehajú v tomto poradí: `gif_disabled`, možnosti obrázka (`size`, `orientation`, `delay`, `coords`), ID partie, partia, jej dĺžka.

        **Povinná relácia:** kvóty sa počítajú na účet. **Limity:** `gif` pri každej požiadavke; `gif_user_min`, `gif_user_hour`, `gif_ip_min` a `gif_ip_hour` iba vtedy, keď treba GIF vytvoriť (pozri popis sekcie). **Časový limit:** `GIF_QUEUE_TIMEOUT_MS` + `GIF_RENDER_TIMEOUT_MS` + 5 sekúnd (predvolene 45 sekúnd).
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
        - name: size
          in: query
          required: false
          description: 'Veľkosť obrázka: `small` (polia 32 px), `medium` (48 px) alebo `large` (72 px).'
          schema:
            type: string
            enum:
              - small
              - medium
              - large
            default: medium
        - name: orientation
          in: query
          required: false
          description: Strana, ktorá je dole na šachovnici.
          schema:
            type: string
            enum:
              - white
              - black
            default: white
        - name: delay
          in: query
          required: false
          description: Milisekundy na ťah (1 až 6 desiatkových číslic).
          schema:
            type: integer
            minimum: 100
            maximum: 3000
            default: 500
        - name: coords
          in: query
          required: false
          description: Či sa okolo šachovnice kreslia písmená stĺpcov a čísla radov (`1`), alebo nie (`0`).
          schema:
            type: string
            enum:
              - '1'
              - '0'
            default: '1'
      responses:
        '200':
          $ref: '#/components/responses/GifFile'
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_option` s `field` (`size`, `orientation`, `delay` alebo `coords`): hodnota mimo povolených. `invalid_game_id`. `invalid_request`: nie je platne kódované pre URL.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: taká partia neexistuje. `gif_disabled`: server má GIF-y vypnuté (`GIF_ENABLED=false`; žetón limitu `gif` sa vráti).'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '422':
          $ref: '#/components/responses/UnprocessableContent'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: limit `gif`, limit vykresľovania alebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`render_failed`: GIF sa nepodarilo vytvoriť (server zapíše dôvod do logu). `internal_error`.'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` s `retryAfter` (3 až 10 sekúnd) a `Retry-After`: front vykresľovania je plný alebo GIF čakal `GIF_QUEUE_TIMEOUT_MS` (10 sekúnd) na voľné vlákno; všetky žetóny limitov, ktoré požiadavka vzala, sa vrátia. `busy`: databáza zostala zamknutá (`retryAfter: 1` iba v tele). `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /gif:
    post:
      operationId: renderPgnGif
      tags:
        - gifs
      summary: Vytvoriť animovaný GIF z ľubovoľnej partie poslanej ako PGN
      description: |-
        Ten istý obrázok ako `GET /games/{id}/gif` pre ľubovoľnú partiu poslanú ako text PGN: partiu uloženú hrou, export z inej stránky, ručne napísanú partiu. Použije sa iba prvá partia z textu.

        - Parser PGN prijíma to isté ako parser samotnej hry: každé PGN, ktoré tento server zapisuje, a bežné exporty iných stránok (komentáre, varianty, NAG a údaje o hodinách sa preskakujú; čísla ťahov a SAN sa čítajú tolerantne; značka `FEN` určuje východiskovú pozíciu, ak `SetUp` nie je `"0"`).
        - Mená a ratingy pochádzajú zo značiek `White`, `Black`, `WhiteElo` a `BlackElo`. Písmená s diakritikou o ňu prídu, ostatné znaky mimo tlačiteľných znakov ASCII sa zmenia na `?` a dlhé mená sa skrátia (48 znakov). Výsledok pochádza zo značky `Result`, inak z konca textu ťahov. Značka `Termination` sa zobrazí, ak nie je `normal`: vtedy o konci partie vypovie koncová pozícia (mat, pat).
        - Tento endpoint kontroluje telo sám: neznáme pole alebo `pgn`, ktoré chýba alebo nie je reťazec, vráti 400 `invalid_request` s `field`; nesprávna možnosť vráti 400 `invalid_option` (`delayMs` musí byť číslo JSON, `coords` boolovská hodnota; `null` sa odmieta).

        **Povinná relácia:** kvóty sa počítajú na účet. **Limity:** ako pri `GET /games/{id}/gif`. **Limit veľkosti tela:** 135 168 bajtov bez ohľadu na `HTTP_BODY_LIMIT` (PGN ako reťazec JSON vrátane escape sekvencií). **Časový limit:** predvolene 45 sekúnd.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GifRequest'
            examples:
              short:
                summary: Krátka ručne zadaná partia, bez súradníc
                value:
                  pgn: 1. f3 e5 2. g4 Qh4# 0-1
                  coords: false
              options:
                summary: Súbor PGN so všetkými možnosťami
                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` s `field` (neznáme pole alebo `pgn` chýba či nie je reťazec; bez `field`, keď telo nie je objekt); `invalid_json`; `invalid_option` s `field` (`size`, `orientation`, `delayMs` alebo `coords`); `invalid_pgn` s `line` a `column` (od 1, stĺpce v znakoch) a `message` parsera: nemožný alebo nejednoznačný ťah, poškodená značka, neznámy variant šachu, viac ako 65 536 bajtov.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`gif_disabled`: server má GIF-y vypnuté (`GIF_ENABLED=false`; žetón limitu `gif` sa vráti).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
          description: '`payload_too_large`: telo väčšie ako 135 168 bajtov.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '422':
          $ref: '#/components/responses/UnprocessableContent'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: limit `gif`, limit vykresľovania alebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`render_failed`: GIF sa nepodarilo vytvoriť (server zapíše dôvod do logu). `internal_error`.'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` s `retryAfter` (3 až 10 sekúnd) a `Retry-After`: front vykresľovania je plný alebo GIF čakal na voľné vlákno príliš dlho; všetky žetóny limitov, ktoré požiadavka vzala, sa vrátia. `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /players/{username}:
    get:
      operationId: getPlayer
      tags:
        - players
      summary: Získať verejný profil hráča
      description: |-
        Verejný profil hráča: ratingy v oficiálnych kategóriách (v poradí servera) a počty partií. `games.total` počíta každú uloženú partiu vrátane nehodnotených a zrušených; `games.rated`, `wins`, `draws` a `losses` sú súčty cez záznamy ratingu, teda iba hodnotené partie. Odpoveď je rovnaká s tokenom aj bez neho.

        **Limit:** `public_read` (na hráča s tokenom, na klienta bez neho).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Username'
      responses:
        '200':
          description: Profil.
          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`: nie je to 2 až 24 znakov z `[A-Za-z0-9_.-]`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: bol poslaný token a nie je platný.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: taký hráč neexistuje alebo ide o odstránený účet.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: limit `public_read` alebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (databáza zostala zamknutá; `retryAfter: 1` v tele, bez hlavičky `Retry-After`), `server_busy` (vyhľadanie relácie narazilo na zamknutú databázu), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /players/{username}/games:
    get:
      operationId: listPlayerGames
      tags:
        - players
      summary: Vypísať nedávne partie hráča
      description: |-
        Nedávne partie hráča od najnovšej, stránkované ako história, ale bez filtrov a bez celkového počtu. `color` je farba tohto hráča. `next` je ID poslednej partie vždy, keď je strana plná, takže ďalšia strana môže byť prázdna. Hráč sa vyhľadá ako prvý: neznámy hráč znamená 404 bez ohľadu na parametre dopytu.

        **Limit:** `public_read` (na hráča s tokenom, na klienta bez neho).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Username'
        - $ref: '#/components/parameters/BeforeQuery'
        - $ref: '#/components/parameters/LimitQuery'
      responses:
        '200':
          description: Jedna strana partií hráča.
          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` nie je ID partie), `invalid_limit` (tieto dve bez `field`).'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: bol poslaný token a nie je platný.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: taký hráč neexistuje alebo ide o odstránený účet.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: limit `public_read` alebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (databáza zostala zamknutá; `retryAfter: 1` v tele, bez hlavičky `Retry-After`), `server_busy` (vyhľadanie relácie narazilo na zamknutú databázu), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /leaderboard:
    get:
      operationId: getLeaderboard
      tags:
        - leaderboard
      summary: Získať najlepších hráčov kategórie
      description: |-
        100 najlepších hodnotených záznamov ratingu oficiálnej kategórie s aspoň `minGames` (`PROVISIONAL_GAMES`) započítanými partiami, bez odstránených účtov a hráčov s potvrdeným podvádzaním. Server prepočíta každý rebríček najviac raz za 10 sekúnd; `updatedAt` uvádza, kedy. Pre kategóriu beží naraz iba jeden výpočet: medzitým ostatné požiadavky dostanú predchádzajúci zoznam alebo na výpočet počkajú, ak ešte žiadny zoznam nie je.

        **Limity:** iba vrstva podľa adresy.
      security: []
      parameters:
        - name: category
          in: query
          required: true
          description: ID oficiálnej kategórie (`3+2` alebo `3%2B2`).
          schema:
            type: string
            pattern: '^\s*[0-9]+[+ ][0-9]+\s*$'
          example: '3+2'
        - name: limit
          in: query
          required: false
          description: Počet hráčov, 1 až 100. Väčšie číslo s najviac 3 číslicami sa považuje za 100; prázdna hodnota sa považuje za chýbajúcu.
          schema:
            type: integer
            minimum: 1
            maximum: 999
            default: 100
      responses:
        '200':
          description: Rebríček.
          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` (chýba alebo nejde o oficiálnu kategóriu), `invalid_limit`.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: vrstva podľa adresy.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (databáza zostala zamknutá; `retryAfter: 1` v tele, bez hlavičky `Retry-After`), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /reports:
    post:
      operationId: reportPlayer
      tags:
        - reports
      summary: Nahlásiť súpera z nedávnej partie
      description: |-
        Nahlási súpera z jednej z hráčových vlastných partií, ktorá sa skončila za posledných 7 dní. Nahlásenie samo osebe nikdy nezmení rating, sankciu ani úroveň integrity. Zvýši prioritu posúdenia, ktorú vidia moderátori, a okrem `abuse` vyžiada analýzu partie šachovým programom.

        Nahlásenie toho istého súpera za tú istú partiu dostane rovnakú odpoveď a nič nezmení; odpoveď nikdy nič neprezradí o nahlásenom účte. `GET /games/{id}` hráčom partie vopred oznámi, či by sa nahlásenie prijalo (`reportable`).

        Tento endpoint kontroluje telo sám, v poradí `gameId`, `reported`, `category`, `comment`: pri chybe odpovie 400 `invalid_request` bez `field`. Polia, ktoré nepozná, ignoruje.

        **Limity:** `reports` (na hráča), potom `REPORTS_PER_DAY` (5) nahlásení na hráča za 24 hodín (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: Nahlásenie je prijaté (alebo už bolo podané).
          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` (bez `field`), `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`report_not_allowed`: nejde o súpera nahlasujúceho hráča v partii, ktorá sa skončila za posledných 7 dní.'
        '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`, s hlavičkou `Retry-After`): `REPORTS_PER_DAY` nahlásení za posledných 24 hodín. `rate_limited`: limit `reports` alebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (vyhľadanie relácie narazilo na zamknutú databázu), `timeout`. Aj `server_busy` s `retryAfter` 2 až 5, keď server výrazne zaostáva (pozri úvod): požiadavka nebola vykonaná.'
  /verify-email:
    servers:
      - url: https://caissa.scacelith.com
        description: Oficiálny server (stránky sú v koreni, mimo `/api/v1`).
      - url: https://{host}:{port}
        description: Ľubovoľný server Scacelith (stránky sú v koreni, mimo `/api/v1`).
        variables:
          host:
            default: caissa.scacelith.com
            description: Verejný názov hostiteľa servera (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Verejný port API (`PUBLIC_API_PORT`, inak `API_PORT`).
    get:
      operationId: showVerifyEmailPage
      tags:
        - pages
      summary: Zobraziť stránku na potvrdenie e-mailu
      description: |-
        Stránka potvrdzovacieho odkazu z e-mailu (registrácia alebo znova odoslaný potvrdzovací e-mail). Zobrazuje iba tlačidlo „Confirm my e-mail address“ (potvrdiť moju e-mailovú adresu), aby odkaz nespotreboval skener e-mailov, ktorý ho otvorí; tlačidlo odošle formulár na `POST /verify-email`.

        **Limit:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Stránka s potvrdzovacím tlačidlom.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Odkaz je neplatný alebo mu vypršala platnosť (stránka HTML).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Limit `page` (stránka HTML) alebo vrstva podľa adresy (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitVerifyEmailPage
      tags:
        - pages
      summary: Potvrdiť e-mailovú adresu
      description: |-
        Formulár potvrdzovacej stránky. Potvrdí adresu; pri novej registrácii vtedy vytvorí účet a hráč sa môže prihlásiť.

        **Limit:** `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: Adresa je potvrdená (a účet novej registrácie vytvorený).
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Odkaz je neplatný, použitý alebo mu vypršala platnosť, alebo je formulár neplatný (stránka HTML).
        '408':
          $ref: '#/components/responses/HtmlRequestTimeout'
        '409':
          $ref: '#/components/responses/HtmlConflict'
          description: Odkaz novej registrácie, ktorej používateľské meno alebo adresu medzitým získal iný účet; účet sa nevytvorí (stránka HTML).
        '413':
          $ref: '#/components/responses/HtmlPayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/HtmlUnsupportedMediaType'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Limit `auth` (stránka HTML) alebo vrstva podľa adresy (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Databáza zostala zamknutá (`Retry-After: 1`) alebo server výrazne zaostáva (`Retry-After` 2 až 5, pozri úvod): nič sa nezmenilo a odkaz stále funguje. Aj vypršanie časového limitu obsluhy požiadavky.'
  /reset-password:
    servers:
      - url: https://caissa.scacelith.com
        description: Oficiálny server (stránky sú v koreni, mimo `/api/v1`).
      - url: https://{host}:{port}
        description: Ľubovoľný server Scacelith (stránky sú v koreni, mimo `/api/v1`).
        variables:
          host:
            default: caissa.scacelith.com
            description: Verejný názov hostiteľa servera (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Verejný port API (`PUBLIC_API_PORT`, inak `API_PORT`).
    get:
      operationId: showResetPasswordPage
      tags:
        - pages
      summary: Zobraziť formulár na resetovanie hesla
      description: |-
        Stránka odkazu na resetovanie hesla: formulár nového hesla (heslo dvakrát). Formulár sa odosiela na `POST /reset-password`.

        **Limit:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Formulár nového hesla.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Odkaz je neplatný (aj keď bol poslaný na adresu, ktorú účet už nemá).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Limit `page` (stránka HTML) alebo vrstva podľa adresy (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitResetPasswordPage
      tags:
        - pages
      summary: Nastaviť nové heslo z formulára na resetovanie
      description: |-
        Formulár stránky na resetovanie; robí to isté ako `POST /auth/password/reset`, v jednej transakcii: všetky zariadenia sa odhlásia, čakajúca zmena e-mailovej adresy sa zruší, ostatné odkazy na resetovanie prestanú fungovať, adresa sa považuje za potvrdenú a vlastník dostane e-mail.

        Najprv sa kontroluje odkaz, potom zhoda oboch hesiel a potom pravidlá pre heslá. Keď je server zaneprázdnený, formulár sa vráti s `Retry-After` a odkaz zostane platný.

        **Limity:** `auth`, potom `auth_reset` (zdieľaný s `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: Heslo je zmenené a všetky zariadenia sú odhlásené.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Znova formulár s chybou (heslá sa nezhodujú, slabé heslo), neplatný odkaz alebo neplatný formulár (stránka HTML).
        '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: Limit `auth` alebo `auth_reset` (stránka HTML), znova formulár s `Retry-After`, keď má tento klient priveľa čakajúcich hashov hesiel (odkaz zostane platný), alebo vrstva podľa adresy (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Znova formulár s `Retry-After`, keď je server zaneprázdnený (front na hashovanie hesiel alebo databáza zostala zamknutá); odkaz zostane platný. Keď server výrazne zaostáva (pozri úvod), namiesto toho stránka „Server error“ (chyba servera) s `Retry-After` (2 až 5): nič sa nevykonalo a odkaz zostane platný. Aj vypršanie časového limitu obsluhy požiadavky.'
  /confirm-email-change:
    servers:
      - url: https://caissa.scacelith.com
        description: Oficiálny server (stránky sú v koreni, mimo `/api/v1`).
      - url: https://{host}:{port}
        description: Ľubovoľný server Scacelith (stránky sú v koreni, mimo `/api/v1`).
        variables:
          host:
            default: caissa.scacelith.com
            description: Verejný názov hostiteľa servera (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Verejný port API (`PUBLIC_API_PORT`, inak `API_PORT`).
    get:
      operationId: showConfirmEmailChangePage
      tags:
        - pages
      summary: Zobraziť stránku na potvrdenie zmeny e-mailu
      description: |-
        Stránka odkazu na zmenu e-mailu: zobrazí novú adresu a meno účtu s tlačidlom „Use this e-mail address“ (použiť túto e-mailovú adresu), ktoré odošle formulár na `POST /confirm-email-change`.

        **Limit:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Stránka s novou adresou a jej potvrdzovacím tlačidlom.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Odkaz je neplatný alebo mu vypršala platnosť (aj keď sa adresa účtu od požiadavky zmenila).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Limit `page` (stránka HTML) alebo vrstva podľa adresy (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitConfirmEmailChangePage
      tags:
        - pages
      summary: Potvrdiť novú e-mailovú adresu
      description: |-
        Formulár stránky na zmenu e-mailu. Adresa sa zmení a považuje sa za potvrdenú; zariadenia zostanú prihlásené; skôr odoslané odkazy (potvrdenie, resetovanie hesla, iné zmeny) prestanú fungovať; predchádzajúca adresa dostane oznámenie s maskovanou novou adresou.

        **Limit:** `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: Adresa je zmenená.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Odkaz je neplatný, použitý alebo mu vypršala platnosť, alebo je formulár neplatný (stránka HTML).
        '408':
          $ref: '#/components/responses/HtmlRequestTimeout'
        '409':
          $ref: '#/components/responses/HtmlConflict'
          description: Adresu medzitým získal iný účet (stránka HTML).
        '413':
          $ref: '#/components/responses/HtmlPayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/HtmlUnsupportedMediaType'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Limit `auth` (stránka HTML) alebo vrstva podľa adresy (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Databáza zostala zamknutá (`Retry-After: 1`) alebo server výrazne zaostáva (`Retry-After` 2 až 5, pozri úvod): nič sa nezmenilo a odkaz stále funguje. Aj vypršanie časového limitu obsluhy požiadavky.'
  /healthz:
    servers:
      - url: https://caissa.scacelith.com
        description: Oficiálny server, v koreni.
      - url: https://caissa.scacelith.com/api/v1
        description: Oficiálny server, pod `/api/v1`.
      - url: https://{host}:{port}
        description: Ľubovoľný server Scacelith, v koreni.
        variables:
          host:
            default: caissa.scacelith.com
            description: Verejný názov hostiteľa servera (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Verejný port API (`PUBLIC_API_PORT`, inak `API_PORT`).
      - url: https://{host}:{port}/api/v1
        description: Ľubovoľný server Scacelith, pod `/api/v1`.
        variables:
          host:
            default: caissa.scacelith.com
            description: Verejný názov hostiteľa servera (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Verejný port API (`PUBLIC_API_PORT`, inak `API_PORT`).
    get:
      operationId: getLiveness
      tags:
        - health
      summary: Overiť, že proces beží
      description: |-
        Odpovedá 200 `ok`, kým proces beží. Funguje aj `HEAD`. Každá iná metóda (vrátane `OPTIONS`) dostane odpoveď 405 `method_not_allowed` s `Allow: GET, HEAD`.

        **Limity:** iba vrstva podľa adresy.
      security: []
      responses:
        '200':
          description: Proces beží.
          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`: vrstva podľa adresy.'
  /readyz:
    servers:
      - url: https://caissa.scacelith.com
        description: Oficiálny server, v koreni.
      - url: https://caissa.scacelith.com/api/v1
        description: Oficiálny server, pod `/api/v1`.
      - url: https://{host}:{port}
        description: Ľubovoľný server Scacelith, v koreni.
        variables:
          host:
            default: caissa.scacelith.com
            description: Verejný názov hostiteľa servera (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Verejný port API (`PUBLIC_API_PORT`, inak `API_PORT`).
      - url: https://{host}:{port}/api/v1
        description: Ľubovoľný server Scacelith, pod `/api/v1`.
        variables:
          host:
            default: caissa.scacelith.com
            description: Verejný názov hostiteľa servera (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Verejný port API (`PUBLIC_API_PORT`, inak `API_PORT`).
    get:
      operationId: getReadiness
      tags:
        - health
      summary: Overiť, že server prijíma hráčov
      description: |-
        Odpovedá 200 `ready`, keď server prijíma hráčov, a 503 `not_ready`, kým sa spúšťa alebo zastavuje. Funguje aj `HEAD`. Každá iná metóda (vrátane `OPTIONS`) dostane odpoveď 405 `method_not_allowed` s `Allow: GET, HEAD`.

        **Limity:** iba vrstva podľa adresy.
      security: []
      responses:
        '200':
          description: Server prijíma hráčov.
          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`: vrstva podľa adresy.'
        '503':
          description: Server sa spúšťa alebo zastavuje.
          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: |-
        Token relácie (`sct_` a za ním 43 znakov base64url) z odpovede na prihlásenie, v hlavičke `Authorization`: `Authorization: Bearer <token>`. Predpona je presne `Bearer` (rozlišujú sa veľké a malé písmená) a jedna medzera; hodnota bez nej alebo token, ktorý nemá 1 až 512 tlačiteľných znakov ASCII, dostane odpoveď 401 `invalid_token` ako každý iný neplatný token. Prázdna hlavička sa považuje za chýbajúcu.
  parameters:
    GameId:
      name: id
      in: path
      required: true
      description: ID partie, kladné desiatkové celé číslo s najviac 16 číslicami bez úvodných núl, menšie ako 2^53.
      schema:
        type: integer
        format: int64
        minimum: 1
        maximum: 9007199254740991
      example: 4100000000001
    SessionId:
      name: id
      in: path
      required: true
      description: ID relácie, ako ho uvádza `GET /auth/sessions`, zapísané bez úvodných núl (`03` nie je relácia 3).
      schema:
        type: integer
        format: int64
        minimum: 1
      example: 3
    Username:
      name: username
      in: path
      required: true
      description: Používateľské meno hráča, bez ohľadu na veľkosť písmen. Server prijme každé meno s 2 až 24 znakmi z `[A-Za-z0-9_.-]` (najširšie pravidlo, aké kedy povoľoval).
      schema:
        type: string
        pattern: '^[A-Za-z0-9_.-]{2,24}$'
      example: alice
    BeforeQuery:
      name: before
      in: query
      required: false
      description: ID partie; vypíšu sa iba staršie partie. Pošlite `next` z predchádzajúcej strany. Prázdna hodnota sa považuje za chýbajúcu.
      schema:
        type: integer
        format: int64
        minimum: 1
        maximum: 9007199254740991
      example: 4100000000002
    LimitQuery:
      name: limit
      in: query
      required: false
      description: Veľkosť strany, 1 až 50. Väčšie číslo s najviac 3 číslicami sa považuje za 50; prázdna hodnota sa považuje za chýbajúcu.
      schema:
        type: integer
        minimum: 1
        maximum: 999
        default: 20
    LinkTokenQuery:
      name: token
      in: query
      required: false
      description: Token odkazu z e-mailu (43 znakov base64url). Ak chýba alebo je nesprávny, zobrazí sa stránka „link invalid or expired“ (odkaz je neplatný alebo mu vypršala platnosť).
      schema:
        type: string
        pattern: '^[A-Za-z0-9_-]{43}$'
      example: asfioO4P0r_Zn5DxWzcQuF6Drgxu7wXGjCyS-UFbTaw
  headers:
    CacheControl:
      description: Každá odpoveď servera zakazuje ukladanie do vyrovnávacej pamäte.
      schema:
        type: string
        const: no-store
    RetryAfter:
      description: Počet sekúnd, ktoré treba počkať pred ďalším pokusom; rovnaká hodnota ako `retryAfter` v tele.
      schema:
        type: integer
        minimum: 1
      example: 30
    WwwAuthenticate:
      description: Výzva Bearer, s `error="invalid_token"` pri neplatnom tokene.
      schema:
        type: string
        enum:
          - Bearer realm="scacelith"
          - Bearer realm="scacelith", error="invalid_token"
    ConnectionClose:
      description: Server po tejto odpovedi zatvorí spojenie.
      schema:
        type: string
        const: close
  responses:
    BadRequest:
      description: '`invalid_request` (s `field`, keď je chyba v niektorom poli tela) alebo `invalid_json`.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalidRequest:
              summary: Pole porušuje schému
              value:
                error: invalid_request
                message: '"password" is required'
                field: password
            invalidJson:
              summary: Telo nie je JSON
              value:
                error: invalid_json
                message: The body is not valid JSON.
            weakPassword:
              summary: Slabé heslo
              value:
                error: weak_password
                message: The password must have at least 10 characters.
                reason: too_short
            invalidPgn:
              summary: PGN, ktoré sa nedá prečítať
              value:
                error: invalid_pgn
                message: illegal move 'Ke3'
                line: 1
                column: 13
    Unauthorized:
      description: '`unauthorized` (chýba hlavička `Authorization`) alebo `invalid_token` (token má chybný formát, vypršala jeho platnosť, bol odvolaný alebo patrí odstránenému účtu).'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        WWW-Authenticate:
          $ref: '#/components/headers/WwwAuthenticate'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            unauthorized:
              summary: Bez relácie
              value:
                error: unauthorized
                message: Log in first.
            invalidToken:
              summary: Neplatný token
              value:
                error: invalid_token
                message: The session is invalid or has expired; log in again.
    Forbidden:
      description: Požiadavka je odmietnutá.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalidPassword:
              summary: Nesprávne heslo pri opätovnej autentifikácii
              value:
                error: invalid_password
                message: Wrong password.
            banned:
              summary: Pozastavený účet
              value:
                error: banned
                message: This account is banned.
                until: 1791487639708
    NotFound:
      description: '`not_found` alebo funkcia, ktorú tento server vypol.'
      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`: telo nedorazilo do 10 sekúnd. Server zatvorí spojenie.'
      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: Požiadavka je v konflikte so stavom servera.
      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`: pokus o prihlásenie cez Google alebo tiket je neznámy, už použitý alebo mu vypršala platnosť; začnite prihlásenie znova.'
      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`: telo presahuje `HTTP_BODY_LIMIT` (predvolene 16 384 bajtov). Server zatvorí spojenie.'
      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`: cieľ požiadavky presahuje 4096 znakov.'
      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`: telo nie je `application/json` alebo jeho znaková sada nie je 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`: partia má viac ako `GIF_MAX_PLIES` (600) polťahov.'
      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`: vyriešte výzvu `pow` a pošlite rovnakú požiadavku znova s `pow: { challenge, nonce }`. `reason` uvádza dôvod: `required`, `malformed`, `signature`, `endpoint`, `network`, `expired`, `bits`, `work` alebo `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` (limit počtu požiadaviek, kvóta účtu alebo front na hashovanie hesiel) alebo `too_many_attempts` (obmedzenie na úrovni účtu), s `retryAfter` a hlavičkou `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: Limit počtu požiadaviek
              value:
                error: rate_limited
                message: Too many requests; try again later.
                retryAfter: 30
            tooManyAttempts:
              summary: Príliš veľa neúspechov na tomto účte
              value:
                error: too_many_attempts
                message: Too many attempts; wait before trying again.
                retryAfter: 4
    InternalError:
      description: '`internal_error`: neočakávané zlyhanie. Server ho zapíše do logu.'
      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`: nesprávny `state` alebo `iss`, alebo Google odmietol kód či poslal ID token, ktorý neprejde overením (vrátane kódu alebo ID tokenu druhého klienta, hry alebo webovej stránky). Nikdy neobsahuje text od Googlu.'
      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` alebo `timeout`: skúste to znova po `retryAfter`, ak je uvedené.'
      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 je zaneprázdnený
              value:
                error: server_busy
                message: The server is busy; try again in a few seconds.
                retryAfter: 9
            busy:
              summary: Databáza zostala zamknutá (čítanie)
              value:
                error: busy
                message: Try again shortly.
                retryAfter: 1
            timeout:
              summary: Server potreboval príliš veľa času
              value:
                error: timeout
                message: The server took too long to answer; try again.
    GifFile:
      description: Animovaný GIF ako príloha.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Content-Disposition:
          description: '`attachment; filename="scacelith-<id>.gif"` pre partiu z tohto servera, `attachment; filename="scacelith-game.gif"` pre PGN poslané cez `POST /gif`.'
          schema:
            type: string
            pattern: '^attachment; filename="scacelith-([1-9][0-9]{0,15}|game)\.gif"$'
          example: attachment; filename="scacelith-4100000000001.gif"
        Content-Length:
          description: Veľkosť súboru v bajtoch.
          schema:
            type: integer
            minimum: 1
      content:
        image/gif:
          schema:
            type: string
            format: binary
    HtmlPage:
      description: Stránka HTML.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlBadRequest:
      description: Stránka HTML, ktorá vysvetľuje odmietnutie.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlRequestTimeout:
      description: Formulár nedorazil do 10 sekúnd (stránka HTML). Server zatvorí spojenie.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Connection:
          $ref: '#/components/headers/ConnectionClose'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlConflict:
      description: Stránka HTML, ktorá vysvetľuje konflikt.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlPayloadTooLarge:
      description: Formulár presahuje `HTTP_BODY_LIMIT` (stránka HTML). Server zatvorí spojenie.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Connection:
          $ref: '#/components/headers/ConnectionClose'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlUnsupportedMediaType:
      description: Telo nie je formulár (`application/x-www-form-urlencoded`) ani JSON alebo jeho znaková sada nie je UTF-8 (stránka HTML).
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlTooManyRequests:
      description: Limit počtu požiadaviek (stránka HTML), s `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: 'Neočakávané zlyhanie: stránka HTML s nadpisom „Server error“ (chyba servera). Server ho zapíše do logu.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlServiceUnavailable:
      description: 'Server je zaneprázdnený (databáza zostala zamknutá alebo server výrazne zaostáva; s `Retry-After`) alebo potreboval príliš veľa času: stránka HTML s nadpisom „Server error“ (chyba servera).'
      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: Jediný tvar každej chybovej odpovede JSON API.
      required:
        - error
        - message
      properties:
        error:
          type: string
          pattern: '^[a-z][a-z0-9_]*$'
          description: Chybový kód v tvare `snake_case`. Klient podľa neho volí, čo zobrazí.
          examples:
            - rate_limited
        message:
          type: string
          description: Anglická veta určená pre logy a ako záložný text.
        retryAfter:
          type: integer
          minimum: 1
          description: Počet sekúnd, ktoré treba počkať pred ďalším pokusom; prítomné iba pri odmietnutiach, ktoré časom pominú. Odpoveď potom nesie aj hlavičku `Retry-After` s rovnakou hodnotou, okrem 503 `busy` pri čítaní histórie, partií, hráčov a rebríčka.
        field:
          type: string
          description: Pole alebo parameter dopytu, v ktorom je chyba (`invalid_request`, `invalid_option` a `invalid_cursor`, `invalid_limit` a `invalid_filter` z `GET /account/games`). Vnorené pole sa zapisuje s bodkou (`pow.nonce`).
        reason:
          type: string
          description: 'Dôvod: pravidlo, ktoré `weak_password` porušuje, alebo prečo bol dôkaz práce vyžiadaný či odmietnutý (`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: 'Iba `banned`: koniec pozastavenia (ms od epochy Unixu), `null` pri trvalom pozastavení.'
        line:
          type: integer
          minimum: 1
          description: 'Iba `invalid_pgn`: riadok chyby, od 1.'
        column:
          type: integer
          minimum: 1
          description: 'Iba `invalid_pgn`: stĺpec chyby, od 1, v znakoch.'
    PowChallenge:
      type: object
      description: Výzva dôkazu práce (`pow` z odpovede 428 `pow_required`).
      required:
        - challenge
        - bits
        - expiresAt
      properties:
        challenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{16,400}\.[A-Za-z0-9_-]{43}$'
          description: Podpísaná výzva, ktorú treba poslať späť bez zmeny.
        bits:
          type: integer
          minimum: 1
          maximum: 26
          description: Počet úvodných nulových bitov, ktoré musí mať `SHA-256(challenge + ":" + nonce)`.
        expiresAt:
          type: integer
          format: int64
          description: Kedy výzva vyprší (ms od epochy Unixu), 2 minúty po vydaní.
    PowAnswer:
      type: object
      description: Odpoveď na výzvu dôkazu práce.
      additionalProperties: false
      required:
        - challenge
        - nonce
      properties:
        challenge:
          type: string
          minLength: 16
          maxLength: 512
          description: '`challenge` z odpovede 428 bez zmeny.'
        nonce:
          type: string
          pattern: '^[0-9]{1,20}$'
          description: Desiatkový reťazec s najviac 20 číslicami taký, že `SHA-256(challenge + ":" + nonce)` začína `bits` nulovými bitmi.
    ServerInfo:
      type: object
      description: Čo klient potrebuje pred prihlásením alebo pripojením.
      required:
        - name
        - serverId
        - motd
        - protocol
        - wsPort
        - wsPath
        - registration
        - emailVerification
        - sso
        - mfa
        - pow
        - categories
        - limits
      properties:
        name:
          type: string
          description: Názov servera (`SERVER_NAME`).
        serverId:
          type:
            - string
            - 'null'
          format: uuid
          description: UUID, ktoré databáza dostane pri prvom spustení. Pri reštartoch zostáva rovnaké. Je `null`, keď ho databáza nevie poskytnúť.
        motd:
          type: string
          description: Správa dňa (`SERVER_MOTD`), môže byť prázdna.
        protocol:
          type: object
          description: Protokol WebSocket (`PROTOCOL.md`).
          required:
            - min
            - max
            - schema
            - subprotocol
          properties:
            min:
              type: integer
              minimum: 1
              description: Najstaršia verzia protokolu, ktorú server podporuje.
            max:
              type: integer
              minimum: 1
              description: Najnovšia verzia protokolu, ktorú server podporuje.
            schema:
              type: integer
              format: int64
              minimum: 0
              maximum: 4294967295
              description: 'Odtlačok schémy protokolu (prvé 4 bajty SHA-256 jej kanonickej podoby ako celé číslo bez znamienka: `0xc649140f`, 3326678031, pre vedľajšiu verziu protokolu 2); iba informatívny, nikdy sa neporovnáva.'
            subprotocol:
              type: string
              description: Token podprotokolu WebSocket (`Sec-WebSocket-Protocol`).
        wsPort:
          type: integer
          minimum: 0
          maximum: 65535
          description: Port WebSocketu, ktorý používajú hráči (`PUBLIC_WS_PORT`, inak `WS_PORT`, inak `API_PORT`).
        wsPath:
          type: string
          const: /ws
          description: Cesta WebSocketu.
        registration:
          type: string
          enum:
            - open
            - closed
          description: Či možno vytvárať nové účty.
        emailVerification:
          type: boolean
          description: Či nové účty potvrdzujú svoju adresu (`REQUIRE_EMAIL_VERIFICATION`).
        sso:
          type: object
          required:
            - google
            - googleWeb
          properties:
            google:
              type: boolean
              description: Či sa hre ponúka prihlásenie cez Google.
            googleWeb:
              type: boolean
              description: Či sa prihlásenie cez Google ponúka aj webovej stránke (`POST /auth/sso/google/web/start`).
        mfa:
          type: boolean
          const: true
          description: Dvojfaktorové overenie je vždy k dispozícii.
        pow:
          type: object
          required:
            - register
          properties:
            register:
              type: integer
              minimum: 0
              maximum: 26
              description: Bity dôkazu práce, ktoré vyžaduje registrácia (0, ak sa dôkaz nevyžaduje).
        categories:
          type: array
          description: Oficiálne (hodnotené) tempá hry (`RATED_CATEGORIES`) v poradí servera. Každé iné tempo hry je `custom`.
          items:
            $ref: '#/components/schemas/Category'
        limits:
          type: object
          description: Pravidlá, ktoré môže klient skontrolovať pred odoslaním formulára.
          required:
            - usernameMin
            - usernameMax
            - usernamePattern
            - passwordMinLength
            - passwordMaxBytes
            - customTimeControls
            - reportsPerDay
            - wsMaxMessageBytes
          properties:
            usernameMin:
              type: integer
              description: Najkratšie používateľské meno (`USERNAME_MIN`).
            usernameMax:
              type: integer
              description: Najdlhšie používateľské meno (`USERNAME_MAX`).
            usernamePattern:
              type: string
              description: Regulárny výraz, ktorému musí zodpovedať nové používateľské meno.
            passwordMinLength:
              type: integer
              description: Najkratšie heslo v znakoch (`PASSWORD_MIN_LENGTH`).
            passwordMaxBytes:
              type: integer
              description: Najdlhšie heslo v bajtoch UTF-8.
            customTimeControls:
              type: boolean
              description: Či výzvy a súkromné partie môžu používať vlastné tempá hry.
            reportsPerDay:
              type: integer
              description: Koľko nahlásení môže hráč podať za 24 hodín (`REPORTS_PER_DAY`).
            wsMaxMessageBytes:
              type: integer
              description: Najväčšia správa WebSocketu, ktorú môže klient poslať, v bajtoch.
      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: Oficiálne tempo hry.
      required:
        - id
        - baseSec
        - incSec
      properties:
        id:
          type: string
          pattern: '^[0-9]+\+[0-9]+$'
          description: 'ID kategórie: minúty a prírastok v sekundách (`3+2`).'
        baseSec:
          type: number
          minimum: 0
          description: Základný čas v sekundách.
        incSec:
          type: number
          minimum: 0
          description: Prírastok na ťah v sekundách.
    AccountView:
      type: object
      description: Účet tak, ako ho vidí jeho hráč (`user` v odpovediach na prihlásenie a v `GET /account/me`).
      required:
        - id
        - username
        - email
        - emailVerified
        - mfaEnabled
        - googleLinked
        - hasPassword
        - acceptChallenges
        - createdAt
        - lastLoginAt
        - pendingEmail
      properties:
        id:
          type: integer
          format: int64
          description: ID účtu.
        username:
          type: string
          description: Používateľské meno.
        email:
          type: string
          format: email
          description: E-mailová adresa.
        emailVerified:
          type: boolean
          description: Či je adresa potvrdená.
        mfaEnabled:
          type: boolean
          description: Či je dvojfaktorové overenie zapnuté.
        googleLinked:
          type: boolean
          description: Či je prepojený účet Google.
        hasPassword:
          type: boolean
          description: '`false` pri účte vytvorenom cez Google, ktorý si ešte nenastavil heslo.'
        acceptChallenges:
          type: string
          enum:
            - all
            - none
          description: Či sa prijímajú priame výzvy podľa mena (pozri `PUT /account/preferences`).
        createdAt:
          type: integer
          format: int64
          description: Kedy bol účet vytvorený (ms od epochy Unixu).
        lastLoginAt:
          type:
            - integer
            - 'null'
          format: int64
          description: Posledné prihlásenie (ms od epochy Unixu) alebo `null`.
        pendingEmail:
          type:
            - string
            - 'null'
          format: email
          description: Nová adresa pri zmene e-mailu, ktorá čaká na potvrdenie odkazom, alebo `null`.
    SessionAnswer:
      type: object
      description: Nová relácia.
      required:
        - token
        - expiresAt
        - user
      properties:
        token:
          type: string
          pattern: '^sct_[A-Za-z0-9_-]{43}$'
          description: Token relácie pre hlavičku `Authorization` a WebSocket.
        expiresAt:
          type: integer
          format: int64
          description: Absolútny koniec relácie (ms od epochy Unixu); limit nečinnosti ju môže ukončiť skôr.
        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: Druhý krok prihlásenia s dvojfaktorovým overením; pokračujte cez `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: Token kroku pre `POST /auth/login/mfa`.
        expiresIn:
          type: integer
          const: 300
          description: Zostávajúce sekundy na dokončenie kroku.
    LoginAnswer:
      description: Relácia alebo druhý krok prihlásenia s dvojfaktorovým overením.
      oneOf:
        - $ref: '#/components/schemas/SessionAnswer'
        - $ref: '#/components/schemas/MfaChallenge'
    SsoNeedsUsername:
      type: object
      description: Prvé prihlásenie cez Google; pokračujte cez `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: Tiket pre `POST /auth/sso/complete`, platný 10 minút.
        suggestedUsername:
          type: string
          description: Používateľské meno vytvorené z mena v účte Google alebo z adresy, alebo `""`, keď nič nevyhovuje.
    SsoNeedsPassword:
      type: object
      description: Adresu používa účet s heslom; pokračujte cez `POST /auth/sso/google/link`. Zatiaľ nie je nič prepojené.
      required:
        - needsPassword
        - linkTicket
        - username
        - expiresIn
      properties:
        needsPassword:
          type: boolean
          const: true
        linkTicket:
          type: string
          pattern: '^sso_[A-Za-z0-9_-]{43}$'
          description: Tiket pre `POST /auth/sso/google/link`.
        username:
          type: string
          description: Používateľské meno tohto účtu (vidí ho iba ten, kto Googlu preukázal vlastníctvo adresy).
        expiresIn:
          type: integer
          const: 600
          description: Počet sekúnd, počas ktorých tiket zostáva platný.
    SsoFinishAnswer:
      description: Odpoveď `POST /auth/sso/google/finish`.
      oneOf:
        - $ref: '#/components/schemas/SessionAnswer'
        - $ref: '#/components/schemas/MfaChallenge'
        - $ref: '#/components/schemas/SsoNeedsUsername'
        - $ref: '#/components/schemas/SsoNeedsPassword'
    SsoStartAnswer:
      type: object
      description: Pokus o prihlásenie cez Google.
      required:
        - attemptId
        - authUrl
        - state
        - expiresIn
      properties:
        attemptId:
          type: string
          pattern: '^sso_[A-Za-z0-9_-]{43}$'
          description: Pokus, pre `POST /auth/sso/google/finish`.
        authUrl:
          type: string
          format: uri
          description: URL Google, ktorú treba po kontrole otvoriť v systémovom prehliadači.
        state:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: '`state`, ktorý musí Google poslať späť.'
        expiresIn:
          type: integer
          const: 600
          description: Počet sekúnd, počas ktorých pokus zostáva platný.
    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: Používateľské meno; ďalej platia pravidlá servera (pozri popis).
        email:
          type: string
          minLength: 1
          maxLength: 254
          description: E-mailová adresa. Odstránia sa z nej medzery na začiatku a na konci a uloží sa malými písmenami.
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Heslo; ďalej platia pravidlá pre heslá.
        pow:
          $ref: '#/components/schemas/PowAnswer'
    LoginRequest:
      type: object
      additionalProperties: false
      required:
        - login
        - password
      properties:
        login:
          type: string
          minLength: 1
          maxLength: 254
          description: Používateľské meno alebo e-mailová adresa (akýkoľvek text so `@`).
        password:
          type: string
          minLength: 1
          maxLength: 1024
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
        pow:
          $ref: '#/components/schemas/PowAnswer'
    ClientLabel:
      type: string
      maxLength: 64
      description: Nepovinné. Zobrazuje sa v zozname prihlásených zariadení (napríklad `Scacelith 1.4 (Windows)`).
    MfaLoginRequest:
      type: object
      additionalProperties: false
      required:
        - mfaToken
      properties:
        mfaToken:
          type: string
          minLength: 1
          maxLength: 64
          description: '`mfaToken` z odpovede na prihlásenie (alebo na prihlásenie cez Google).'
        code:
          type: string
          maxLength: 32
          description: Nepovinné. 6-ciferný kód z overovacej aplikácie alebo kód na obnovenie.
        recoveryCode:
          type: string
          maxLength: 32
          description: Nepovinné. Kód na obnovenie. Vyžaduje sa jedno z `code` alebo `recoveryCode`.
    EmailRequest:
      type: object
      additionalProperties: false
      required:
        - email
      properties:
        email:
          type: string
          minLength: 1
          maxLength: 254
          description: E-mailová adresa.
    PasswordResetRequest:
      type: object
      additionalProperties: false
      required:
        - token
        - newPassword
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: Parameter `token` z odkazu na resetovanie hesla.
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Nové heslo; platia pravidlá pre heslá ako pri registrácii.
    SsoStartRequest:
      type: object
      additionalProperties: false
      required:
        - codeChallenge
        - redirectPort
      properties:
        codeChallenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: Výzva PKCE S256 z `codeVerifier` klienta (`BASE64URL(SHA-256(codeVerifier))`).
        redirectPort:
          type: integer
          minimum: 1024
          maximum: 65535
          description: Port poslucháča klienta na `127.0.0.1`.
    SsoWebStartRequest:
      type: object
      additionalProperties: false
      required:
        - codeChallenge
      properties:
        codeChallenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: Výzva PKCE S256 z `codeVerifier` stránky (`BASE64URL(SHA-256(codeVerifier))`).
    SsoFinishRequest:
      type: object
      additionalProperties: false
      required:
        - attemptId
        - codeVerifier
        - state
        - code
      properties:
        attemptId:
          type: string
          minLength: 1
          maxLength: 64
          description: '`attemptId` z `POST /auth/sso/google/start`.'
        codeVerifier:
          type: string
          pattern: '^[A-Za-z0-9._~-]{43,128}$'
          description: Verifikátor PKCE; jeho SHA-256 musí zodpovedať výzve poslanej do `start`.
        state:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: '`state` tak, ako ho Google poslal späť.'
        code:
          type: string
          minLength: 1
          maxLength: 2048
          pattern: '^[\x21-\x7E]+$'
          description: '`code` tak, ako ho Google poslal späť (tlačiteľné znaky ASCII bez medzier).'
        iss:
          type: string
          minLength: 1
          maxLength: 256
          description: Nepovinné. `iss` tak, ako ho Google poslal späť, ak ho poslal.
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
    SsoLinkRequest:
      type: object
      additionalProperties: false
      required:
        - linkTicket
        - password
      properties:
        linkTicket:
          type: string
          minLength: 1
          maxLength: 64
          description: '`linkTicket` z `finish`, platný 10 minút.'
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Heslo účtu.
        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` z `finish`, platný 10 minút.'
        username:
          type: string
          minLength: 1
          maxLength: 64
          description: Používateľské meno; platia pravidlá registrácie.
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
    SessionList:
      type: object
      required:
        - sessions
      properties:
        sessions:
          type: array
          description: Aktívne relácie, zoradené od naposledy použitej.
          items:
            $ref: '#/components/schemas/SessionEntry'
    SessionEntry:
      type: object
      description: Aktívna relácia (prihlásené zariadenie).
      required:
        - id
        - createdAt
        - lastSeenAt
        - expiresAt
        - clientLabel
        - current
      properties:
        id:
          type: integer
          format: int64
          description: ID relácie pre `DELETE /auth/sessions/{id}`.
        createdAt:
          type: integer
          format: int64
          description: Prihlásenie (ms od epochy Unixu).
        lastSeenAt:
          type: integer
          format: int64
          description: Posledné použitie (ms od epochy Unixu), aktualizované najviac raz za 5 minút (neskôr, kým server výrazne zaostáva).
        expiresAt:
          type: integer
          format: int64
          description: Absolútny koniec (ms od epochy Unixu); limit nečinnosti môže reláciu ukončiť skôr.
        clientLabel:
          type:
            - string
            - 'null'
          description: '`clientLabel` z prihlásenia alebo `null`, ak ho prihlásenie neposlalo.'
        current:
          type: boolean
          description: Či ide o reláciu, ktorá posiela požiadavku.
    AccountMe:
      type: object
      required:
        - user
        - ratings
        - sanctions
        - ban
      properties:
        user:
          $ref: '#/components/schemas/AccountView'
        ratings:
          type: array
          description: Jeden záznam pre každú kategóriu, v ktorej hráč hral hodnotené partie.
          items:
            $ref: '#/components/schemas/RatingSummary'
        sanctions:
          type: array
          description: Aktívne sankcie.
          items:
            $ref: '#/components/schemas/ActiveSanction'
        ban:
          description: '`{ until }` počas pozastavenia účtu, inak `null`.'
          oneOf:
            - $ref: '#/components/schemas/Ban'
            - type: 'null'
    RatingSummary:
      type: object
      description: Záznam ratingu jednej kategórie.
      required:
        - category
        - rating
        - games
        - wins
        - draws
        - losses
        - peak
        - provisional
      properties:
        category:
          type: string
          description: ID kategórie.
        rating:
          type: integer
          description: Rating.
        games:
          type: integer
          minimum: 0
          description: Partie odohrané v kategórii (započítané aj nezapočítané).
        wins:
          type: integer
          minimum: 0
        draws:
          type: integer
          minimum: 0
        losses:
          type: integer
          minimum: 0
        peak:
          type: integer
          description: Najvyšší dosiahnutý rating.
        provisional:
          type: boolean
          description: '`true`, kým rating ešte nie je stanovený (fáza bez ratingu) alebo má menej ako `PROVISIONAL_GAMES` započítaných partií (hra ho zobrazuje ako `1510?`).'
    ActiveSanction:
      type: object
      required:
        - kind
        - reason
        - startsAt
        - endsAt
      properties:
        kind:
          type: string
          enum:
            - ban
            - mm_block
            - warning
          description: Druh sankcie.
        reason:
          type:
            - string
            - 'null'
          description: Uvedený dôvod, ak existuje.
        startsAt:
          type: integer
          format: int64
          description: Začiatok (ms od epochy Unixu).
        endsAt:
          type:
            - integer
            - 'null'
          format: int64
          description: Koniec (ms od epochy Unixu), `null`, ak je sankcia trvalá.
    Ban:
      type: object
      required:
        - until
      properties:
        until:
          type:
            - integer
            - 'null'
          format: int64
          description: Koniec pozastavenia (ms od epochy Unixu), `null`, ak je trvalé.
    Preferences:
      type: object
      additionalProperties: false
      required:
        - acceptChallenges
      properties:
        acceptChallenges:
          type: string
          enum:
            - all
            - none
          description: '`all` na prijímanie priamych výziev podľa mena, `none` na ich odmietanie.'
    PasswordChangeRequest:
      type: object
      additionalProperties: false
      required:
        - currentPassword
        - newPassword
      properties:
        currentPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Aktuálne heslo.
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Nové heslo; platia pravidlá pre heslá ako pri registrácii.
    PasswordOnlyRequest:
      type: object
      additionalProperties: false
      required:
        - password
      properties:
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Heslo účtu.
    TotpEnableRequest:
      type: object
      additionalProperties: false
      required:
        - code
      properties:
        code:
          type: string
          pattern: '^[0-9]{6}$'
          description: Kód z čakajúceho tajného kľúča, presne 6 číslic.
    ReauthCredentials:
      type: object
      additionalProperties: false
      required:
        - password
      properties:
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Heslo účtu.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: Nepovinné; vyžaduje sa pri dvojfaktorovom overení (alebo `recoveryCode`). Kód z overovacej aplikácie alebo kód na obnovenie.
        recoveryCode:
          type: string
          minLength: 1
          maxLength: 32
          description: Nepovinné. Kód na obnovenie (`xxxx-xxxx-xx`; na veľkosti písmen, medzerách a pomlčkách nezáleží).
    RecoveryCodesRequest:
      type: object
      additionalProperties: false
      required:
        - password
        - code
      properties:
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Heslo účtu.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: Kód z overovacej aplikácie (kód na obnovenie sa odmietne).
    EmailChangeRequest:
      type: object
      additionalProperties: false
      required:
        - newEmail
        - password
      properties:
        newEmail:
          type: string
          minLength: 1
          maxLength: 254
          description: Nová adresa; odstránia sa medzery na začiatku a na konci, prevedie sa na malé písmená a potom sa skontroluje ako adresa pri registrácii.
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Heslo účtu.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: Nepovinné; vyžaduje sa pri dvojfaktorovom overení (alebo `recoveryCode`). Kód z overovacej aplikácie alebo kód na obnovenie.
        recoveryCode:
          type: string
          minLength: 1
          maxLength: 32
          description: Nepovinné. Kód na obnovenie.
    TotpSetup:
      type: object
      description: Čakajúci tajný kľúč pre overovaciu aplikáciu.
      required:
        - secret
        - uri
        - algorithm
        - digits
        - period
      properties:
        secret:
          type: string
          pattern: '^[A-Z2-7]+$'
          description: Tajný kľúč v base32 na zadanie do aplikácie.
        uri:
          type: string
          format: uri
          description: URI `otpauth://totp/...` na zobrazenie ako QR kód.
        algorithm:
          type: string
          const: SHA1
        digits:
          type: integer
          const: 6
        period:
          type: integer
          const: 30
          description: Počet sekúnd na krok.
    RecoveryCodeList:
      type: array
      description: 10 kódov na obnovenie, zobrazených iba tento raz. Každý funguje iba raz.
      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: Jedna strana partie.
      required:
        - name
        - rating
        - ratingAfter
        - ratingDiff
      properties:
        name:
          type: string
          description: Meno v zázname partie (`deleted#<id>` pri odstránenom účte).
        rating:
          type:
            - integer
            - 'null'
          description: Rating na začiatku, `null`, ak nie je známy.
        ratingAfter:
          type:
            - integer
            - 'null'
          description: Rating po partii. Hodnotená partia ho má vždy; `null` iba pri partii, ktorá sa do ratingov nezapočítava (nehodnotená, s vlastným tempom hry, zrušená).
        ratingDiff:
          type:
            - integer
            - 'null'
          description: Zmena ratingu, `0`, keď pravidlá ratingu ponechajú rating bez zmeny (nulové skóre podľa FIDE, súper bez ratingu, prvé partie hráča bez ratingu); `null` ako pri `ratingAfter`.
    GameSummary:
      type: object
      description: Súhrn uloženej partie.
      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: ID partie.
        category:
          type: string
          description: ID oficiálnej kategórie (`3+2`) alebo `custom`.
        rated:
          type: boolean
          description: Či sa partia započítava do ratingov.
        timeControl:
          type: string
          pattern: '^[0-9]+\+[0-9]+$'
          description: Tempo hry v sekundách (`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` (pozri kódy partií).'
        reason:
          type: integer
          minimum: 0
          maximum: 255
          description: Kód dôvodu ukončenia (pozri kódy partií).
        result:
          type: string
          enum:
            - 1-0
            - 0-1
            - 1/2-1/2
            - '*'
          description: Výsledok, `*` pri zrušenej partii.
        termination:
          $ref: '#/components/schemas/Termination'
        plies:
          type: integer
          minimum: 0
          description: Počet polťahov.
        startedAt:
          type: integer
          format: int64
          description: Začiatok (ms od epochy Unixu).
        endedAt:
          type: integer
          format: int64
          description: Koniec (ms od epochy Unixu).
    Termination:
      type: string
      description: Názov dôvodu ukončenia (pozri kódy partií); `Unknown` pre kód, ktorý protokol nepozná.
      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: Súhrn partie z pohľadu jedného hráča.
      allOf:
        - $ref: '#/components/schemas/GameSummary'
        - type: object
          required:
            - color
          properties:
            color:
              type: string
              enum:
                - white
                - black
              description: Farba hráča.
    HistoryGameSummary:
      description: Partia z histórie prihláseného hráča.
      allOf:
        - $ref: '#/components/schemas/PlayerGameSummary'
        - type: object
          required:
            - baseMs
            - incMs
            - outcome
          properties:
            baseMs:
              type: integer
              format: int64
              minimum: 0
              description: Základný čas v milisekundách.
            incMs:
              type: integer
              format: int64
              minimum: 0
              description: Prírastok v milisekundách.
            outcome:
              type: string
              enum:
                - win
                - loss
                - draw
                - aborted
              description: Výsledok z pohľadu hráča.
    HistoryPage:
      type: object
      required:
        - games
        - next
        - total
      properties:
        games:
          type: array
          description: Partie na strane, od najnovšej.
          items:
            $ref: '#/components/schemas/HistoryGameSummary'
        next:
          type:
            - integer
            - 'null'
          format: int64
          description: ID, ktoré treba poslať ako `before` pre ďalšiu stranu, alebo `null` na poslednej strane.
        total:
          type: integer
          minimum: 0
          description: Počet partií zodpovedajúcich filtru na všetkých stranách.
    MoveRecord:
      type: object
      description: Jeden polťah partie.
      required:
        - uci
        - spentMs
        - clockMs
      properties:
        uci:
          type: string
          pattern: '^[a-h][1-8][a-h][1-8][nbrq]?$'
          description: Ťah v notácii UCI, pri premene s pridaným `n`, `b`, `r` alebo `q`.
        spentMs:
          type:
            - integer
            - 'null'
          minimum: 0
          description: Čas započítaný za ťah (ms), `null`, ak ho záznam nemá.
        clockMs:
          type:
            - integer
            - 'null'
          minimum: 0
          description: Stav hodín hráča, ktorý ťahal, po ťahu (ms), `null`, ak ho záznam nemá.
    PgnTagsView:
      type: object
      description: Hlavné značky PGN na zobrazenie. `Termination` je tu názov dôvodu ukončenia; súbor PGN používa štandardné hodnoty PGN.
      required:
        - Event
        - Site
        - Date
        - Round
        - White
        - Black
        - Result
        - WhiteElo
        - BlackElo
        - TimeControl
        - Termination
        - PlyCount
      properties:
        Event:
          type: string
          description: '`<SERVER_NAME> rated <category>` alebo `<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: Dátum začiatku v UTC.
        Round:
          type: string
          const: '-'
        White:
          type: string
        Black:
          type: string
        Result:
          type: string
          enum:
            - 1-0
            - 0-1
            - 1/2-1/2
            - '*'
        WhiteElo:
          description: Rating na začiatku alebo `"-"`.
          oneOf:
            - type: integer
            - type: string
              const: '-'
        BlackElo:
          description: Rating na začiatku alebo `"-"`.
          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: Záznam partie s ťahmi a hodinami. Má polia súhrnu partie, bez `color` a `outcome`.
      allOf:
        - $ref: '#/components/schemas/GameSummary'
        - type: object
          required:
            - baseMs
            - incMs
            - statusName
            - rematchOf
            - moves
            - pgn
          properties:
            baseMs:
              type: integer
              format: int64
              minimum: 0
              description: Základný čas v milisekundách.
            incMs:
              type: integer
              format: int64
              minimum: 0
              description: Prírastok v milisekundách.
            statusName:
              type: string
              enum:
                - WhiteWins
                - BlackWins
                - Draw
                - Aborted
              description: Názov hodnoty `status`.
            rematchOf:
              type:
                - integer
                - 'null'
              format: int64
              description: ID partie, ktorej odvetou je táto partia, alebo `null`.
            moves:
              type: array
              description: Jedna položka na polťah.
              items:
                $ref: '#/components/schemas/MoveRecord'
            pgn:
              $ref: '#/components/schemas/PgnTagsView'
            you:
              type: string
              enum:
                - white
                - black
              description: Iba keď túto partiu hral hráč, ktorému token patrí; jeho farba.
            reportable:
              type: boolean
              description: Iba keď túto partiu hral hráč, ktorému token patrí; `true`, keď by `POST /reports` teraz prijal nahlásenie súpera za túto partiu (partia sa skončila za posledných 7 dní, denná kvóta nie je vyčerpaná a súper ešte nie je za túto partiu nahlásený).
    PlayerProfile:
      type: object
      required:
        - username
        - createdAt
        - ratings
        - games
      properties:
        username:
          type: string
          description: Používateľské meno tak, ako ho hráč napísal.
        createdAt:
          type: integer
          format: int64
          description: Kedy bol účet vytvorený (ms od epochy Unixu).
        ratings:
          type: array
          description: Iba oficiálne kategórie, v poradí servera.
          items:
            $ref: '#/components/schemas/RatingSummary'
        games:
          type: object
          required:
            - total
            - rated
            - wins
            - draws
            - losses
          properties:
            total:
              type: integer
              minimum: 0
              description: Všetky uložené partie vrátane nehodnotených a zrušených.
            rated:
              type: integer
              minimum: 0
              description: Hodnotené partie, sčítané cez záznamy ratingu.
            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: Používateľské meno tak, ako ho hráč napísal.
        games:
          type: array
          description: Partie na strane, od najnovšej.
          items:
            $ref: '#/components/schemas/PlayerGameSummary'
        next:
          type:
            - integer
            - 'null'
          format: int64
          description: ID poslednej partie vždy, keď je strana plná (ďalšia strana potom môže byť prázdna), inak `null`.
    Leaderboard:
      type: object
      required:
        - category
        - minGames
        - updatedAt
        - players
      properties:
        category:
          type: string
          description: ID kategórie.
        minGames:
          type: integer
          minimum: 0
          description: Počet započítaných partií, ktorý záznam potrebuje, aby sa zobrazil v rebríčku (`PROVISIONAL_GAMES`).
        updatedAt:
          type: integer
          format: int64
          description: Kedy bol rebríček vypočítaný (ms od epochy Unixu).
        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: Text PGN, najviac 65 536 bajtov UTF-8. Použije sa iba prvá partia.
        size:
          type: string
          enum:
            - small
            - medium
            - large
          default: medium
          description: Nepovinné. Veľkosť obrázka.
        orientation:
          type: string
          enum:
            - white
            - black
          default: white
          description: Nepovinné. Strana, ktorá je dole na šachovnici.
        delayMs:
          type: integer
          minimum: 100
          maximum: 3000
          default: 500
          description: Nepovinné. Milisekundy na ťah, číslo JSON s celočíselnou hodnotou.
        coords:
          type: boolean
          default: true
          description: Nepovinné. Či sa okolo šachovnice kreslia písmená stĺpcov a čísla radov.
    ReportRequest:
      type: object
      required:
        - gameId
        - reported
        - category
      properties:
        gameId:
          description: Partia ako celé číslo alebo reťazec s 1 až 16 číslicami.
          oneOf:
            - type: integer
              format: int64
              minimum: 1
              maximum: 9007199254740991
            - type: string
              pattern: '^[0-9]{1,16}$'
        reported:
          type: string
          minLength: 1
          maxLength: 24
          description: Používateľské meno súpera, ako je v zázname partie alebo ako znie teraz, bez ohľadu na veľkosť písmen. Nesmie byť prázdne.
        category:
          type: string
          enum:
            - cheating
            - abuse
            - other
          description: Čoho sa nahlásenie týka.
        comment:
          type:
            - string
            - 'null'
          description: Nepovinné. Najviac 500 znakov po odstránení riadiacich znakov (okrem tabulátora a nového riadka) a medzier na začiatku a na konci textu.
    AccountExport:
      type: object
      description: Všetko, čo server o účte uchováva (časy v ms od epochy Unixu).
      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: Kedy bol export vytvorený.
        server:
          type: object
          required:
            - name
            - host
          properties:
            name:
              type: string
              description: '`SERVER_NAME`.'
            host:
              type: string
              description: '`SERVER_PUBLIC_HOST`.'
        notes:
          type: array
          description: Čo súbor obsahuje a čo vynecháva, v jednoduchej angličtine pre hráča.
          items:
            type: string
        account:
          description: Zobrazenie účtu z `GET /account/me` s `googleEmail`.
          allOf:
            - $ref: '#/components/schemas/AccountView'
            - type: object
              required:
                - googleEmail
              properties:
                googleEmail:
                  type:
                    - string
                    - 'null'
                  description: Adresa prepojeného účtu Google alebo `null`.
        ratings:
          type: array
          description: Úplné záznamy ratingu.
          items:
            $ref: '#/components/schemas/ExportRating'
        ratingRefunds:
          type: array
          description: Body ratingu vrátené po tom, ako sa zistilo, že súper podvádzal, sčítané za deň (UTC) a kategóriu, od najnovších. Neuvádzajú sa partie ani podvádzajúci hráči.
          items:
            $ref: '#/components/schemas/RatingRefund'
        sessions:
          type: array
          description: Všetky uložené relácie od najnovšej, bez akéhokoľvek tokenu. Čistenie po uplynutí doby uchovávania odstráni reláciu s vypršanou platnosťou a odhlásenú reláciu deň po odhlásení.
          items:
            $ref: '#/components/schemas/ExportSession'
        securityEvents:
          type: array
          description: Bezpečnostné udalosti od najnovšej, uchovávané `RETENTION_SECURITY_DAYS` dní.
          items:
            $ref: '#/components/schemas/SecurityEvent'
        sanctions:
          type: array
          description: Všetky sankcie vrátane zrušených. Meno moderátora sa nikdy neuvádza.
          items:
            $ref: '#/components/schemas/ExportSanction'
        conduct:
          type: array
          description: Udalosti správania zaznamenané pri hráčových opustených, zrušených a nezačatých partiách (bez prvého ťahu včas), uchovávané 30 dní.
          items:
            $ref: '#/components/schemas/ConductEvent'
        reportsFiled:
          type: array
          description: Nahlásenia, ktoré hráč podal.
          items:
            $ref: '#/components/schemas/FiledReport'
        games:
          type: object
          required:
            - total
            - list
          properties:
            total:
              type: integer
              minimum: 0
              description: Počet partií.
            list:
              type: array
              description: Všetky partie od najnovšej ako súhrny z `GET /account/games`. Ťahy sú na `GET /games/{id}` a PGN na `GET /games/{id}/pgn`.
              items:
                $ref: '#/components/schemas/HistoryGameSummary'
    ExportRating:
      description: Úplný záznam ratingu.
      allOf:
        - $ref: '#/components/schemas/RatingSummary'
        - type: object
          required:
            - rated
            - countedGames
            - updatedAt
          properties:
            rated:
              type: boolean
              description: Či hráč už opustil fázu bez ratingu.
            countedGames:
              type: integer
              minimum: 0
              description: Partie, ktoré sa započítali do ratingu.
            updatedAt:
              type: integer
              format: int64
              description: Kedy sa záznam naposledy zmenil.
    RatingRefund:
      type: object
      required:
        - day
        - category
        - points
      properties:
        day:
          type: integer
          format: int64
          description: 00:00 UTC daného dňa (ms od epochy Unixu).
        category:
          type: string
        points:
          type: integer
          description: Body vrátené v daný deň v danej kategórii.
    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: Kedy bola relácia odhlásená, alebo `null`.
        clientLabel:
          type:
            - string
            - 'null'
        ip:
          type:
            - string
            - 'null'
          description: Adresa prihlásenia, vymazaná po `RETENTION_IP_DAYS` dňoch.
    SecurityEvent:
      type: object
      description: |-
        Bezpečnostná udalosť. `ip` sa uvádza iba pri tom, čo sa vykonalo v prihlásenom stave, heslom účtu (a druhým faktorom) alebo odkazom poslaným na jeho adresu: `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` a `account_exported`; každý iný druh má `ip: null` a `ip` sa vymaže po `RETENTION_IP_DAYS` dňoch.

        `detail` uchováva iba tieto polia: `login`: `method`; `sso_login`, `sso_account_created`: `provider`; `sso_linked`: `provider` a `method` (`password` alebo `password+totp`); `login_failed`: `failures`; `login_lockout`: `retryAfterMs`; `mfa_failed`: `attempts`; `recovery_code_used`: `remaining`; `reauth_failed`: `factor`; `session_revoked` a `sessions_revoked_all`: `reason`; `email_change_refused`: `reason`; `sanction_auto`: `kind`, `gameId`, `until`. Udalosť `moderator_action` uchováva iba `{ action }`, pre akcie `ban`, `unban`, `reset_mfa`, `verify_email` a `revoke_sessions` (ostatné akcie moderátorov sa vynechávajú). Každý iný druh má `detail: null`. Udalosti `rating_refund` sa vynechávajú: ich body sú v `ratingRefunds`.
      required:
        - kind
        - at
        - ip
        - detail
      properties:
        kind:
          type: string
          description: Druh udalosti (`login`, `password_changed`...).
        at:
          type: integer
          format: int64
          description: Kedy sa stala.
        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: Aktuálne verejné meno nahláseného hráča.
        category:
          type: string
          enum:
            - cheating
            - abuse
            - other
        comment:
          type:
            - string
            - 'null'
        createdAt:
          type: integer
          format: int64
        status:
          type: string
          enum:
            - open
            - closed
          description: Či je nahlásenie stále otvorené (či bol nahlásený hráč sankcionovaný, sa neuvádza).
    LinkTokenForm:
      type: object
      additionalProperties: false
      required:
        - token
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: Token odkazu z e-mailu (skryté pole formulára stránky).
    ResetPasswordForm:
      type: object
      additionalProperties: false
      required:
        - token
        - newPassword
        - confirmPassword
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: Token odkazu na resetovanie hesla (skryté pole formulára).
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Nové heslo; platia pravidlá pre heslá ako pri registrácii.
        confirmPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Znova nové heslo; musí byť rovnaké.
    HtmlDocument:
      type: string
      contentMediaType: text/html
      description: Stránka HTML v UTF-8 (`text/html; charset=utf-8`), bez JavaScriptu a externých zdrojov.
