openapi: 3.1.1
info:
  title: HTTPS API serveru Scacelith
  version: "1.1.0"
  summary: HTTPS API každého serveru Scacelith, oficiálního i komunitních.
  description: |-
    Toto HTTPS API poskytuje každý server Scacelith, oficiální `caissa.scacelith.com` i každý komunitní server. Hra ho používá na vše mimo samotnou partii: registraci a přihlášení (včetně dvoufázového ověření a přihlášení přes Google), stránku účtu, historii partií, stahování PGN a animované GIFy partií, přihlášená zařízení, stažení dat, smazání účtu a hlášení.

    Hraní naživo (hledání soupeře, výzvy, tahy, hodiny) probíhá přes WebSocket téhož serveru (`wss://<host>/ws`), otevřený tokenem relace z tohoto API; je popsán v `PROTOCOL.md`, ne zde.

    Níže uvedené výchozí hodnoty platí pro server s nezměněnou konfigurací; komunitní server je může změnit (`CONFIG.md` uvádí každé nastavení).

    ## Základní URL, port a přenos

    - Oficiální server: `https://caissa.scacelith.com/api/v1`, port TCP 443.
    - Komunitní server: `https://<SERVER_PUBLIC_HOST>[:<port>]/api/v1`. `API_PORT` je ve výchozím nastavení 443; za NAT nebo proxy je port, který používají hráči, `PUBLIC_API_PORT`.
    - WebSocket hry je ve výchozím nastavení na stejném portu (`WS_PORT` ho přesune jinam; `GET /info` klientovi sdělí, kde je).
    - HTTP/1.1 přes TLS. S `TLS_MODE=proxy` ukončuje TLS reverzní proxy před serverem. `TLS_MODE=off` (prosté HTTP) je jen pro lokální vývoj.
    - HTML stránky otevírané z odkazů v e-mailech (`/verify-email`, `/reset-password`, `/confirm-email-change`) jsou v kořeni serveru, mimo `/api/v1`. Endpointy kontroly stavu odpovídají jak v kořeni, tak pod `/api/v1`. Přihlášení přes Google nemá na serveru žádnou stránku: Google pošle prohlížeč zpět přímo do hry, na `127.0.0.1`, nebo na stránku webu, který přihlášení zahájil (`GOOGLE_WEB_REDIRECT_URI`).
    - Koncové lomítko se ignoruje (`/api/v1/info/` je `/api/v1/info`) a parametry cesty se dekódují z kódování URL (na parametr, který není platným kódováním URL, server odpoví 400 `invalid_request`).

    ## Požadavky

    - **Těla JSON.** Požadavky `POST`, `PUT` a `DELETE` nesou objekt JSON s `Content-Type: application/json`. Jiný `charset` než UTF-8 se odmítá, stejně jako jakýkoli jiný typ obsahu (415 `unsupported_media_type`). Prázdné tělo se počítá jako `{}`, což je právě to, co přijímají endpointy bez parametrů (`POST /auth/logout`, `POST /auth/logout-all`, `DELETE /auth/sessions/{id}`).
    - **Velikost a čas.** Tělo je omezeno na `HTTP_BODY_LIMIT` bajtů (ve výchozím nastavení 16 384; `POST /gif` má vlastní limit 135 168 bajtů): 413 `payload_too_large`. Musí dorazit do 10 sekund: 408 `request_timeout`. Po kterékoli z těchto chyb server spojení ukončí.
    - **Striktní schémata.** Pole, které endpoint nezná, se odmítá, pole neoznačené jako nepovinné je povinné a kontrolují se typy a délky. Na každé takové selhání server odpoví 400 `invalid_request` s `field`, které uvádí název pole (u vnořeného pole s tečkami, například `pow.nonce`). Řetězce nesmějí obsahovat řídicí znaky. Délky se počítají v kódových jednotkách UTF-16, takže znak mimo základní vícejazyčnou rovinu (emoji) se počítá dvakrát. Na tělo, které není JSON, server odpoví 400 `invalid_json`. `POST /gif` a `POST /reports` svá těla kontrolují samy (viz jednotlivé endpointy).
    - **Řetězce dotazu.** Platí první výskyt parametru, neznámé parametry se ignorují a `+` se dekóduje jako mezera: endpointy, které přijímají kategorii tempa hry, přijmou `3%2B2` i `3+2`.
    - **Metody.** `HEAD` je `GET` bez těla. `OPTIONS` na existující cestě vrátí 204 s hlavičkou `Allow` (a s hlavičkami předběžného požadavku CORS, viz níže). Na jakoukoli jinou metodu, kterou cesta nemá, server odpoví 405 `method_not_allowed` s `Allow`.
    - **Cíl požadavku.** Nesmí přesáhnout 4096 znaků (414 `uri_too_long`). Požadavek, který vůbec nelze zpracovat, nebo jehož záhlaví je příliš velké či přichází příliš pomalu, dostane prázdnou odpověď 400, 431 nebo 408 a spojení se ukončí.
    - **CORS: seznam povolených původů, ve výchozím nastavení vypnutý.** API slouží hře, která `Origin` neposílá. S prázdným `CORS_ORIGINS` (výchozí stav) se nikdy neposílá žádná hlavička `Access-Control-*`, takže žádná webová stránka nemůže odpověď přečíst. `CORS_ORIGINS` uvádí webové původy, jejichž stránky smějí volat API z prohlížeče, každý přesně tak, jak ho prohlížeče posílají v `Origin`: `https://host` nebo `https://host:port`, malými písmeny, bez cesty, koncového lomítka a výchozího portu; `http://` jen pro `localhost`, `127.0.0.1` a `[::1]` (lokální vývoj); žádné zástupné znaky. Oficiální server pro svůj web nastavuje `CORS_ORIGINS=https://scacelith.com,https://www.scacelith.com`. Pak platí:
      - Každá odpověď pro cestu pod `/api/v1` kromě endpointů kontroly stavu, úspěšná i chybová (včetně 429 vrstvy podle adresy), nese `Vary: Origin`. Když je `Origin` požadavku bajt po bajtu shodný s jedním z uvedených původů, nese také `Access-Control-Allow-Origin: <that origin>` a, kromě odpovědi na předběžný požadavek, `Access-Control-Expose-Headers: Retry-After, Content-Disposition`, aby stránka mohla přečíst chybové kódy, `Retry-After` a názvy souborů stahovaných PGN a GIFů.
      - Na předběžný požadavek (preflight; `OPTIONS` s `Access-Control-Request-Method`) z uvedeného původu na existující cestu server odpoví 204 bez těla, s `Allow`, `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods` (metody z `Allow`), `Access-Control-Allow-Headers: Authorization, Content-Type`, `Access-Control-Max-Age: 600` a `Vary: Origin`. Nepotřebuje žádný token a nezapočítává se do žádné kvóty účtu ani do žádného limitu endpointu; jako každý požadavek spotřebuje jeden žeton vrstvy podle adresy. Prohlížeč předběžný požadavek znovu používá po dobu 10 minut, ale jen pro přesně tutéž URL včetně řetězce dotazu. Požadavek bez `Authorization` a bez těla JSON, například `GET /info` nebo `GET /leaderboard`, předběžný požadavek nepotřebuje.
      - Jakákoli jiná odpověď na předběžný požadavek znamená jeho selhání, i když nese hlavičky CORS: 404 pro neexistující cestu nebo 429 vrstvy podle adresy. Prohlížeč pak požadavek neodešle a stránka uvidí síťovou chybu, bez odpovědi i bez jejího `Retry-After`.
      - `Access-Control-Allow-Credentials` se nikdy neposílá, stejně jako `Access-Control-Allow-Origin: *`: stránka posílá svůj token relace v `Authorization: Bearer`, stejně jako hra, a API nenastavuje žádné cookie. HTML stránky, endpointy kontroly stavu a prázdné odpovědi na nezpracovatelné požadavky nenesou žádnou hlavičku CORS. WebSocket CORS nepoužívá: stránky, které ho smějí otevřít, uvádí `WS_ALLOWED_ORIGINS`.
      - Stránka z neuvedeného původu nepřečte nic a její zápisy napříč weby (cross-site) se nikdy neodešlou: těla JSON a `Authorization` vyžadují předběžný požadavek, jehož odpověď pro tento původ nemá `Access-Control-Allow-Origin`. `Cross-Origin-Resource-Policy: same-origin` zůstává na každé odpovědi: uplatní se jen u požadavků `no-cors` (jiný web vkládá odpověď do své stránky), nikdy u požadavků `cors` uvedené stránky.

    ## Odpovědi a chyby

    - Odpovědi jsou JSON v UTF-8, kromě stahování PGN (`application/x-chess-pgn`), animovaných GIFů (`image/gif`) a HTML stránek. Časy jsou v milisekundách od 1970-01-01 UTC. ID jsou celá čísla. ID partií mají až 16 číslic, ale zůstávají pod 2^53, takže je číslo JSON (double) uchová přesně.
    - Každá odpověď API nese `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'` (HTML stránky mají vlastní politiku). S `TLS_MODE=native` nese také `Strict-Transport-Security: max-age=31536000`. Odpovědi, které naslouchací proces zapisuje sám, ještě před API (`GET` a `HEAD` endpointů kontroly stavu, 429 vrstvy podle adresy a 500 `internal_error`, když API úplně selže), nesou stejné hlavičky kromě `Cross-Origin-Resource-Policy` a jejich `Content-Type` je `application/json` bez znakové sady.
    - Odpověď musí být přečtena do 60 sekund od chvíle, kdy ji server má připravenou, což má význam jen u velkých odpovědí (GIF, export dat, dlouhé PGN). Doba, kterou server potřebuje na přípravu odpovědi, se nezapočítává do této lhůty ani do 30 sekund bez jediného přijatého či odeslaného bajtu, po kterých se spojení ukončí.

    Chyby mají jednotný tvar (schéma `Error`):

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

    `message` slouží pro logy a jako záložní text; klient vybírá, co zobrazí, podle `error`. `retryAfter` (v sekundách) se objevuje jen u odmítnutí, která časem pominou, a odpověď pak nese i hlavičku `Retry-After` se stejnou hodnotou; jedinou výjimkou je 503 `busy` při čtení historie, partií, hráčů a žebříčku, které má jen toto pole. Některé chyby přidávají další pole: `field` (neplatný vstup), `reason` (`weak_password`, `pow_required`), `pow` (`pow_required`), `until` (`banned`), `line` a `column` (`invalid_pgn`).

    Chyby, které může vrátit jakýkoli endpoint:

    | Stav | `error` | Kdy |
    |---|---|---|
    | 400 | `invalid_request` | Tělo porušuje schéma endpointu (`field` uvádí kde), cíl požadavku nebo `Content-Length` má chybný formát, parametr cesty není platným kódováním URL, nebo bylo tělo useknuto. |
    | 400 | `invalid_json` | Tělo není JSON. |
    | 401 | `unauthorized` | Chybí hlavička `Authorization` u endpointu, který vyžaduje relaci. |
    | 401 | `invalid_token` | Token má chybný formát, vypršel, byl zneplatněn nebo patří smazanému účtu; i u endpointů, kde je relace nepovinná. |
    | 404 | `not_found` | Takový endpoint neexistuje. Používají ho i některé endpointy: taková partie, hráč nebo relace neexistuje. |
    | 405 | `method_not_allowed` | Cesta existuje pro jiné metody (viz `Allow`). |
    | 408 | `request_timeout` | Tělo nedorazilo do 10 sekund. |
    | 413 | `payload_too_large` | Tělo přesahuje `HTTP_BODY_LIMIT` (u `POST /gif` 135 168 bajtů). |
    | 414 | `uri_too_long` | Cíl požadavku přesahuje 4096 znaků. |
    | 415 | `unsupported_media_type` | Tělo není `application/json` nebo jeho znaková sada není UTF-8. |
    | 429 | `rate_limited` | Limit počtu požadavků (viz níže): `retryAfter` a k tomu hlavička `Retry-After`. |
    | 500 | `internal_error` | Neočekávané selhání. Server ho zaznamená do logu. |
    | 503 | `timeout` | Server neodpověděl do 30 sekund (do 60 sekund u exportu, do 45 sekund u GIFů při výchozím nastavení). |
    | 503 | `server_busy` | Vyhledání relace nebo změna účtu narazily na zamčenou databázi (`retryAfter` 1), nebo je plná fronta hashování hesel (viz níže). Také s `retryAfter` 2 až 5, když server výrazně nestíhá: u `POST`, `PUT`, `PATCH` nebo `DELETE` čeká v databázi příliš mnoho zápisů; u jakéhokoli požadavku stále běží příliš mnoho dřívějších požadavků po vypršení jejich časového limitu, nebo se token relace nepodařilo ověřit do 5 sekund. Požadavek nebyl proveden a jeho limity počtu požadavků nic nespotřebovaly. |

    Endpointy pro čtení (historie partií, partie, hráči, žebříček) a export odpovídají 503 `busy` s `retryAfter: 1`, když databáze zůstala zamčená.

    Endpointy, které ověřují nebo hashují heslo, mohou odpovědět také jednou z těchto chyb, obě s náhodným `retryAfter` 5 až 15 sekund:

    - 503 `server_busy`: fronta hashování hesel na serveru (`PASSWORD_HASH_QUEUE_MAX`) je plná, nebo vypršela doba čekání.
    - 429 `rate_limited`: fronta je zaplněná alespoň z poloviny a tento klient (adresa IPv4 nebo IPv6 /48) v ní už má `PASSWORD_HASH_WAITERS_PER_SOURCE` čekajících hashů. Tato odpověď 429 vrací žetony limitů počtu požadavků, které požadavek spotřeboval.

    Nic se nezměnilo a žádný neúspěšný pokus se nezapočítal (kromě `POST /auth/sso/google/link`, kde se pokus tiketu a neúspěch účtu započítaly už před hashováním) a odkaz pro obnovení hesla zůstává platný.

    HTML stránky vracejí své chyby jako HTML stránky se stejnými stavovými kódy.

    ## Autentizace

    Endpointy, které vyžadují relaci, přijímají token typu bearer v hlavičce `Authorization` (schéma `bearerAuth`):

    ```
    Authorization: Bearer sct_L_8GDd7uzfQ3QQWtqrsWXDTsFWzRwIvJcwIGHhjWPS8
    ```

    - **Získání tokenu.** Token (`sct_` následované 43 znaky base64url) vydává `POST /auth/login`, po něm `POST /auth/login/mfa`, když je zapnuté dvoufázové ověření, dále `POST /auth/sso/google/finish` a `POST /auth/sso/google/link` (přihlášení přes Google) a `POST /auth/sso/complete`. Každý z nich odpoví `{ token, expiresAt, user }`. Server ukládá jen hash SHA-256 tokenu.
    - **Povinná relace.** Na chybějící hlavičku server odpoví 401 `unauthorized` s `WWW-Authenticate: Bearer realm="scacelith"`. Na neplatný token odpoví 401 `invalid_token` s `WWW-Authenticate: Bearer realm="scacelith", error="invalid_token"`. Klient by měl token, který dostane `invalid_token`, zapomenout a přihlásit se znovu.
    - **Nepovinná relace.** U `GET /games/{id}`, `GET /games/{id}/pgn`, `GET /players/{username}` a `GET /players/{username}/games` je relace nepovinná. Bez hlavičky vracejí veřejný pohled; pokud je hlavička odeslána, její token musí být platný.
    - **Zaneprázdněné úložiště.** Když databáze během ověřování tokenu zůstane zamčená, endpoint, který relaci vyžaduje nebo přijímá, odpoví 503 `server_busy` s `retryAfter: 1` (a `Retry-After`); když ověření trvá déle než 5 sekund, odpoví 503 `server_busy` s `retryAfter` 2 až 5. V obou případech token nebyl posouzen: ponechte si ho a pošlete požadavek znovu.
    - **Životnost.** Relace skončí při tom z obou termínů, který nastane dřív: `SESSION_MAX_DAYS` (90) dní po přihlášení (to je `expiresAt`), nebo `SESSION_IDLE_DAYS` (30) dní bez použití. Každé použití limit nečinnosti posune; server zapisuje novou hodnotu nejvýše jednou za 5 minut, na pozadí. Dokud má jeho databáze mnoho čekajících zápisů, odkládá tento zápis na pozdější požadavek, kromě poslední hodiny před limitem nečinnosti, takže používaná relace nevyprší. Účet má nejvýše `MAX_SESSIONS_PER_USER` (10) relací: nové přihlášení nad tento počet zneplatní nejstarší relaci.
    - **Zneplatnění.** Odhlášení (`POST /auth/logout`, `POST /auth/logout-all`, `DELETE /auth/sessions/{id}`) zneplatňuje relace; stejně tak změna hesla (ostatní relace), obnovení hesla a smazání účtu (všechny relace) a administrátor. Zneplatnění přes API platí okamžitě a WebSocket otevřený se zneplatněnou relací se zavře. Změna nebo obnovení hesla a smazání účtu zneplatňují relace ve stejné transakci jako samotnou změnu: když některá z nich selže, žádná relace nebyla zneplatněna a nic jiného se také nezměnilo. Server ukládá výsledky vyhledání relací do mezipaměti na 30 sekund, takže zneplatnění administrátorským příkazem, který běží jako samostatný proces, se projeví do 30 sekund.
    - **Rozsah.** Token patří jednomu serveru a otevírá i jeho WebSocket. Nikdy ho neposílejte na jiný server.

    ## Limity počtu požadavků a další omezení

    Limity platí pro jeden ze dvou rozsahů. **Klient:** adresa IPv4 nebo IPv6 /64 (v režimu proxy pochází adresa z `X-Forwarded-For` odeslané adresou z `TRUSTED_PROXIES`); některé limity navíc počítají každou IPv6 /48 jako celek, nad rámec každé z jejích sítí /64. **Hráč:** přihlášený účet bez ohledu na adresu; limit počítaný na hráče u endpointu, kde je relace nepovinná, se u požadavku bez tokenu počítá na klienta.

    Každý limit je kbelík žetonů (token bucket), který pojme `limit` požadavků a průběžně se doplňuje rychlostí `limit / window`; `retryAfter` je doba do dalšího žetonu. Limity označené jako *sdílené* se navíc počítají v klouzavém okně stejné délky, takže žádné okno neobsahuje o mnoho víc než `limit` požadavků. Každý limit se počítá pro celý server. Když jeden z limitů endpointu požadavek odmítne, žetony, které za tento požadavek spotřebovaly jeho ostatní limity, se vrátí. Na odmítnutí server odpoví 429 `rate_limited` s `retryAfter` a `Retry-After`.

    - **Vrstva podle adresy.** Každý požadavek (jakákoli cesta a metoda, včetně endpointů kontroly stavu a přechodů na WebSocket) nejprve spotřebuje jeden žeton z kvóty svého klienta: `HTTP_RATE_PER_IP` (600) za minutu s nárazovou rezervou (burst) na půl minuty a `HTTP_RATE_PER_PREFIX` (4 × `HTTP_RATE_PER_IP`) pro IPv6 /48. Klient může mít také nejvýše `IP_MAX_INFLIGHT` (32 × `WORKERS`) rozpracovaných požadavků (nad tento počet `retryAfter` 1). Klient, který po odmítnutích pokračuje dál, je zablokován: `ABUSE_BLOCK_REFUSALS_PER_MIN` (600) odmítnutí za minutu ho zablokuje na 1 minutu, pak na 4, 16 a 60 minut při každém dalším zablokování během 6 hodin. Odmítnutí limity `auth`, `auth_*` a `reauth` se počítá za 5; odmítnutí limity počítanými na hráče se nepočítají nikdy. Adresy v `ABUSE_EXEMPT` tuto vrstvu přeskakují, kvótu účtu a limity endpointů však ne.
    - **Kvóta účtu.** Každý požadavek s platným tokenem relace se navíc započítává svému účtu: `USER_RATE_PER_MIN` (120) za minutu pro všechny endpointy dohromady, bez ohledu na adresu, s nárazovou rezervou (burst) na půl minuty.
    - **Limity endpointů:**

    | Limit | Výchozí hodnota | Počítá se na | Endpointy |
    |---|---|---|---|
    | `auth` | `AUTH_RATE_PER_IP` (20) / 10 min, sdílený | klienta a `AUTH_RATE_PER_PREFIX` (5 × `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) / hodinu, sdílený | klienta, 3krát tolik na IPv6 /48 | `POST /auth/register` |
    | `auth_mail` | `AUTH_MAIL_PER_HOUR` (10) / hodinu, sdílený | klienta, 3krát tolik na IPv6 /48 | `POST /auth/verify-email/resend` |
    | `auth_forgot` | `AUTH_FORGOT_PER_HOUR` (3) / hodinu, sdílený | klienta, 3krát tolik na IPv6 /48 | `POST /auth/password/forgot` |
    | `auth_forgot_day` | `AUTH_FORGOT_PER_DAY` (10) / 24 hodin, sdílený | klienta, 3krát tolik na IPv6 /48 | `POST /auth/password/forgot` |
    | `auth_reset` | `AUTH_RESET_PER_HOUR` (10) / hodinu, sdílený | klienta, 3krát tolik na IPv6 /48 | `POST /auth/password/reset`, `POST /reset-password` |
    | `reauth` | hodnoty limitu `auth`, ve vlastním kbelíku, sdílený | 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, sdílený | hráče | tytéž endpointy jako `reauth` |
    | `account` | 60 / min | hráče | `GET /account/me`, `PUT /account/preferences` |
    | `account_games` | 60 / min | hráče | `GET /account/games` |
    | `account_export` | 5 / hodinu, sdílený | hráče | `POST /account/export` (kontroluje se před `reauth`; počítá se každý pokus) |
    | `sessions` | 60 / min | hráče | `POST /auth/logout`, `/auth/logout-all`, `GET /auth/sessions`, `DELETE /auth/sessions/{id}` |
    | `public_read` | 60 / min | hráče (klienta bez tokenu) | `GET /players/{username}`, `/players/{username}/games`, `/games/{id}`, `/games/{id}/pgn` (jeden kbelík pro všechny čtyři) |
    | `gif` | 30 / min | hráče | `GET /games/{id}/gif`, `POST /gif` (jeden kbelík pro oba) |
    | `gif_user_min`, `gif_user_hour` | `GIF_USER_RENDERS_PER_MIN` (4) / min a `GIF_USER_RENDERS_PER_HOUR` (30) / hodinu, sdílené | hráče | tytéž dva, jen když se GIF musí vytvořit (ne z mezipaměti) |
    | `gif_ip_min`, `gif_ip_hour` | `GIF_IP_RENDERS_PER_MIN` (12) / min a `GIF_IP_RENDERS_PER_HOUR` (120) / hodinu, sdílené | klienta (všechny jeho účty dohromady), 3krát tolik na IPv6 /48 | tytéž, za stejných podmínek jako výše |
    | `reports` | 30 / hodinu | hráče | `POST /reports` |
    | `sso_start` | 30 / 10 min, sdílený | klienta a 90 na IPv6 /48 | `POST /auth/sso/google/start`, `POST /auth/sso/google/web/start` (dohromady) |
    | `sso_finish` | 30 / min | klienta | `POST /auth/sso/google/finish` |
    | `page` | 60 / min | klienta | `GET /verify-email`, `/reset-password`, `/confirm-email-change` |

    `GET /info` a `GET /leaderboard` nemají vlastní limit: platí pro ně jen vrstva podle adresy. Endpoint s několika limity je kontroluje v pořadí uvedeném v jeho popisu.

    Server zpracovává požadavek v tomto pořadí: vrstva podle adresy, pak endpointy kontroly stavu; přiřazení endpointu; kontroly, zda server výrazně nestíhá (503 `server_busy`): příliš mnoho dřívějších požadavků stále běžících po vypršení časového limitu a u `POST`, `PUT`, `PATCH` nebo `DELETE` nevyřízené zápisy databáze; autentizace, do 5 sekund (při delší době 503 `server_busy`); kvóta účtu, pokud požadavek nese relaci; limity endpointu; tělo; samotný endpoint (endpointy GIF spotřebují své limity vykreslování, jen když mají GIF vytvořit). Požadavek odmítnutý kvůli tokenu tedy nespotřebuje žádné žetony endpointu, kdežto požadavek s neplatným tělem je spotřebuje.

    Další omezení, která uplatňují samy endpointy:

    - **Neúspěšná přihlášení na jedno přihlašovací jméno** (uživatelské jméno nebo e-mail): od `AUTH_FAILURES_PER_ACCOUNT` (5) neúspěchů musí každý pokus čekat dvakrát déle než předchozí (2 s, 4 s a tak dále, až 15 minut): 429 `too_many_attempts` s `retryAfter`. Počítadlo se vynuluje po hodině bez neúspěchů.
    - **Neúspěšné druhé faktory při přihlášení:** stejné pravidlo od 5. chybného kódu účtu. Jeden krok přihlášení přijme nejvýše 5 chybných kódů.
    - **Kódy druhého faktoru jednoho účtu:** nejvýše `AUTH_MFA_PER_ACCOUNT` (10) kódů (z ověřovací aplikace nebo záložních, správných i chybných) za 15 minut, z jakékoli adresy, při přihlášení i při opětovné autentizaci; nad tento počet 429 `too_many_attempts` ještě před kontrolou kódu, takže se záložní kód nespotřebuje.
    - **Neúspěšné opětovné autentizace účtu** (chybné heslo nebo kód): stejné pravidlo (429 `too_many_attempts`), společné pro všechny endpointy, které vyžadují opětovnou autentizaci.
    - **E-maily:** jeden potvrzovací e-mail nebo e-mail pro obnovení hesla na adresu za 5 minut (odpověď zůstává stejná); jedno upozornění „someone tried to use your address“ (někdo se pokusil použít vaši adresu) na adresu za hodinu.
    - **Hlášení:** `REPORTS_PER_DAY` (5) na hráče za 24 hodin: 429 `report_limit`.

    ## Důkaz práce

    `POST /auth/register` vyžaduje důkaz práce vždy, když je `POW_REGISTER_BITS` větší než 0 (ve výchozím nastavení 18; `GET /info` ho uvádí jako `pow.register`). `POST /auth/login` a `POST /auth/sso/google/link` (jeden druh výzvy pro oba) ho vyžadují jen po dobu 5 minut poté, co server zaznamená vlnu neúspěšných přihlášení (`POW_LOGIN_TRIGGER_PER_MIN`, pak `POW_LOGIN_BITS`); klient se to dozví z odpovědi.

    1. Na požadavek bez důkazu (nebo s odmítnutým důkazem) server odpoví 428 `pow_required` s `reason` a výzvou `pow` `{ challenge, bits, expiresAt }`.
    2. Najděte nonce: desítkový řetězec nejvýše 20 číslic takový, že `SHA-256(challenge + ":" + nonce)` začíná `bits` nulovými bity (nejvýznamnější bit prvního bajtu jako první). 18 bitů vyžaduje v průměru asi 260 000 hashů.
    3. Pošlete stejný požadavek znovu s `"pow": { "challenge": "...", "nonce": "123456" }` v těle.

    Výzva platí 2 minuty a jen jednou, pro jeden endpoint a jednu síť klienta (adresu IPv4 nebo IPv6 /64). Je podepsaná, takže si o ní server až do jejího návratu nic neuchovává. `reason` uvádí, proč byl důkaz odmítnut: `required`, `malformed`, `signature`, `endpoint`, `network`, `expired`, `bits`, `work` nebo `replayed`.

    ## Opětovná autentizace

    Změny účtu znovu vyžadují heslo a při zapnutém dvoufázovém ověření i druhý faktor:

    - jen heslo: `POST /account/password` a `POST /account/mfa/totp/setup`;
    - heslo a kód z ověřovací aplikace, přičemž záložní kód se odmítá: `POST /account/mfa/recovery-codes`;
    - heslo a kód z ověřovací aplikace nebo záložní kód: `POST /account/mfa/totp/disable`, `POST /account/email`, `POST /account/export` a `POST /account/delete`.

    V tělech obsahuje `code` šestimístný kód z ověřovací aplikace a `recoveryCode` záložní kód (`xxxx-xxxx-xx`; na velikosti písmen, mezerách a pomlčkách nezáleží). Tam, kde se záložní kódy přijímají, lze záložní kód poslat i v `code`. Každý kód funguje jen jednou: použitý kód z ověřovací aplikace se odmítá až do dalšího třicetisekundového kroku a záložní kód po použití zaniká.

    | Stav | `error` | Kdy |
    |---|---|---|
    | 403 | `invalid_password` | Chybné heslo, nebo bylo heslo během zpracování požadavku obnoveno či změněno (nic se nezměnilo). |
    | 403 | `mfa_code_required` | Dvoufázové ověření je zapnuté a nebylo odesláno ani `code`, ani `recoveryCode`. |
    | 403 | `invalid_code` | Chybný nebo už použitý kód. |
    | 400 | `password_not_set` | Účet jen s přihlášením přes Google zatím nemá heslo (nastaví ho „Zapomenuté heslo?“). |
    | 429 | `too_many_attempts` | Příliš mnoho neúspěchů nebo příliš mnoho vyzkoušených kódů u tohoto účtu. |
    | 503 / 429 | `server_busy` / `rate_limited` | Fronta hashování hesel je přetížená. |

    Chybná hesla a kódy se započítávají do počítadla neúspěchů účtu a zaznamenávají se jako bezpečnostní události.

    ## Port metrik

    Kromě portu API odpovídá server prostým HTTP na portu metrik (`METRICS_PORT`, 9464, na adrese `METRICS_BIND`, ve výchozím nastavení 127.0.0.1; nezpřístupňujte ho veřejně). Není součástí tohoto API: `GET /healthz` odpoví `ok`; `GET /readyz` odpoví `ready`, jakmile je spuštění dokončeno (každý shard přehrál svůj žurnál a naslouchací procesy jsou navázané), a to až do začátku vypínání, jinak 503 `not ready`; `GET /metrics` poskytuje metriky Prometheus a s nastaveným `METRICS_TOKEN` vyžaduje `Authorization: Bearer <token>` s přesně tímto tokenem.
  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 serveru Scacelith a jeho referenční dokumentace (API.md, PROTOCOL.md, CONFIG.md).
  url: https://github.com/DarkCenobyte/scacelith-chess-server
servers:
  - url: https://caissa.scacelith.com/api/v1
    description: Oficiální server.
  - url: https://{host}:{port}/api/v1
    description: Libovolný server Scacelith, například komunitní server.
    variables:
      host:
        default: caissa.scacelith.com
        description: Veřejný název hostitele serveru (`SERVER_PUBLIC_HOST`).
      port:
        default: "443"
        description: Veřejný port API (`PUBLIC_API_PORT`, jinak `API_PORT`).
tags:
  - name: server-info
    x-displayName: Informace o serveru
    description: Co klient potřebuje vědět, než se přihlásí nebo připojí.
  - name: health
    x-displayName: Kontrola stavu
    description: |-
      Živost (liveness) a připravenost (readiness) serveru pro monitorování. Tyto endpointy odpovídají na portu API ještě před autentizací a všemi limity endpointů, ale jako každý požadavek spotřebují žeton vrstvy podle adresy; monitorovacího hostitele lze uvést v `ABUSE_EXEMPT`. U každého endpointu fungují obě cesty: v kořeni serveru i pod `/api/v1`.

      Port metrik má vlastní endpointy kontroly stavu, popsané v úvodu.
  - name: auth
    x-displayName: Registrace a přihlášení
    description: |-
      Vytvoření účtu, přihlášení heslem (a druhým faktorem), potvrzovací e-maily a e-maily pro obnovení hesla.

      `POST /auth/login`, `POST /auth/login/mfa`, `POST /auth/sso/google/finish`, `POST /auth/sso/google/link` a `POST /auth/sso/complete` vracejí relaci `{ token, expiresAt, user }` (nebo, u prvních z nich, druhý krok).
  - name: google-sign-in
    x-displayName: Přihlášení přes Google
    description: |-
      K dispozici, když `GET /info` uvádí `sso.google: true`; jinak každý z níže uvedených endpointů odpoví 404 `sso_disabled`. Web se přihlašuje stejně, svým vlastním klientem, když je `sso.googleWeb` true (níže, **Na webu**). Hra se přihlašuje přes systémový prohlížeč tokem pro nainstalované aplikace podle RFC 8252 (klient typu „Desktop app“, aplikace pro počítač): Google pošle prohlížeč zpět na naslouchací proces hry na `127.0.0.1`, nikdy na tento server, a hra nikdy nevidí přihlašovací údaje Google.

      1. Klient naslouchá na `127.0.0.1:0` (port vybere systém) a vytvoří dvojici PKCE: `codeVerifier` o délce 43 až 128 znaků `[A-Za-z0-9._~-]` a `codeChallenge = BASE64URL(SHA-256(codeVerifier))`, který má 43 znaků a žádné doplnění (padding).
      2. `POST /auth/sso/google/start` s výzvou a portem vrátí URL Google a `state` pokusu. Klient URL zkontroluje (viz níže) a otevře ji v prohlížeči.
      3. Google pošle prohlížeč na `http://127.0.0.1:<port>/oauth2/google/<tag>?code=...&state=...`. Klient přijme jen `state` z odpovědi na start a po přesměrování s `error=` nic neposílá.
      4. `POST /auth/sso/google/finish` s ID pokusu, `codeVerifier`, `state` a `code` (a `iss`, pokud ho Google poslal).
      5. Odpovědí je relace, krok dvoufázového ověření (pokračujte přes `POST /auth/login/mfa`), `needsUsername` pro nového hráče (pokračujte přes `POST /auth/sso/complete`), nebo `needsPassword`, když adresu používá účet s heslem (pokračujte přes `POST /auth/sso/google/link`).

      **Značka původu.** URI přesměrování nese značku serveru, který hráč přidal, aby hra odmítla URL, kterou jiný server získal pro své vlastní hráče. Původ je hostitel malými písmeny (literál IPv6 v hranatých závorkách), `:` a desítkový port API, vždy vypsaný, včetně 443: server použije `SERVER_PUBLIC_HOST` a svůj veřejný port API, hra adresu, ke které je připojena. Značka je prvních 22 znaků base64url (bez doplnění) z SHA-256(UTF-8 `"scacelith-sso-origin-v1\n"` + původ). URI přesměrování je `"http://127.0.0.1:" + port + "/oauth2/google/" + tag`: vždy literál IPv4 a port naslouchacího procesu hry (1024 až 65535). Server od klienta nikdy nepřebírá URI, hostitele ani cestu.

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

      Přihlášení přes Google proto funguje jen hráčům, kteří server přidali přesně pod `SERVER_PUBLIC_HOST` s jeho veřejným portem API.

      **Co hra kontroluje, než otevře prohlížeč.** `authUrl` začíná přesně `https://accounts.google.com/o/oauth2/v2/auth?`, je to tisknutelné ASCII kratší než 4096 znaků a její dotaz obsahuje právě jeden `response_type=code`, právě jeden `redirect_uri` rovný URI, které hra spočítá ze svého portu a své značky původu, právě jeden `state` rovný `state` z odpovědi, `code_challenge_method=S256` a `code_challenge` o 43 znacích. Jinak hra zastaví svůj naslouchací proces a nic neotevře. `finish` a `link` posílá jen serveru, který odpověděl na `start`.

      **Na webu.** Web (ten oficiální: <https://scacelith.com/>) se přihlašuje klientem typu „Web application“ (webová aplikace) téhož projektu Google (`GOOGLE_WEB_CLIENT_ID`). Google pošle prohlížeč zpět na jednu stránku webu, kterou nastavil provozovatel (`GOOGLE_WEB_REDIRECT_URI`, zapsaná u klienta v seznamu „Authorized redirect URIs“, tj. autorizovaných URI přesměrování), a tato stránka pošle kód na `finish`; ani zde server od klienta nepřebírá žádné URI přesměrování.

      1. Stránka vytvoří dvojici PKCE jako výše, uchová verifikátor v kartě (`sessionStorage`) a pošle `POST /auth/sso/google/web/start` s výzvou.
      2. Zkontroluje `authUrl` stejně jako hra, jen s tím rozdílem, že její `redirect_uri` musí být stránkou vlastního původu webu, a pak na ni přejde. Nenačítá se žádný skript Google.
      3. Google pošle prohlížeč na `GOOGLE_WEB_REDIRECT_URI?code=...&state=...`. Stránka přijme jen `state` z odpovědi na start a po přesměrování s `error=` nic neposílá.
      4. `finish`, `link` a `complete` následují jako u hry. Pokus si pamatuje, že ho zahájil web: kód se vymění s klientem a stránkou webu a ID token musí uvádět tohoto klienta (kód nebo ID token klienta hry selže s 502 `sso_failed`, a naopak).

      Účet Google vede ke stejnému účtu Scacelith ze hry i z webu: propojení uchovává subjekt Google (`sub`), který identifikuje účet Google bez ohledu na klienta. Původ webu musí být v `CORS_ORIGINS` (jinak `check-config` varuje).

      **K jakému účtu přihlášení přes Google vede:** k účtu už propojenému s tímto účtem Google; jinak k aktivnímu účtu s adresou, kterou Google potvrdil (má-li heslo, `finish` odpoví `needsPassword` a propojení se uloží teprve tehdy, když projde jeho heslo a pak jeho druhý faktor, je-li zapnutý; bez hesla 409 `sso_account_exists`); jinak k novému účtu (`needsUsername`). Účet Google se nikdy nepropojí s existujícím účtem jen na základě adresy. Na adresu účtu přijde e-mail, když přihlášení přes Google vytvoří účet a když je přidáno k existujícímu účtu.
  - name: sessions
    x-displayName: Relace
    description: Přihlášená zařízení účtu a odhlašování.
  - name: account
    x-displayName: Účet
    description: Přehled účtu, jeho předvolby a heslo.
  - name: two-step-verification
    x-displayName: Dvoufázové ověření
    description: Ověřovací aplikace (TOTP, RFC 6238) se SHA-1, 6 číslicemi, třicetisekundovými kroky a tolerancí jednoho kroku oběma směry, a k tomu jednorázové záložní kódy.
  - name: email-change
    x-displayName: Změna e-mailové adresy
    description: Změna adresy účtu, potvrzená odkazem odeslaným na novou adresu.
  - name: data-export
    x-displayName: Export dat
    description: Vše, co server o účtu uchovává, jako jeden soubor JSON.
  - name: account-deletion
    x-displayName: Smazání účtu
    description: Trvalé smazání účtu.
  - name: game-history
    x-displayName: Historie partií
    description: Vlastní partie přihlášeného hráče, s filtry a stránkováním.
  - name: games
    x-displayName: Partie a PGN
    description: |-
      Záznamy partií s jejich tahy a časy na hodinách a jejich soubory PGN.

      **Kódy partií.** `status`: 1 `WhiteWins`, 2 `BlackWins`, 3 `Draw`, 4 `Aborted` (ukládají se jen skončené partie). `result`: `1-0`, `0-1`, `1/2-1/2` nebo `*` (zrušená). `reason` s jeho názvem (`termination`) a slovy, kterými končí text tahů v souboru PGN (v souboru anglicky, níže s českým překladem); kódy 7, 14 a 21 jsou remízy (hráč, kterému vypršel čas, který se vzdal nebo opustil partii, stál proti soupeři, který nemohl dát mat: FIDE 6.9 a 5.1.2; pravidlo uvádí `DESIGN.md` 6.3), a server nikdy neukončí partii kódy 4 a 13 (patří do společného seznamu důvodů):

      | Kód | `termination` | Slova v PGN |
      |---|---|---|
      | 1 | `Checkmate` | Checkmate (mat) |
      | 2 | `Resignation` | Resignation (vzdání se) |
      | 3 | `Timeout` | Loss on time (prohra na čas) |
      | 4 | `IllegalMoves` | Second illegal move (forfeit) (druhý nepřípustný tah, kontumace) |
      | 5 | `Stalemate` | Stalemate (pat) |
      | 6 | `InsufficientMaterial` | Dead position (insufficient material) (mrtvá pozice, nedostatečný materiál) |
      | 7 | `TimeoutVsInsufficient` | Flag fall, but the opponent cannot checkmate (překročení času, ale soupeř nemůže dát mat) |
      | 8 | `FivefoldRepetition` | Fivefold repetition (pětinásobné opakování pozice) |
      | 9 | `SeventyFiveMoves` | 75-move rule (pravidlo 75 tahů) |
      | 10 | `ThreefoldClaim` | Threefold repetition (claimed) (trojí opakování pozice, reklamováno) |
      | 11 | `FiftyMoveClaim` | 50-move rule (claimed) (pravidlo 50 tahů, reklamováno) |
      | 12 | `Agreement` | Draw by agreement (remíza dohodou) |
      | 13 | `IllegalMovesVsInsufficient` | Second illegal move, but the opponent cannot checkmate (druhý nepřípustný tah, ale soupeř nemůže dát mat) |
      | 14 | `ResignationVsInsufficient` | Resignation, but the opponent cannot checkmate (vzdání se, ale soupeř nemůže dát mat) |
      | 20 | `Abandonment` | Abandoned (disconnected for too long) (opuštění partie, odpojení na příliš dlouho) |
      | 21 | `AbandonmentVsInsufficient` | Abandoned, but the opponent cannot checkmate (opuštění partie, ale soupeř nemůže dát mat) |
      | 22 | `Aborted` | Game aborted (partie zrušena) |
      | 23 | `NoShow` | Aborted: first move not played in time (zrušeno: první tah nebyl zahrán včas) |
      | 24 | `Forfeit` | Forfeit (fair play violation) (kontumace, porušení fair play) |
      | 25 | `ServerAborted` | Aborted by the server (zrušeno serverem) |
      | 26 | `BothDisconnected` | Aborted: both players disconnected (zrušeno: oba hráči se odpojili) |

      Kód 14 je nový v minoritní verzi protokolu 1: klient napsaný před ní by měl neznámý kód zobrazit jako prosté ukončení partie s jejím `result`.
  - name: gifs
    x-displayName: Animované GIFy
    description: |-
      Partie jako animovaný GIF k uložení nebo ke sdílení: šachovnice viděná shora, jeden snímek na každou pozici od výchozí po konečnou, jména a ratingy hráčů nad šachovnicí, poslední tah pod ní a na posledním snímku výsledek a způsob, jakým partie skončila.

      **Náklady, mezipaměť a kvóty.** GIF se vytváří ve vlastním vykreslovacím vlákně, nikdy ve vláknech, která obsluhují partie, a s nejnižší prioritou CPU: `GIF_THREADS` vláken (ve výchozím nastavení `WORKERS`), spuštěných s prvním GIFem a zastavených po minutě bez GIFu. Až `GIF_QUEUE_MAX` (4 × `WORKERS`) GIFů čeká na vlákno, každý nejvýše `GIF_QUEUE_TIMEOUT_MS` (10 s); vykreslení smí trvat `GIF_RENDER_TIMEOUT_MS` (30 s). Server uchovává vytvořené GIFy v mezipaměti o velikosti `GIF_CACHE_MB` MB (32 × `WORKERS`), přičemž jako první odcházejí nejdéle nepoužité; GIF z mezipaměti nebo GIF, který se právě vytváří pro jiný požadavek, nestojí žádné vykreslení. Klíč mezipaměti zahrnuje vše, co mění obrázek, včetně jmen.

      Každý požadavek se započítává do `gif` (30 za minutu na hráče pro oba endpointy). GIF, který se musí vytvořit, se navíc započítává do limitů vykreslování: na hráče `GIF_USER_RENDERS_PER_MIN` (4) za minutu a `GIF_USER_RENDERS_PER_HOUR` (30) za hodinu; na klienta, všechny jeho účty dohromady, `GIF_IP_RENDERS_PER_MIN` (12) za minutu a `GIF_IP_RENDERS_PER_HOUR` (120) za hodinu, a 3krát tolik na IPv6 /48. Klient by si měl stažený soubor ponechat, místo aby o něj žádal znovu, a po 429 nebo 503 počkat `retryAfter`.

      Velikosti: `small` (pole 32 px: 284 × 350 pixelů), `medium` (48 px: 424 × 515), `large` (72 px: 628 × 762); bez souřadnic 268 × 342, 400 × 503 a 600 × 748. Výchozí pozice zůstane zobrazená alespoň 1 s (případně po dobu zpoždění, je-li delší), konečná pozice 3 s a GIF se přehrává ve smyčce; po prvním snímku se ukládá jen ta část obrázku, která se mění. Partie o 40 tazích zabere asi 135, 205 a 325 KiB (small, medium, large), partie o 150 tazích 0,5, 0,8 a 1,2 MiB.
  - name: players
    x-displayName: Hráči
    description: Veřejné profily a nedávné partie. Jen veřejné údaje, nikdy e-mailová adresa, relace, sankce ani úroveň integrity. Smazaný účet nemá profil.
  - name: leaderboard
    x-displayName: Žebříček
    description: Nejlepší hráči každé oficiální kategorie.
  - name: reports
    x-displayName: Hlášení
    description: Nahlášení soupeře z nedávné partie moderátorům.
  - name: pages
    x-displayName: HTML stránky
    description: |-
      Stránky pro prohlížeč, otevírané z odkazů v e-mailech. Jejich odkazy vedou na `https://<SERVER_PUBLIC_HOST>` (s `:<PUBLIC_API_PORT>`, pokud není 443), mimo `/api/v1`.

      - Stránky nespouštějí žádný JavaScript a nenačítají žádné externí zdroje. Jsou poskytovány s `Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'`.
      - `GET` jen zobrazí tlačítko nebo formulář, aby odkaz nespotřeboval poštovní skener, který ho otevře. Změna proběhne při `POST`: formulář odeslaný jako `application/x-www-form-urlencoded` (přijímá se i tělo JSON) s tokenem odkazu ve skrytém poli. Pole uvedené dvakrát se odmítá.
      - Chyby (limity počtu požadavků, neplatná pole) jsou také HTML stránky, s nadpisem „Request refused“ (požadavek odmítnut), nebo od 500 výš „Server error“ (chyba serveru). Jen selhání zjištěná ještě před přiřazením stránky (příliš dlouhý nebo chybně utvořený cíl požadavku, vrstva podle adresy) mají odpověď v JSON.
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 hlášení
    tags:
      - players
      - leaderboard
      - reports
  - name: browser-pages
    x-displayName: Stránky pro prohlížeč
    tags:
      - pages
paths:
  /info:
    get:
      operationId: getServerInfo
      tags:
        - server-info
      summary: Získat název, verze a porty serveru a pravidla registrace
      description: |-
        Co klient potřebuje, než se přihlásí nebo připojí: název a ID serveru, verze protokolu WebSocket a umístění WebSocketu, pravidla registrace, oficiální tempa hry a limity, které klient může zkontrolovat ještě před odesláním formuláře.

        **Limity:** jen vrstva podle adresy.
      security: []
      responses:
        '200':
          description: Popis serveru.
          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 podle adresy.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /auth/register:
    post:
      operationId: registerAccount
      tags:
        - auth
      summary: Vytvořit účet
      description: |-
        Vytvoří účet: jakmile je jeho e-mailová adresa potvrzena odkazem, který na ni byl odeslán, nebo ihned, pokud se e-mail nepotvrzuje.

        **S potvrzením e-mailu** (`REQUIRE_EMAIL_VERIFICATION`, výchozí stav): 202 `verification_sent`. Účet zatím neexistuje: registrace čeká 24 hodin a mezitím rezervuje své uživatelské jméno. Na adresu přijde odkaz platný po dobu těchto 24 hodin, nejvýše jeden na adresu za 5 minut (nový pošle `POST /auth/verify-email/resend`); účet se vytvoří, s potvrzenou adresou, při použití odkazu (tlačítkem na stránce `/verify-email`) a hráč se pak může přihlásit. Do té doby přihlášení s tímto uživatelským jménem vrací `invalid_credentials`, jako u neznámého účtu, a veřejný profil neexistuje. Nová registrace se stejnou adresou nahradí tu čekající. Odpověď je stejná, i když adresu už používá jiný účet: odkaz se pak neposílá, vlastník tohoto účtu místo toho dostane upozornění (nejvýše jedno za hodinu) a uživatelské jméno se rezervuje stejně, takže nic neprozradí, zda adresa má účet. Registrace, jejíž odkaz nebyl použit, se po 24 hodinách zahodí a její uživatelské jméno je opět volné.

        **Bez potvrzení e-mailu** (`REQUIRE_EMAIL_VERIFICATION=false`): 201 `ready`, účet se vytvoří ihned a může se přihlásit.

        **Pravidla.** `username`: `USERNAME_MIN` až `USERNAME_MAX` znaků (3 až 20), písmena, číslice, `_` a `-`, na začátku písmeno nebo číslice (`GET /info` je uvádí jako `limits`); rezervovaná jména (`admin`, `moderator`, `deleted`...) a některé předpony se odmítají; jedinečné bez ohledu na velikost písmen. `email`: oříznutý o mezery na okrajích a uložený malými písmeny, čisté ASCII s doménou obsahující tečku. `password`: alespoň `PASSWORD_MIN_LENGTH` (10) znaků a nejvýše 256 bajtů v UTF-8; nesmí obsahovat uživatelské jméno ani místní část e-mailu a nesmí být běžně používaným heslem.

        **Chyby**, kontrolované v tomto pořadí: 403 `registration_closed`; 400 `invalid_username`; 400 `invalid_email`; 400 `weak_password`; 409 `username_taken`; 428 `pow_required`; chyby fronty hashování hesel; 409 `email_taken` (jen bez potvrzení e-mailu).

        **Limity:** `auth`, pak `auth_register` (10 registrací za hodinu na klienta). **Důkaz práce:** vždy, když je `POW_REGISTER_BITS` větší než 0.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
            examples:
              first:
                summary: První pokus, bez důkazu práce
                value:
                  username: alice
                  email: alice@example.org
                  password: correct horse battery
              withPow:
                summary: Znovu odesláno s důkazem 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 vytvořen a může se přihlásit (tento server e-mail nepotvrzuje).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    const: ready
              example:
                status: ready
        '202':
          description: Registrace čeká na svůj potvrzovací odkaz (nebo adresa už má účet; odpověď to neprozradí).
          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` nebo `weak_password` s `reason`: `too_short`, `too_long`, `contains_username`, `contains_email` nebo `too_common`.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`registration_closed`: registrace je na tomto serveru uzavřená (`GET /info` uvádí `registration: closed`).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`username_taken`: uživatelské jméno má některý účet, nebo ho rezervuje čekající registrace s jinou adresou. `email_taken`: adresu používá jiný účet (jen bez potvrzení 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` nebo `auth_register`, nebo fronta hashování hesel.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (fronta hashování hesel, nebo databáze zůstala zamčená), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /auth/login:
    post:
      operationId: logIn
      tags:
        - auth
      summary: Přihlásit se heslem
      description: |-
        Přihlášení pomocí uživatelského jména nebo e-mailové adresy a hesla. Jsou možné dvě odpovědi, obě se stavem 200: relace, nebo, když je zapnuté dvoufázové ověření, druhý krok, který je třeba dokončit do 5 minut přes `POST /auth/login/mfa`.

        Neznámý účet, chybné heslo a účet bez hesla dostanou stejnou odpověď za stejnou dobu: 401 `invalid_credentials`. Kontroly účtu (`banned`, `email_unverified`) přicházejí až po správném hesle. Od `AUTH_FAILURES_PER_ACCOUNT` (5) neúspěchů na jedno přihlašovací jméno musí každý pokus čekat (429 `too_many_attempts`).

        **Limit:** `auth`. **Důkaz práce:** jen během vlny neúspěšných přihlášení (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: Relace, nebo druhý krok přihlášení s dvoufázovým ověřením.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginAnswer'
              examples:
                session:
                  summary: Přihlášení proběhlo
                  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: Dvoufázové ověření je zapnuté
                  value:
                    mfaRequired: true
                    mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                    expiresIn: 300
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_credentials`: neznámý účet, chybné heslo nebo účet bez hesla.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: 'Jen po správném hesle: `banned` s `until` (ms od epochy Unixu, `null` při trvalém pozastavení), nebo `email_unverified` (starší účet, jehož adresa ještě není potvrzená).'
        '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` (prodleva po neúspěších u tohoto přihlašovacího jména), nebo `rate_limited` (limit `auth`, fronta hashování hesel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (fronta hashování hesel, nebo databáze zůstala zamčená), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /auth/login/mfa:
    post:
      operationId: logInWithSecondFactor
      tags:
        - auth
      summary: Dokončit přihlášení druhým faktorem
      description: |-
        Druhý krok přihlášení s dvoufázovým ověřením, poté co `POST /auth/login` nebo přihlášení přes Google (`POST /auth/sso/google/finish` nebo `POST /auth/sso/google/link`) vrátí `mfaRequired`. Pošlete `code` (šestimístný kód z ověřovací aplikace, nebo záložní kód) nebo `recoveryCode`. Záložní kód použitý zde zaniká.

        Jeden krok přijme nejvýše 5 chybných kódů; krok skončí také při obnovení nebo změně hesla. Po kroku s heslem při propojení s Googlem se propojení uloží, jen když zde kód projde.

        **Limity:** `auth` a nejvýše `AUTH_MFA_PER_ACCOUNT` (10) kódů za 15 minut na účet, z jakékoli adresy; nad tento počet se kód nekontroluje, takže se záložní kód nespotřebuje.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MfaLoginRequest'
            examples:
              authenticator:
                summary: Kód z ověřovací aplikace
                value:
                  mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                  code: '123456'
              recovery:
                summary: Záložní kód
                value:
                  mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                  recoveryCode: j7v5-3ezx-zn
      responses:
        '200':
          description: Přihlášení proběhlo.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionAnswer'
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_request`: nebylo odesláno ani `code`, ani `recoveryCode`, nebo tělo porušuje schéma; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_mfa_token`: krok vypršel, byl použit nebo skončil po 5 chybných kódech, nebo bylo od prvního kroku heslo obnoveno či změněno (přihlaste se znovu). `invalid_code`: chybný nebo 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`: jen po kroku s heslem při propojení s Googlem; účet Google byl mezitím propojen s jiným účtem.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: jen po kroku s heslem při propojení s Googlem; účet se mezitím změnil, zahajte přihlášení znovu.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts`: prodleva po neúspěších u účtu, nebo jsou vyčerpány jeho kódy `AUTH_MFA_PER_ACCOUNT` za posledních 15 minut. `rate_limited`: limit `auth`.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` s `retryAfter: 1`: databáze zůstala zamčená (po kroku propojení s Googlem se nic nepropojilo: zahajte přihlášení znovu). `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /auth/logout:
    post:
      operationId: logOut
      tags:
        - sessions
      summary: Odhlásit tuto relaci
      description: |-
        Zneplatní relaci použitého tokenu. WebSocket otevřený s touto relací se zavře. Bez těla (nebo `{}`).

        **Limit:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Odhlášení proběhlo.
          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`: jiné tělo než `{}`; `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` nebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databáze zůstala zamčená), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /auth/logout-all:
    post:
      operationId: logOutEverywhere
      tags:
        - sessions
      summary: Odhlásit všechny relace
      description: |-
        Zneplatní všechny relace účtu včetně této. WebSockety otevřené s nimi se zavřou. Bez těla (nebo `{}`).

        **Limit:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Všechny relace jsou odhlášené.
          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`: jiné tělo než `{}`; `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` nebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databáze zůstala zamčená), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /auth/verify-email/resend:
    post:
      operationId: resendVerificationEmail
      tags:
        - auth
      summary: Znovu poslat potvrzovací odkaz
      description: |-
        Znovu pošle odkaz pro potvrzení e-mailu. Odpověď je 202 `accepted` bez ohledu na adresu, takže nikdy neprozradí, zda ji používá účet nebo registrace.

        Požadavek se projeví nejvýše jednou za 5 minut na adresu. Čekající registrace s touto adresou dostane znovu svých 24 hodin, ať už adresu používá jiný účet, nebo ne, takže její uživatelské jméno zůstane v obou případech rezervované stejně dlouho. Odkaz se pošle jen pro tuto registraci, pokud adresa nemá účet (nový odkaz platný 24 hodin nahradí předchozí), nebo pro aktivní nepotvrzený účet s touto adresou.

        **Limity:** `auth`, pak `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: Přijato (bez ohledu 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` nebo `auth_mail` (bez ohledu na adresu).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` s `retryAfter: 1`: databáze zůstala zamčená při vyhledávání účtu nebo při ukládání odkazu nepotvrzeného účtu (prodloužení čekající registrace se provádí jen podle možností (best effort) a nikdy nezpůsobí selhání požadavku); pětiminutový interval adresy přesto začne běžet, takže nový požadavek během něj nic neudělá. `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /auth/password/forgot:
    post:
      operationId: requestPasswordReset
      tags:
        - auth
      summary: Poslat odkaz pro obnovení hesla
      description: |-
        Pošle odkaz pro obnovení hesla, platný jednu hodinu, který otevře stránku `/reset-password`. Odpověď je 202 `accepted` bez ohledu na adresu. Odkaz se pošle jen aktivnímu účtu, nejvýše jednou za 5 minut na adresu. Účet jen s přihlášením přes Google si takto nastaví své první heslo.

        Obnovení hesla má nejpřísnější limity celého API, všechny počítané pro celý server: 3 požadavky za hodinu (`AUTH_FORGOT_PER_HOUR`) a 10 za 24 hodin (`AUTH_FORGOT_PER_DAY`) na klienta (adresu IPv4 nebo IPv6 /64), 3krát tolik na IPv6 /48, navíc k limitu `auth` (20 za 10 minut) a k jednomu e-mailu na adresu za 5 minut. Ani 202, ani 429 neprozradí, zda adresu používá nějaký účet. Nastavení nového hesla má vlastní limit (`auth_reset`).

        **Limity:** `auth`, `auth_forgot`, pak `auth_forgot_day`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmailRequest'
            example:
              email: alice@example.org
      responses:
        '202':
          description: Přijato (bez ohledu 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` nebo `auth_forgot_day` (bez ohledu na adresu).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` s `retryAfter: 1`: databáze zůstala zamčená při vyhledávání účtu nebo při ukládání odkazu; pětiminutový interval adresy přesto začne běžet, takže nový požadavek během něj žádný odkaz nepošle. `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /auth/password/reset:
    post:
      operationId: resetPassword
      tags:
        - auth
      summary: Nastavit nové heslo tokenem z odkazu pro obnovení hesla
      description: |-
        Nastaví nové heslo pomocí tokenu z odkazu pro obnovení hesla (totéž dělá stránka `/reset-password`). Pro nové heslo platí pravidla registrace.

        Obnovení má tyto účinky, v jedné transakci (buď všechny, nebo žádný): zneplatní všechny relace a zruší čekající změnu e-mailu; ostatní odkazy pro obnovení hesla k účtu přestanou fungovat; adresa se považuje za potvrzenou (odkaz to prokázal); vlastník dostane e-mail. Dvoufázového ověření se obnovení nedotkne. Po chybě fronty hashování hesel, po 503 nebo po 500 se nic nezměnilo: staré heslo i relace zůstávají a odkaz zůstává platný.

        **Limity:** `auth`, pak `auth_reset` (10 za hodinu na klienta, 30 na IPv6 /48, sdílený se 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 změněno a všechny relace jsou odhlášené.
          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ý nebo vypršel, nebo byl poslán na adresu, kterou úč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` nebo `auth_reset`, nebo fronta hashování hesel.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: fronta hashování hesel, nebo databáze zůstala zamčená (`retryAfter: 1`, nic se nezměnilo). `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /auth/sso/google/start:
    post:
      operationId: startGoogleSignIn
      tags:
        - google-sign-in
      summary: Zahájit přihlášení přes Google
      description: |-
        Zahájí pokus o přihlášení přes Google pro výzvu PKCE a port naslouchacího procesu hry na `127.0.0.1`. Odpověď obsahuje URL Google, kterou je třeba otevřít v systémovém prohlížeči, a `state` pokusu; pokus platí 10 minut.

        `authUrl` obsahuje `client_id`, `redirect_uri` (sestavené z `redirectPort` a značky původu serveru), `response_type=code`, `scope=openid email profile`, `state`, `nonce`, `code_challenge` (S256 vlastního verifikátoru serveru pro Google) s `code_challenge_method=S256` a `prompt=select_account`. Hra ji před otevřením zkontroluje (viz popis sekce).

        **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`: tento server přihlášení přes Google nenabízí.'
        '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áze zůstala zamčená), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /auth/sso/google/web/start:
    post:
      operationId: startGoogleWebSignIn
      tags:
        - google-sign-in
      summary: Zahájit přihlášení přes Google na webu
      description: |-
        Zahájí pokus webu o přihlášení přes Google pro výzvu PKCE jeho stránky. Odpověď je stejná jako pro hru: URL Google, na kterou se má přejít, a `state` pokusu; pokus platí 10 minut. K dispozici, když `GET /info` uvádí `sso.googleWeb: true`.

        `authUrl` obsahuje `client_id` webu a `redirect_uri=GOOGLE_WEB_REDIRECT_URI` se stejnými ostatními parametry jako pro hru. Stránka ji před přechodem na ni zkontroluje (viz **Na webu** v popisu sekce).

        **Limit:** `sso_start`, sdílený se zahájením přihlášení ze hry.
      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`: chybějící nebo chybně utvořené `codeChallenge`, nebo jakékoli jiné pole (včetně `redirectPort`).'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: tento server přihlášení přes Google nenabízí, nebo ho nenabízí webům.'
        '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` (sdílený se zahájením přihlášení ze hry).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databáze zůstala zamčená), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /auth/sso/google/finish:
    post:
      operationId: finishGoogleSignIn
      tags:
        - google-sign-in
      summary: Předat odpověď Googlu serveru
      description: |-
        Předá serveru `code` a `state`, které Google poslal naslouchacímu procesu hry nebo stránce webu, spolu s ID pokusu a verifikátorem PKCE. ID pokusu je bez verifikátoru k ničemu a kód je k ničemu bez vlastního verifikátoru PKCE serveru a tajného klíče klienta.

        Server zkontroluje pokus (neznámý, použitý nebo vypršelý: 410), pak verifikátor (chybný ponechá pokus použitelný), pak pokus jednou použije, zkontroluje `state` a `iss`, vymění kód s Googlem a ověří ID token. Odpověď (200) je jedna z těchto:

        - `{ token, expiresAt, user }`: přihlášení k propojenému účtu proběhlo;
        - `{ mfaRequired, mfaToken, expiresIn }`: pokračujte přes `POST /auth/login/mfa`;
        - `{ needsUsername, ssoTicket, suggestedUsername }`: nový účet; pokračujte přes `POST /auth/sso/complete` do 10 minut. `suggestedUsername` pochází ze jména u Googlu nebo z adresy a je `""`, když nic nevyhovuje;
        - `{ needsPassword, linkTicket, username, expiresIn }`: účet s touto adresou má heslo; pokračujte přes `POST /auth/sso/google/link`. Zatím není nic propojeno.

        **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: Relace, druhý krok, nebo další krok prvního přihlášení přes Google.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SsoFinishAnswer'
              examples:
                session:
                  summary: Přihlášení k propojenému účtu proběhlo
                  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: Dvoufázové ověření 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žívá účet s heslem
                  value:
                    needsPassword: true
                    linkTicket: sso_ZfEI6iZjePTHbnbevupaD9O7GjBCzcL-GJu2XGOdFGc
                    username: alice
                    expiresIn: 600
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_verifier`: verifikátor neodpovídá výzvě pokusu (pokus zůstává použitelný). `sso_email_unverified`: Google adresu nepotvrdil. `registration_closed`. `account_disabled`. Jen u propojeného účtu: `banned` (s `until`), `email_unverified`.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: tento server přihlášení přes Google nenabízí.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_account_exists`: adresu používá aktivní účet bez hesla (není uvedeno, který).'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: neznámý, použitý nebo vypršelý pokus.'
        '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áze zůstala zamčená), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /auth/sso/google/link:
    post:
      operationId: linkGoogleAccount
      tags:
        - google-sign-in
      summary: Propojit Google s existujícím účtem pomocí jeho hesla
      description: |-
        Poté, co `finish` odpověděl `needsPassword`: propojí Google s existujícím účtem pomocí hesla tohoto účtu, zadaného ve hře. Odpovědí (200) je relace (propojení je uloženo a hráč přihlášen), nebo, když je zapnuté dvoufázové ověření, `{ mfaRequired, mfaToken, expiresIn }`: pokračujte přes `POST /auth/login/mfa` a propojení se uloží, jen když tam kód projde.

        Chybné heslo ponechá tiket použitelný, celkem pro 5 pokusů. Pokus se započítá těsně před kontrolou hesla: 429 `too_many_attempts` ani 428 `pow_required` žádný nespotřebují, zatímco odmítnutí frontou hashování hesel (503 `server_busy`, 429 `rate_limited`) už jeden spotřebovalo a počítá se jako neúspěch účtu. Prodleva po neúspěších používá stejné počítadlo jako `POST /auth/login`.

        **Limit:** `auth` (včetně počítání na IPv6 /48). **Důkaz práce:** jako u `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: Relace, nebo druhý krok přihlášení.
          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ávné heslo; tiket zůstává platný, celkem na 5 pokusů.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`banned` (s `until`), jen po zadání správného hesla.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: tento server přihlášení přes Google nenabízí.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_already_linked`: účet Google byl mezitím propojen s jiným účtem.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: neznámý, použitý nebo vypršelý tiket, páté nesprávné heslo, účet, jehož stav nebo adresa se od `finish` změnily, nebo účet, jehož stav, adresa, heslo nebo dvoufázové ověření se změnily během ukládání propojení. Zahajte přihlášení znovu.'
        '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` (prodleva po neúspěších u účtu), nebo `rate_limited` (limit `auth`, fronta hashování hesel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: fronta hashování hesel, nebo databáze zůstala zamčená (`retryAfter: 1`, nic nebylo propojeno). `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /auth/sso/complete:
    post:
      operationId: completeGoogleSignUp
      tags:
        - google-sign-in
      summary: Vytvořit účet při prvním přihlášení přes Google
      description: |-
        Poté, co `finish` odpověděl `needsUsername`: vytvoří účet se zvoleným uživatelským jménem (platí pravidla registrace) a přihlásí ho. Účet nemá heslo: `hasPassword` je `false` a heslo mu nastaví „Zapomenuté heslo?“ (`POST /auth/password/forgot`).

        **Limit:** `auth` (včetně jeho počítání pro 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 vytvořen a přihlášen.
          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 přihlášení přes Google nenabízí.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`username_taken` (uživatelské jméno má některý účet, nebo ho drží čekající registrace jiné adresy), `sso_already_linked`, `email_taken`.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: neznámý, použitý nebo vypršelý tiket.'
        '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áze zůstala zamčená), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /auth/sessions:
    get:
      operationId: listSessions
      tags:
        - sessions
      summary: Vypsat přihlášená zařízení
      description: |-
        Aktivní relace účtu (přihlášená zařízení), naposledy použité jako první. `current` označuje relaci, ze které požadavek přichází. `lastSeenAt` se aktualizuje nejvýše jednou za 5 minut (když server výrazně nestíhá, i později); `expiresAt` je absolutní konec a limit nečinnosti může relaci ukončit dříve.

        **Limit:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Aktivní relace.
          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` nebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databáze zůstala zamčená), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /auth/sessions/{id}:
    delete:
      operationId: revokeSession
      tags:
        - sessions
      summary: Odhlásit jedno zařízení
      description: |-
        Odhlásí jednu relaci účtu; odhlásit lze i tu aktuální. WebSocket otevřený s touto relací se zavře. Bez těla (nebo `{}`).

        **Limit:** `sessions`.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/SessionId'
      responses:
        '200':
          description: Relace je odhlášena.
          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`: jiné tělo než `{}`, nebo `id`, které není v platném kódování URL; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: tento účet nemá aktivní relaci 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` nebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databáze zůstala zamčená), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /account/me:
    get:
      operationId: getAccount
      tags:
        - account
      summary: Získat účet, jeho ratingy a aktivní sankce
      description: |-
        Účet tak, jak ho vidí jeho hráč: pohled na účet (také `user` v každé odpovědi na přihlášení), jeden záznam ratingu pro každou kategorii, ve které hráč odehrál partie na rating, aktivní sankce a pozastavení účtu. Úroveň integrity podle anti-cheatu se 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`, nebo `invalid_token` (také když byl účet smazán).'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: limit `account` nebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databáze zůstala zamčená), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /account/preferences:
    put:
      operationId: updatePreferences
      tags:
        - account
      summary: Přijímat nebo odmítat přímé výzvy
      description: |-
        Nastaví, zda ostatní hráči smějí tohoto hráče vyzvat podle jména. S `none` se přímé výzvy odmítají: vyzyvatel se dozví, že hráč není k dispozici. Bez opětovné autentizace.

        **Limit:** `account`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Preferences'
            example:
              acceptChallenges: none
      responses:
        '200':
          description: Nyní platné předvolby.
          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` nebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databáze zůstala zamčená), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /account/password:
    post:
      operationId: changePassword
      tags:
        - account
      summary: Změnit heslo
      description: |-
        Změní heslo. Je potřeba současné heslo, ale žádný druhý faktor, ani při zapnutém dvoufázovém ověření. Nové heslo se řídí pravidly registrace (kontroluje se až po současném hesle).

        Změna má v jedné transakci (buď všechny, nebo žádný) tyto účinky: zneplatní všechny ostatní relace (tato zůstane přihlášená), zruší čekající změnu e-mailu, odkazy pro obnovení hesla k účtu přestanou fungovat a vlastníkovi odejde e-mail.

        **Opětovná autentizace:** jen 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 změněno.
          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 jen s přihlášením přes Google), `weak_password` (s `reason`), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password`: nesprávné současné heslo, nebo obnovení či změna hesla, ke které došlo během kontroly požadavku.'
        '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úspěšné opětovné autentizace účtu), nebo `rate_limited` (limit `reauth` nebo `reauth_user`, kvóta účtu, fronta hashování hesel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: fronta hashování hesel, nebo databáze zůstala zamčená (`retryAfter: 1`, nic se nezměnilo). `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /account/mfa/totp/setup:
    post:
      operationId: startTotpSetup
      tags:
        - two-step-verification
      summary: Začít zapínat dvoufázové ověření
      description: |-
        Uloží nový čekající tajný klíč pro ověřovací aplikaci, který nahradí případný dřívější čekající klíč, a vrátí ho pro tuto aplikaci (jako text a jako URI `otpauth://` k zobrazení v podobě QR kódu). Dvoufázové ověření ještě není zapnuté: zapne ho `POST /account/mfa/totp/enable` s kódem z tohoto tajného klíče.

        **Opětovná autentizace:** jen 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: Čekající tajný klíč.
          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 jen s přihlášením přes 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 se před heslem).'
        '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úspěšné opětovné autentizace účtu), nebo `rate_limited` (limit `reauth` nebo `reauth_user`, kvóta účtu, fronta hashování hesel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (fronta hashování hesel, nebo databáze zůstala zamčená), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /account/mfa/totp/enable:
    post:
      operationId: enableTotp
      tags:
        - two-step-verification
      summary: Dokončit zapnutí dvoufázového ověření a získat záložní kódy
      description: |-
        Zapne dvoufázové ověření kódem z čekajícího tajného klíče (přesně 6 číslic). Heslo se tu nepožaduje; bylo zadáno při nastavení. Odpověď obsahuje 10 záložních kódů, které se zobrazí jen tentokrát.

        **Limity:** `reauth`, potom `reauth_user`; nesprávný kód se počítá mezi neúspěšné opětovné autentizace účtu.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TotpEnableRequest'
            example:
              code: '123456'
      responses:
        '200':
          description: Dvoufázové ověření 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á přesně 6 číslic, nebo tělo porušuje schéma; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_code`: nesprávný kód (zkontrolujte čas na zařízení).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`mfa_already_enabled`; `mfa_setup_required`: žádný čekající tajný klíč (nejprve zavolejte `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úspěšné opětovné autentizace účtu), nebo `rate_limited` (limit `reauth` nebo `reauth_user`, kvóta účtu).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (databáze zůstala zamčená), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /account/mfa/totp/disable:
    post:
      operationId: disableTotp
      tags:
        - two-step-verification
      summary: Vypnout dvoufázové ověření
      description: |-
        Vypne dvoufázové ověření. Tajný klíč i záložní kódy se smažou a vlastník dostane e-mail.

        **Opětovná autentizace:** heslo a kód z ověřovací aplikace nebo záložní kód (je povinné jedno z `code` nebo `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: Dvoufázové ověření 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 jen s přihlášením přes Google), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`mfa_code_required` (ani `code`, ani `recoveryCode`; kontroluje se před heslem), `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úspěšné opětovné autentizace, nebo příliš mnoho zkoušených kódů na tomto účtu), nebo `rate_limited` (limit `reauth` nebo `reauth_user`, kvóta účtu, fronta hashování hesel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (fronta hashování hesel, nebo databáze zůstala zamčená), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /account/mfa/recovery-codes:
    post:
      operationId: regenerateRecoveryCodes
      tags:
        - two-step-verification
      summary: Nahradit záložní kódy
      description: |-
        Nahradí záložní kódy 10 novými; ty staré přestanou fungovat.

        **Opětovná autentizace:** heslo a kód z ověřovací aplikace v `code` (záložní kód se odmítne 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é záložní kódy, zobrazené jen tentokrát.
          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 jen s přihlášením přes Google), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password`, `mfa_code_required`, `invalid_code` (také pro záložní kód).'
        '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úspěšné opětovné autentizace, nebo příliš mnoho zkoušených kódů na tomto účtu), nebo `rate_limited` (limit `reauth` nebo `reauth_user`, kvóta účtu, fronta hashování hesel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (fronta hashování hesel, nebo databáze zůstala zamčená), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /account/email:
    post:
      operationId: changeEmail
      tags:
        - email-change
      summary: Změnit e-mailovou adresu
      description: |-
        Změní e-mailovou adresu účtu. `newEmail` se ořízne o mezery na začátku a na konci, převede na malá písmena a pak se kontroluje stejně jako adresa při registraci.

        **S potvrzením e-mailem** (výchozí): 202 `verification_sent`. Na novou adresu odejde odkaz platný 24 hodin; otevírá `/confirm-email-change` a adresa se změní, teprve až hráč stiskne tlačítko na této stránce. Do té doby `GET /account/me` ukazuje novou adresu jako `pendingEmail`. Nový požadavek nahradí ten čekající; změna nebo obnovení hesla ho zruší. Na danou novou adresu odejde nejvýše jeden odkaz za 5 minut, ať o něj žádá kdokoli (požadavek na změnu, která už čeká, ponechá dříve odeslaný odkaz, který zůstává platný); odpověď je stejná. Současná adresa dostane upozornění, že byla vyžádána změna na maskovanou adresu (`a***@example.org`). Odpověď i `pendingEmail` jsou stejné, když novou adresu už používá jiný účet: odkaz se pak neodešle, takže se tato změna nikdy nedokončí, a vlastník té adresy místo toho dostane upozornění (nejvýše jedno za hodinu).

        Po potvrzení odkazu se adresa změní a platí za potvrzenou, zařízení zůstanou přihlášená, dříve odeslané odkazy (potvrzení, obnovení hesla, jiné změny) přestanou fungovat a na dosavadní adresu přijde zpráva s maskovanou novou adresou.

        **Bez potvrzení e-mailem** (`REQUIRE_EMAIL_VERIFICATION=false`): adresa se změní hned (200 `email_changed`) a na dosavadní adresu přijde zpráva. Pokud adresu používá jiný účet, odpověď je 409 `email_taken` a vlastník té adresy dostane upozornění.

        **Opětovná autentizace:** heslo a při dvoufázovém ověření kód z ověřovací aplikace nebo záložní kód. `invalid_email` a `same_email` se kontrolují před heslem, takže se nepočítají jako neúspěšný pokus. **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 se změnila hned (na tomto serveru bez potvrzení e-mailem).
          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, tak jak je uložená.
              example:
                status: email_changed
                email: alice.new@example.org
        '202':
          description: Na novou adresu byl odeslán potvrzovací odkaz (nebo adresa patří jinému účtu; odpověď to neprozradí).
          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` (současná adresa účtu), `password_not_set` (účet jen s přihlášením přes Google), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password` (také když požadavek předběhla změna nebo obnovení hesla: žádný odkaz se neodešle), `mfa_code_required`, `invalid_code`.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`email_taken`: jen bez potvrzení e-mailem.'
        '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úspěšné opětovné autentizace, nebo příliš mnoho zkoušených kódů na tomto účtu), nebo `rate_limited` (limit `reauth` nebo `reauth_user`, kvóta účtu, fronta hashování hesel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: fronta hashování hesel, nebo databáze zůstala zamčená (`retryAfter: 1`: nic se nezměnilo a stejný požadavek lze poslat znovu). `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /account/export:
    post:
      operationId: exportAccountData
      tags:
        - data-export
      summary: Stáhnout data účtu
      description: |-
        Vše, co server o účtu uchovává, jako jeden soubor JSON k uložení (`format` `scacelith-account-export`, `version` 1). Export zaznamená bezpečnostní událost (`account_exported`). Pole `notes` v dokumentu hráči jednoduchou angličtinou sdělí, co je vynecháno.

        **V exportu nikdy není:** hash hesla, tajný klíč dvoufázového ověření a záložní kódy; žádný token relace ani odkazu, ani jeho hash; data anti-cheatu (úroveň a skóre integrity, anomálie, analýza partií, váha hlášení); hlášení, která na hráče podali jiní hráči; totožnost moderátorů; soukromá data jiných hráčů (soupeři jsou uvedeni pod svým veřejným jménem a ratingem, nic neprozrazuje, zda byl jiný hráč sankcionován, a není zahrnuta žádná IP adresa, která by mohla patřit jiné osobě).

        **Opětovná autentizace:** heslo a při dvoufázovém ověření kód z ověřovací aplikace nebo záložní kód. **Limity:** `account_export` (5 za hodinu na hráče, pro celý server; počítá se každý pokus včetně neúspěšných; kontroluje se jako první), potom `reauth` a `reauth_user`. **Časový limit:** 60 sekund.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReauthCredentials'
            example:
              password: correct horse battery
              code: '123456'
      responses:
        '200':
          description: Dokument exportu jako příloha.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            Content-Disposition:
              description: '`attachment; filename="scacelith-account-<username>.json"`. Znaky uživatelského jména jiné než písmena, číslice, `_`, `.` a `-` se nahradí `_`.'
              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 jen s přihlášením přes 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úspěšné opětovné autentizace, nebo příliš mnoho zkoušených kódů na tomto účtu), nebo `rate_limited` (limit `account_export`, `reauth` nebo `reauth_user`, kvóta účtu, fronta hashování hesel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (databáze zůstala zamčená, s hlavičkou `Retry-After: 1`), `server_busy` (fronta hashování hesel), `timeout` (60 sekund). Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /account/delete:
    post:
      operationId: deleteAccount
      tags:
        - account-deletion
      summary: Smazat účet
      description: |-
        Smaže účet; to nelze vrátit zpět.

        - Všechny relace se okamžitě zneplatní: token od té chvíle dostává 401 `invalid_token`.
        - Uživatelské jméno se změní na `deleted#<id>`, v účtu i ve všech záznamech partií.
        - Vymažou se: e-mailová adresa, hash hesla, tajný klíč dvoufázového ověření a záložní kódy, relace a tokeny odkazů, propojení s Googlem, záznam integrity z anti-cheatu a IP adresy uložené u bezpečnostních událostí.
        - Ratingy a partie zůstávají. Partie zůstanou čitelné pod anonymním jménem a `GET /players/{username}` pro dřívější jméno odpoví 404.

        **Opětovná autentizace:** heslo a při dvoufázovém ověření kód z ověřovací aplikace nebo záložní kód. **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 smazán.
          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 jen s přihlášením přes 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úspěšné opětovné autentizace, nebo příliš mnoho zkoušených kódů na tomto účtu), nebo `rate_limited` (limit `reauth` nebo `reauth_user`, kvóta účtu, fronta hashování hesel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (fronta hashování hesel, nebo databáze zůstala zamčená), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /account/games:
    get:
      operationId: listAccountGames
      tags:
        - game-history
      summary: Vypsat partie hráče s filtrem a stránkováním
      description: |-
        Partie přihlášeného hráče od nejnovějších, filtrované a stránkované, s počtem partií odpovídajících filtru. Všechny parametry dotazu jsou nepovinné a prázdná hodnota se počítá jako chybějící.

        Stránkování: předejte `next` z předchozí stránky jako `before`; na poslední stránce je `next` rovno `null`. `total` počítá partie odpovídající filtru napříč všemi stránkami.

        **Limit:** `account_games`.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/BeforeQuery'
        - $ref: '#/components/parameters/LimitQuery'
        - name: category
          in: query
          required: false
          description: ID oficiální kategorie (`3+2` nebo `3%2B2`), nebo `custom` pro všechny partie s jiným tempem hry.
          schema:
            type: string
            pattern: '^\s*([0-9]+[+ ][0-9]+|custom)\s*$'
          example: '3+2'
        - name: rated
          in: query
          required: false
          description: Jen partie na rating (`true`), nebo bez ratingu (`false`).
          schema:
            type: string
            enum:
              - 'true'
              - 'false'
        - name: result
          in: query
          required: false
          description: Jen partie vyhrané, prohrané nebo remizované, z pohledu hráče. Zrušené partie se zobrazí jen bez tohoto filtru.
          schema:
            type: string
            enum:
              - win
              - loss
              - draw
      responses:
        '200':
          description: Jedna stránka historie.
          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` není ID partie), `invalid_limit`, nebo `invalid_filter` (`category`, `rated` nebo `result`), vždy s `field`, které uvádí parametr.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: limit `account_games` nebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (databáze zůstala zamčená; `retryAfter: 1` v těle, bez hlavičky `Retry-After`), `server_busy` (vyhledání relace narazilo na zamčenou databázi), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /games/{id}:
    get:
      operationId: getGame
      tags:
        - games
      summary: Získat záznam partie s tahy a stavem hodin
      description: |-
        Jeden záznam partie s tahy a stavem hodin. Bez tokenu, nebo s tokenem hráče, který partii nehrál, je odpověď veřejná. Pokud hráč tokenu partii hrál, odpověď navíc obsahuje `you` a `reportable`.

        **Limit:** `public_read` (s tokenem na hráče, bez něj na klienta).
      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`: není kladné celé číslo o nejvýše 16 číslicích (menší než 2^53); `invalid_request`: neplatné kódování URL.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: byl poslán token a není platný.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: taková partie neexistuje.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: limit `public_read` nebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (databáze zůstala zamčená; `retryAfter: 1` v těle, bez hlavičky `Retry-After`), `server_busy` (vyhledání relace narazilo na zamčenou databázi), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /games/{id}/pgn:
    get:
      operationId: getGamePgn
      tags:
        - games
      summary: Stáhnout partii jako soubor PGN
      description: |-
        Tatáž partie jako soubor PGN: jedna partie s konci řádků `\n` a textem tahů v řádcích kratších než 80 sloupců. Odpověď je stejná s tokenem i bez něj.

        Značky v tomto pořadí: `Event` (`<SERVER_NAME> rated <category>` nebo `<SERVER_NAME> casual <category>`), `Site` (`SERVER_PUBLIC_HOST`), `Date` (datum začátku v UTC), `Round`, `White`, `Black`, `Result` (`*` u zrušené partie), `UTCDate` a `UTCTime` (začátek), `WhiteElo` a `BlackElo` (ratingy na začátku, nebo `-`), `WhiteRatingDiff` a `BlackRatingDiff` (změny, například `+10` a `-10`, v každé partii na rating, `+0`, když pravidla ratingu nechají rating beze změny; partie bez ratingu, s vlastním tempem nebo zrušená nemá ani jednu z nich), `TimeControl` (v sekundách), `Termination` (standardní hodnota PGN: `normal`; `time forfeit`, překročení času, i když partie skončí remízou; `abandoned`; `rules infraction`, druhý nepřípustný tah nebo kontumace kvůli fair play; `unterminated`, zrušená partie), `PlyCount`, `ScacelithGameId` (desítkové ID).

        Každý tah nese `{[%clk h:mm:ss.f] [%emt h:mm:ss.f]}`: čas na hodinách táhnoucího hráče po tahu a čas, který se mu za tah započítal, v desetinách sekundy (oříznuto). Hodnota chybí, když ji záznam nemá. Po posledním tahu následuje slovy důvod ukončení, potom výsledek.

        **Limit:** `public_read` (s tokenem na hráče, bez něj na klienta).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
      responses:
        '200':
          description: Soubor 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`: není kladné celé číslo o nejvýše 16 číslicích (menší než 2^53); `invalid_request`: neplatné kódování URL.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: byl poslán token a není platný.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: taková partie neexistuje.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: limit `public_read` nebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`internal_error`: mimo jiné tehdy, když uložené tahy nelze přehrát až k uloženému konci (server to zaznamená do logu).'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (databáze zůstala zamčená; `retryAfter: 1` v těle, bez hlavičky `Retry-After`), `server_busy` (vyhledání relace narazilo na zamčenou databázi), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /games/{id}/gif:
    get:
      operationId: getGameGif
      tags:
        - gifs
      summary: Stáhnout partii z tohoto serveru jako animovaný GIF
      description: |-
        Partie jako animovaný GIF. Jména a ratingy jsou ze záznamu partie (ratingy na začátku, smazaný účet jako `deleted#<id>`), stejně jako výsledek a způsob ukončení (`Resignation`, `Loss on time`…).

        Kontroly probíhají v tomto pořadí: `gif_disabled`, volby obrázku (`size`, `orientation`, `delay`, `coords`), ID partie, partie, její délka.

        **Vyžaduje se relace:** kvóty se počítají na účet. **Limity:** `gif` u každého požadavku; `gif_user_min`, `gif_user_hour`, `gif_ip_min` a `gif_ip_hour` jen tehdy, když se GIF musí vytvořit (viz popis sekce). **Časový limit:** `GIF_QUEUE_TIMEOUT_MS` + `GIF_RENDER_TIMEOUT_MS` + 5 sekund (ve výchozím nastavení 45 sekund).
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
        - name: size
          in: query
          required: false
          description: 'Velikost obrázku: `small` (pole 32 px), `medium` (48 px) nebo `large` (72 px).'
          schema:
            type: string
            enum:
              - small
              - medium
              - large
            default: medium
        - name: orientation
          in: query
          required: false
          description: Strana dole na šachovnici.
          schema:
            type: string
            enum:
              - white
              - black
            default: white
        - name: delay
          in: query
          required: false
          description: Milisekundy na tah (1 až 6 desítkových číslic).
          schema:
            type: integer
            minimum: 100
            maximum: 3000
            default: 500
        - name: coords
          in: query
          required: false
          description: Zda se kolem šachovnice vykreslí písmena sloupců a čísla řad (`1`), nebo ne (`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` nebo `coords`): hodnota mimo povolené. `invalid_game_id`. `invalid_request`: neplatné kódování URL.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: taková partie neexistuje. `gif_disabled`: server má GIFy vypnuté (`GIF_ENABLED=false`; žeton limitu `gif` se vrátí).'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '422':
          $ref: '#/components/responses/UnprocessableContent'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: limit `gif`, limit vykreslování, nebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`render_failed`: GIF se nepodařilo vytvořit (server do logu zaznamená důvod). `internal_error`.'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` s `retryAfter` (3 až 10 sekund) a `Retry-After`: fronta vykreslování je plná, nebo GIF čekal `GIF_QUEUE_TIMEOUT_MS` (10 sekund) na volné vlákno; všechny žetony limitů, které požadavek spotřeboval, se vrátí. `busy`: databáze zůstala zamčená (`retryAfter: 1` jen v těle). `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /gif:
    post:
      operationId: renderPgnGif
      tags:
        - gifs
      summary: Vytvořit animovaný GIF libovolné partie poslané jako PGN
      description: |-
        Stejný obrázek jako `GET /games/{id}/gif` pro libovolnou partii poslanou jako text PGN: partii uloženou hrou, export z jiného webu, partii napsanou ručně. Použije se jen první partie z textu.

        - Čtečka PGN přijímá to, co přijímá čtečka samotné hry: každé PGN, které tento server zapíše, i běžné exporty jiných webů (komentáře, varianty, NAG a anotace hodin se přeskakují; čísla tahů a SAN se čtou benevolentně; značka `FEN` určuje výchozí pozici, pokud `SetUp` není `"0"`).
        - Jména a ratingy pocházejí ze značek `White`, `Black`, `WhiteElo` a `BlackElo`. Písmena s diakritikou o ni přijdou, ostatní znaky mimo tisknutelné ASCII se změní na `?` a dlouhá jména se zkrátí (48 znaků). Výsledek pochází ze značky `Result`, jinak z konce textu tahů. Značka `Termination` se zobrazí, pokud není `normal`: o konci partie pak vypovídá koncová pozice (mat, pat).
        - Tento endpoint si tělo kontroluje sám: na neznámé pole nebo chybějící či neřetězcové `pgn` odpoví 400 `invalid_request` s `field`; na chybnou volbu 400 `invalid_option` (`delayMs` musí být číslo JSON, `coords` boolean; `null` se odmítá).

        **Vyžaduje se relace:** kvóty se počítají na účet. **Limity:** jako u `GET /games/{id}/gif`. **Limit velikosti těla:** 135 168 bajtů bez ohledu na `HTTP_BODY_LIMIT` (PGN jako řetězec JSON včetně escape sekvencí). **Časový limit:** ve výchozím nastavení 45 sekund.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GifRequest'
            examples:
              short:
                summary: Krátká ručně zadaná partie, bez souřadnic
                value:
                  pgn: 1. f3 e5 2. g4 Qh4# 0-1
                  coords: false
              options:
                summary: Soubor PGN se všemi volbami
                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ámé pole, nebo `pgn` chybí či není řetězec; bez `field`, když tělo není objekt); `invalid_json`; `invalid_option` s `field` (`size`, `orientation`, `delayMs` nebo `coords`); `invalid_pgn` s `line` a `column` (od 1, sloupce ve znacích) a `message` čtečky: nepřípustný nebo nejednoznačný tah, poškozená značka, neznámá šachová varianta, více než 65 536 bajtů.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`gif_disabled`: server má GIFy vypnuté (`GIF_ENABLED=false`; žeton limitu `gif` se vrátí).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
          description: '`payload_too_large`: tělo větší než 135 168 bajtů.'
        '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 vykreslování, nebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`render_failed`: GIF se nepodařilo vytvořit (server do logu zaznamená důvod). `internal_error`.'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` s `retryAfter` (3 až 10 sekund) a `Retry-After`: fronta vykreslování je plná, nebo GIF čekal na volné vlákno příliš dlouho; všechny žetony limitů, které požadavek spotřeboval, se vrátí. `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /players/{username}:
    get:
      operationId: getPlayer
      tags:
        - players
      summary: Získat veřejný profil hráče
      description: |-
        Veřejný profil hráče: ratingy v oficiálních kategoriích (v pořadí serveru) a počty partií. `games.total` počítá všechny uložené partie včetně partií bez ratingu a zrušených; `games.rated`, `wins`, `draws` a `losses` se sčítají přes záznamy ratingu, takže zahrnují jen partie na rating. Odpověď je stejná s tokenem i bez něj.

        **Limit:** `public_read` (s tokenem na hráče, bez něj na klienta).
      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`: nejde o 2 až 24 znaků z `[A-Za-z0-9_.-]`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: byl poslán token a není platný.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: takový hráč neexistuje, nebo jde o smazaný účet.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: limit `public_read` nebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (databáze zůstala zamčená; `retryAfter: 1` v těle, bez hlavičky `Retry-After`), `server_busy` (vyhledání relace narazilo na zamčenou databázi), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /players/{username}/games:
    get:
      operationId: listPlayerGames
      tags:
        - players
      summary: Vypsat poslední partie hráče
      description: |-
        Poslední partie hráče od nejnovějších, stránkované jako historie, ale bez filtrů a bez celkového počtu. `color` je strana tohoto hráče. `next` je ID poslední partie vždy, když je stránka plná, takže další stránka může být prázdná. Hráč se vyhledá jako první: neznámý hráč znamená 404 bez ohledu na obsah dotazu.

        **Limit:** `public_read` (s tokenem na hráče, bez něj na klienta).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Username'
        - $ref: '#/components/parameters/BeforeQuery'
        - $ref: '#/components/parameters/LimitQuery'
      responses:
        '200':
          description: Jedna stránka partií hráče.
          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` není ID partie), `invalid_limit` (tyto dvě bez `field`).'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: byl poslán token a není platný.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: takový hráč neexistuje, nebo jde o smazaný účet.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: limit `public_read` nebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (databáze zůstala zamčená; `retryAfter: 1` v těle, bez hlavičky `Retry-After`), `server_busy` (vyhledání relace narazilo na zamčenou databázi), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /leaderboard:
    get:
      operationId: getLeaderboard
      tags:
        - leaderboard
      summary: Získat nejlepší hráče kategorie
      description: |-
        100 nejlepších záznamů ratingu oficiální kategorie s alespoň `minGames` (`PROVISIONAL_GAMES`) započítanými partiemi, bez smazaných účtů a potvrzených podvodníků. Server každý žebříček přepočítává nejvýše jednou za 10 sekund; kdy, uvádí `updatedAt`. Pro jednu kategorii běží vždy jen jeden výpočet: ostatní požadavky mezitím dostanou předchozí seznam, nebo na výpočet počkají, pokud ještě žádný seznam není.

        **Limity:** pouze vrstva podle adresy.
      security: []
      parameters:
        - name: category
          in: query
          required: true
          description: ID oficiální kategorie (`3+2` nebo `3%2B2`).
          schema:
            type: string
            pattern: '^\s*[0-9]+[+ ][0-9]+\s*$'
          example: '3+2'
        - name: limit
          in: query
          required: false
          description: Kolik hráčů, 1 až 100. Větší číslo o nejvýše 3 číslicích se počítá jako 100; prázdná hodnota se počítá jako chybějící.
          schema:
            type: integer
            minimum: 1
            maximum: 999
            default: 100
      responses:
        '200':
          description: Žebříč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` (chybí, nebo nejde o oficiální kategorii), `invalid_limit`.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: vrstva podle adresy.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (databáze zůstala zamčená; `retryAfter: 1` v těle, bez hlavičky `Retry-After`), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /reports:
    post:
      operationId: reportPlayer
      tags:
        - reports
      summary: Nahlásit soupeře z nedávné partie
      description: |-
        Nahlásí soupeře z jedné z vlastních partií hráče, která skončila během posledních 7 dní. Hlášení samo o sobě nikdy nemění rating, sankci ani úroveň integrity. Zvyšuje prioritu prověření, kterou vidí moderátoři, a kromě `abuse` vyžádá analýzu partie enginem.

        Hlášení stejného soupeře za stejnou partii dostane stejnou odpověď a nic nezmění; odpověď o nahlášeném účtu nikdy nic neprozradí. `GET /games/{id}` hráčům partie předem sdělí, zda by hlášení bylo přijato (`reportable`).

        Tento endpoint si tělo kontroluje sám, v pořadí `gameId`, `reported`, `category`, `comment`: při chybě odpoví 400 `invalid_request` bez `field`. Pole, která nezná, ignoruje.

        **Limity:** `reports` (na hráče), potom `REPORTS_PER_DAY` (5) hlášení na hráče za 24 hodin (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: Hlášení je přijato (nebo už bylo podáno dříve).
          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 soupeře nahlašujícího hráče v partii, která skončila během 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` hlášení za posledních 24 hodin. `rate_limited`: limit `reports` nebo kvóta účtu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (vyhledání relace narazilo na zamčenou databázi), `timeout`. Také `server_busy` s `retryAfter` 2 až 5, když server výrazně nestíhá (viz úvod): požadavek nebyl proveden.'
  /verify-email:
    servers:
      - url: https://caissa.scacelith.com
        description: Oficiální server (stránky jsou v kořeni, mimo `/api/v1`).
      - url: https://{host}:{port}
        description: Libovolný server Scacelith (stránky jsou v kořeni, mimo `/api/v1`).
        variables:
          host:
            default: caissa.scacelith.com
            description: Veřejný název hostitele serveru (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Veřejný port API (`PUBLIC_API_PORT`, jinak `API_PORT`).
    get:
      operationId: showVerifyEmailPage
      tags:
        - pages
      summary: Zobrazit stránku pro potvrzení e-mailu
      description: |-
        Stránka odkazu pro potvrzení e-mailu (registrace, nebo znovu odeslaný potvrzovací e-mail). Zobrazí jen tlačítko „Confirm my e-mail address“ (potvrdit moji e-mailovou adresu), aby odkaz nespotřeboval skener pošty, který ho otevře; tlačítko odešle formulář na `POST /verify-email`.

        **Limit:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Stránka s potvrzovacím tlačítkem.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Odkaz je neplatný nebo vypršelý (stránka HTML).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Limit `page` (stránka HTML), nebo vrstva podle adresy (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitVerifyEmailPage
      tags:
        - pages
      summary: Potvrdit e-mailovou adresu
      description: |-
        Formulář potvrzovací stránky. Potvrdí adresu; u nové registrace teprve tehdy vytvoří účet a hráč se může přihlásit.

        **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 potvrzena (a účet nové registrace vytvořen).
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Odkaz je neplatný, použitý nebo vypršelý, nebo je formulář neplatný (stránka HTML).
        '408':
          $ref: '#/components/responses/HtmlRequestTimeout'
        '409':
          $ref: '#/components/responses/HtmlConflict'
          description: Odkaz nové registrace, jejíž uživatelské jméno nebo adresu mezitím zabral jiný účet; žádný účet se nevytvoří (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), nebo vrstva podle adresy (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Databáze zůstala zamčená (`Retry-After: 1`), nebo server výrazně nestíhá (`Retry-After` 2 až 5, viz úvod): nic se nezměnilo a odkaz stále funguje. Také při překročení časového limitu obsluhy požadavku.'
  /reset-password:
    servers:
      - url: https://caissa.scacelith.com
        description: Oficiální server (stránky jsou v kořeni, mimo `/api/v1`).
      - url: https://{host}:{port}
        description: Libovolný server Scacelith (stránky jsou v kořeni, mimo `/api/v1`).
        variables:
          host:
            default: caissa.scacelith.com
            description: Veřejný název hostitele serveru (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Veřejný port API (`PUBLIC_API_PORT`, jinak `API_PORT`).
    get:
      operationId: showResetPasswordPage
      tags:
        - pages
      summary: Zobrazit formulář pro obnovení hesla
      description: |-
        Stránka odkazu pro obnovení hesla: formulář pro nové heslo (heslo dvakrát). Formulář se odesílá na `POST /reset-password`.

        **Limit:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Formulář pro nové heslo.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Odkaz je neplatný (také když byl poslán na adresu, kterou účet už nemá).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Limit `page` (stránka HTML), nebo vrstva podle adresy (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitResetPasswordPage
      tags:
        - pages
      summary: Nastavit nové heslo z formuláře pro obnovení
      description: |-
        Formulář stránky pro obnovení hesla; dělá totéž co `POST /auth/password/reset`, v jedné transakci: všechna zařízení se odhlásí, čekající změna e-mailu se zruší, ostatní odkazy pro obnovení hesla přestanou fungovat, adresa platí za potvrzenou a vlastník dostane e-mail.

        Nejprve se kontroluje odkaz, potom shoda obou hesel, potom pravidla pro hesla. Když je server přetížený, formulář se vrátí s `Retry-After` a odkaz zůstane platný.

        **Limity:** `auth`, potom `auth_reset` (sdílený 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 změněno a všechna zařízení jsou odhlášena.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Znovu formulář s chybou (hesla se liší, slabé heslo), odkaz je neplatný, nebo je formulář neplatný (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` nebo `auth_reset` (stránka HTML), znovu formulář s `Retry-After`, když má tento klient ve frontě příliš mnoho čekajících hashování hesel (odkaz zůstává platný), nebo vrstva podle adresy (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Znovu formulář s `Retry-After`, když je server přetížený (fronta hashování hesel, nebo databáze zůstala zamčená); odkaz zůstává platný. Když server výrazně nestíhá (viz úvod), místo toho stránka „Server error“ (chyba serveru) s `Retry-After` (2 až 5): nic nebylo provedeno a odkaz zůstává platný. Také při překročení časového limitu obsluhy požadavku.'
  /confirm-email-change:
    servers:
      - url: https://caissa.scacelith.com
        description: Oficiální server (stránky jsou v kořeni, mimo `/api/v1`).
      - url: https://{host}:{port}
        description: Libovolný server Scacelith (stránky jsou v kořeni, mimo `/api/v1`).
        variables:
          host:
            default: caissa.scacelith.com
            description: Veřejný název hostitele serveru (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Veřejný port API (`PUBLIC_API_PORT`, jinak `API_PORT`).
    get:
      operationId: showConfirmEmailChangePage
      tags:
        - pages
      summary: Zobrazit stránku pro potvrzení změny e-mailu
      description: |-
        Stránka odkazu pro změnu e-mailu: zobrazí novou adresu a jméno účtu s tlačítkem „Use this e-mail address“ (použít tuto e-mailovou adresu), které odešle formulář 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 potvrzovacím tlačítkem.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Odkaz je neplatný nebo vypršelý (také když se adresa účtu od podání požadavku změnila).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Limit `page` (stránka HTML), nebo vrstva podle adresy (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitConfirmEmailChangePage
      tags:
        - pages
      summary: Potvrdit novou e-mailovou adresu
      description: |-
        Formulář stránky pro změnu e-mailu. Adresa se změní a platí za potvrzenou; zařízení zůstanou přihlášená; dříve odeslané odkazy (potvrzení, obnovení hesla, jiné změny) přestanou fungovat; na dosavadní adresu přijde zpráva 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 změněna.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Odkaz je neplatný, použitý nebo vypršelý, nebo je formulář neplatný (stránka HTML).
        '408':
          $ref: '#/components/responses/HtmlRequestTimeout'
        '409':
          $ref: '#/components/responses/HtmlConflict'
          description: Adresu mezitím zabral jiný úč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), nebo vrstva podle adresy (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Databáze zůstala zamčená (`Retry-After: 1`), nebo server výrazně nestíhá (`Retry-After` 2 až 5, viz úvod): nic se nezměnilo a odkaz stále funguje. Také při překročení časového limitu obsluhy požadavku.'
  /healthz:
    servers:
      - url: https://caissa.scacelith.com
        description: Oficiální server, v kořeni.
      - url: https://caissa.scacelith.com/api/v1
        description: Oficiální server, pod `/api/v1`.
      - url: https://{host}:{port}
        description: Libovolný server Scacelith, v kořeni.
        variables:
          host:
            default: caissa.scacelith.com
            description: Veřejný název hostitele serveru (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Veřejný port API (`PUBLIC_API_PORT`, jinak `API_PORT`).
      - url: https://{host}:{port}/api/v1
        description: Libovolný server Scacelith, pod `/api/v1`.
        variables:
          host:
            default: caissa.scacelith.com
            description: Veřejný název hostitele serveru (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Veřejný port API (`PUBLIC_API_PORT`, jinak `API_PORT`).
    get:
      operationId: getLiveness
      tags:
        - health
      summary: Ověřit, že proces běží
      description: |-
        Dokud proces běží, odpovídá 200 `ok`. Funguje i `HEAD`. Jakákoli jiná metoda odpoví 405 `method_not_allowed` s `Allow: GET, HEAD` (včetně `OPTIONS`).

        **Limity:** pouze vrstva podle adresy.
      security: []
      responses:
        '200':
          description: Proces běží.
          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 podle adresy.'
  /readyz:
    servers:
      - url: https://caissa.scacelith.com
        description: Oficiální server, v kořeni.
      - url: https://caissa.scacelith.com/api/v1
        description: Oficiální server, pod `/api/v1`.
      - url: https://{host}:{port}
        description: Libovolný server Scacelith, v kořeni.
        variables:
          host:
            default: caissa.scacelith.com
            description: Veřejný název hostitele serveru (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Veřejný port API (`PUBLIC_API_PORT`, jinak `API_PORT`).
      - url: https://{host}:{port}/api/v1
        description: Libovolný server Scacelith, pod `/api/v1`.
        variables:
          host:
            default: caissa.scacelith.com
            description: Veřejný název hostitele serveru (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Veřejný port API (`PUBLIC_API_PORT`, jinak `API_PORT`).
    get:
      operationId: getReadiness
      tags:
        - health
      summary: Ověřit, že server přijímá hráče
      description: |-
        Odpovídá 200 `ready`, když server přijímá hráče, a 503 `not_ready`, když se spouští nebo zastavuje. Funguje i `HEAD`. Jakákoli jiná metoda odpoví 405 `method_not_allowed` s `Allow: GET, HEAD` (včetně `OPTIONS`).

        **Limity:** pouze vrstva podle adresy.
      security: []
      responses:
        '200':
          description: Server přijímá hráče.
          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 podle adresy.'
        '503':
          description: Server se spouští nebo 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 relace (`sct_` a za ním 43 znaků base64url) z odpovědi na přihlášení, v hlavičce `Authorization`: `Authorization: Bearer <token>`. Předpona je přesně `Bearer` (rozlišují se velká a malá písmena) a jedna mezera; na hodnotu bez ní, nebo na token, který nemá 1 až 512 tisknutelných znaků ASCII, server odpoví 401 `invalid_token` jako na každý jiný neplatný token. Prázdná hlavička se počítá jako chybějící.
  parameters:
    GameId:
      name: id
      in: path
      required: true
      description: ID partie, kladné desítkové celé číslo o nejvýše 16 číslicích bez úvodních nul, menší než 2^53.
      schema:
        type: integer
        format: int64
        minimum: 1
        maximum: 9007199254740991
      example: 4100000000001
    SessionId:
      name: id
      in: path
      required: true
      description: ID relace, jak ho uvádí `GET /auth/sessions`, zapsané bez úvodních nul (`03` není relace 3).
      schema:
        type: integer
        format: int64
        minimum: 1
      example: 3
    Username:
      name: username
      in: path
      required: true
      description: Uživatelské jméno hráče, bez ohledu na velikost písmen. Server přijme jakékoli jméno o 2 až 24 znacích z `[A-Za-z0-9_.-]` (nejširší pravidlo, jaké kdy povolil).
      schema:
        type: string
        pattern: '^[A-Za-z0-9_.-]{2,24}$'
      example: alice
    BeforeQuery:
      name: before
      in: query
      required: false
      description: ID partie; vypíšou se jen starší partie. Předejte `next` z předchozí stránky. Prázdná hodnota se počítá jako chybějící.
      schema:
        type: integer
        format: int64
        minimum: 1
        maximum: 9007199254740991
      example: 4100000000002
    LimitQuery:
      name: limit
      in: query
      required: false
      description: Velikost stránky, 1 až 50. Větší číslo o nejvýše 3 číslicích se počítá jako 50; prázdná hodnota se počítá jako chybějící.
      schema:
        type: integer
        minimum: 1
        maximum: 999
        default: 20
    LinkTokenQuery:
      name: token
      in: query
      required: false
      description: Token odkazu z e-mailu (43 znaků base64url). Když chybí nebo je nesprávný, zobrazí se stránka „link invalid or expired“ (odkaz je neplatný nebo vypršel).
      schema:
        type: string
        pattern: '^[A-Za-z0-9_-]{43}$'
      example: asfioO4P0r_Zn5DxWzcQuF6Drgxu7wXGjCyS-UFbTaw
  headers:
    CacheControl:
      description: Každá odpověď serveru zakazuje ukládání do mezipaměti.
      schema:
        type: string
        const: no-store
    RetryAfter:
      description: Počet sekund, které je třeba počkat před dalším pokusem; stejná hodnota jako `retryAfter` v těle.
      schema:
        type: integer
        minimum: 1
      example: 30
    WwwAuthenticate:
      description: Výzva typu Bearer, u neplatného tokenu s `error="invalid_token"`.
      schema:
        type: string
        enum:
          - Bearer realm="scacelith"
          - Bearer realm="scacelith", error="invalid_token"
    ConnectionClose:
      description: Server po této odpovědi zavře spojení.
      schema:
        type: string
        const: close
  responses:
    BadRequest:
      description: '`invalid_request` (s `field`, když je chyba v některém poli těla), nebo `invalid_json`.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalidRequest:
              summary: Pole porušuje schéma
              value:
                error: invalid_request
                message: '"password" is required'
                field: password
            invalidJson:
              summary: Tělo není 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, které nelze přečíst
              value:
                error: invalid_pgn
                message: illegal move 'Ke3'
                line: 1
                column: 13
    Unauthorized:
      description: '`unauthorized` (chybí hlavička `Authorization`), nebo `invalid_token` (token má chybný formát, vypršel, byl zneplatněn nebo patří smazané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 relace
              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žadavek je odmítnut.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalidPassword:
              summary: Nesprávné heslo při opětovné autentizaci
              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`, nebo funkce, kterou tento server vypnul.'
      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`: tělo nedorazilo do 10 sekund. Server zavře spojení.'
      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žadavek je v konfliktu se stavem serveru.
      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 přihlášení přes Google nebo tiket je neznámý, použitý nebo vypršelý; zahajte přihlášení znovu.'
      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`: tělo překračuje `HTTP_BODY_LIMIT` (ve výchozím nastavení 16 384 bajtů). Server zavře spojení.'
      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`: cíl požadavku překračuje 4096 znaků.'
      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`: tělo není `application/json`, nebo jeho znaková sada není 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`: partie má více než `GIF_MAX_PLIES` (600) půltahů.'
      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`: vyřešte výzvu `pow` a pošlete stejný požadavek znovu s `pow: { challenge, nonce }`. `reason` uvádí proč: `required`, `malformed`, `signature`, `endpoint`, `network`, `expired`, `bits`, `work` nebo `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žadavků, kvóta účtu nebo fronta hashování hesel), nebo `too_many_attempts` (omezení 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žadavků
              value:
                error: rate_limited
                message: Too many requests; try again later.
                retryAfter: 30
            tooManyAttempts:
              summary: Příliš mnoho neúspěchů na tomto účtu
              value:
                error: too_many_attempts
                message: Too many attempts; wait before trying again.
                retryAfter: 4
    InternalError:
      description: '`internal_error`: neočekávané selhání. Server ho zaznamená 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`: chybný `state` nebo `iss`, nebo Google odmítl kód či poslal ID token, který neprojde ověřením (to zahrnuje i kód nebo ID token druhého z klientů, hry či webu). 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` nebo `timeout`: zkuste to znovu po `retryAfter`, pokud je uvedeno.'
      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 přetížený
              value:
                error: server_busy
                message: The server is busy; try again in a few seconds.
                retryAfter: 9
            busy:
              summary: Databáze zůstala zamčená (čtení)
              value:
                error: busy
                message: Try again shortly.
                retryAfter: 1
            timeout:
              summary: Serveru to trvalo příliš dlouho
              value:
                error: timeout
                message: The server took too long to answer; try again.
    GifFile:
      description: Animovaný GIF jako příloha.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Content-Disposition:
          description: '`attachment; filename="scacelith-<id>.gif"` pro partii z tohoto serveru, `attachment; filename="scacelith-game.gif"` pro PGN poslané přes `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: Velikost souboru v bajtech.
          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, která vysvětluje odmítnutí.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlRequestTimeout:
      description: Formulář nedorazil do 10 sekund (stránka HTML). Server zavře spojení.
      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, která vysvětluje konflikt.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlPayloadTooLarge:
      description: Formulář překračuje `HTTP_BODY_LIMIT` (stránka HTML). Server zavře spojení.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Connection:
          $ref: '#/components/headers/ConnectionClose'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlUnsupportedMediaType:
      description: Tělo není ani formulář (`application/x-www-form-urlencoded`), ani JSON, nebo jeho znaková sada není 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žadavků (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čekávané selhání (stránka HTML s názvem „Server error“, tj. chyba serveru). Server ho zaznamená do logu.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlServiceUnavailable:
      description: Server je přetížený (databáze zůstala zamčená, nebo server výrazně nestíhá; s `Retry-After`), nebo mu odpověď trvala příliš dlouho (stránka HTML s názvem „Server error“, tj. chyba serveru).
      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 všech chybových odpovědí JSON API.
      required:
        - error
        - message
      properties:
        error:
          type: string
          pattern: '^[a-z][a-z0-9_]*$'
          description: Kód chyby ve tvaru `snake_case`. Podle něj klient volí, co zobrazí.
          examples:
            - rate_limited
        message:
          type: string
          description: Anglická věta, určená pro logy a jako náhradní text.
        retryAfter:
          type: integer
          minimum: 1
          description: Počet sekund, které je třeba počkat před dalším pokusem; uvádí se jen u odmítnutí, která časem pominou. Odpověď pak nese i hlavičku `Retry-After` se stejnou hodnotou, kromě 503 `busy` při čtení historie, partií, hráčů a žebříčku.
        field:
          type: string
          description: Chybné pole nebo parametr dotazu (`invalid_request`, `invalid_option` a `invalid_cursor`, `invalid_limit` a `invalid_filter` u `GET /account/games`). Vnořené pole se zapisuje s tečkou (`pow.nonce`).
        reason:
          type: string
          description: 'Důvod: pravidlo, které `weak_password` porušuje, nebo proč byl vyžádán či odmítnut důkaz práce (`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: 'Jen u `banned`: konec pozastavení (ms od epochy Unixu), `null` u trvalého pozastavení.'
        line:
          type: integer
          minimum: 1
          description: 'Jen u `invalid_pgn`: řádek chyby, od 1.'
        column:
          type: integer
          minimum: 1
          description: 'Jen u `invalid_pgn`: sloupec chyby, od 1, ve znacích.'
    PowChallenge:
      type: object
      description: Výzva důkazu práce (`pow` v odpovědi 428 `pow_required`).
      required:
        - challenge
        - bits
        - expiresAt
      properties:
        challenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{16,400}\.[A-Za-z0-9_-]{43}$'
          description: Podepsaná výzva, kterou je třeba poslat zpět beze změny.
        bits:
          type: integer
          minimum: 1
          maximum: 26
          description: Počet úvodních nulových bitů, které musí mít `SHA-256(challenge + ":" + nonce)`.
        expiresAt:
          type: integer
          format: int64
          description: Kdy výzva vyprší (ms od epochy Unixu), 2 minuty po vydání.
    PowAnswer:
      type: object
      description: Odpověď na výzvu důkazu práce.
      additionalProperties: false
      required:
        - challenge
        - nonce
      properties:
        challenge:
          type: string
          minLength: 16
          maxLength: 512
          description: '`challenge` z odpovědi 428, beze změny.'
        nonce:
          type: string
          pattern: '^[0-9]{1,20}$'
          description: Desítkový řetězec o nejvýše 20 číslicích takový, že `SHA-256(challenge + ":" + nonce)` začíná `bits` nulovými bity.
    ServerInfo:
      type: object
      description: Co klient potřebuje, než se přihlásí nebo připojí.
      required:
        - name
        - serverId
        - motd
        - protocol
        - wsPort
        - wsPath
        - registration
        - emailVerification
        - sso
        - mfa
        - pow
        - categories
        - limits
      properties:
        name:
          type: string
          description: Název serveru (`SERVER_NAME`).
        serverId:
          type:
            - string
            - 'null'
          format: uuid
          description: UUID, které databáze dostane při prvním spuštění. Po restartech zůstává stejné. Je `null`, když ho databáze nemůže poskytnout.
        motd:
          type: string
          description: Zpráva dne (`SERVER_MOTD`), případně prázdná.
        protocol:
          type: object
          description: Protokol WebSocketu (`PROTOCOL.md`).
          required:
            - min
            - max
            - schema
            - subprotocol
          properties:
            min:
              type: integer
              minimum: 1
              description: Nejstarší verze protokolu, kterou server podporuje.
            max:
              type: integer
              minimum: 1
              description: Nejnovější verze protokolu, kterou server podporuje.
            schema:
              type: integer
              format: int64
              minimum: 0
              maximum: 4294967295
              description: 'Otisk schématu protokolu (první 4 bajty SHA-256 jeho kanonické podoby jako celé číslo bez znaménka: `0xc649140f`, 3326678031, pro vedlejší verzi protokolu 2); jen informativní, nikdy se neporovnává.'
            subprotocol:
              type: string
              description: Token subprotokolu WebSocketu (`Sec-WebSocket-Protocol`).
        wsPort:
          type: integer
          minimum: 0
          maximum: 65535
          description: Port WebSocketu, který používají hráči (`PUBLIC_WS_PORT`, jinak `WS_PORT`, jinak `API_PORT`).
        wsPath:
          type: string
          const: /ws
          description: Cesta WebSocketu.
        registration:
          type: string
          enum:
            - open
            - closed
          description: Zda lze vytvářet nové účty.
        emailVerification:
          type: boolean
          description: Zda nové účty potvrzují svou adresu (`REQUIRE_EMAIL_VERIFICATION`).
        sso:
          type: object
          required:
            - google
            - googleWeb
          properties:
            google:
              type: boolean
              description: Zda se hře nabízí přihlášení přes Google.
            googleWeb:
              type: boolean
              description: Zda se přihlášení přes Google nabízí i webu (`POST /auth/sso/google/web/start`).
        mfa:
          type: boolean
          const: true
          description: Dvoufázové ověření je vždy k dispozici.
        pow:
          type: object
          required:
            - register
          properties:
            register:
              type: integer
              minimum: 0
              maximum: 26
              description: Počet bitů důkazu práce, který vyžaduje registrace (0 znamená žádný).
        categories:
          type: array
          description: Oficiální tempa hry pro partie na rating (`RATED_CATEGORIES`), v pořadí serveru. Každé jiné tempo hry je `custom`.
          items:
            $ref: '#/components/schemas/Category'
        limits:
          type: object
          description: Pravidla, která klient může zkontrolovat před odesláním formuláře.
          required:
            - usernameMin
            - usernameMax
            - usernamePattern
            - passwordMinLength
            - passwordMaxBytes
            - customTimeControls
            - reportsPerDay
            - wsMaxMessageBytes
          properties:
            usernameMin:
              type: integer
              description: Nejmenší délka uživatelského jména (`USERNAME_MIN`).
            usernameMax:
              type: integer
              description: Největší délka uživatelského jména (`USERNAME_MAX`).
            usernamePattern:
              type: string
              description: Regulární výraz, kterému musí nové uživatelské jméno odpovídat.
            passwordMinLength:
              type: integer
              description: Nejmenší délka hesla ve znacích (`PASSWORD_MIN_LENGTH`).
            passwordMaxBytes:
              type: integer
              description: Největší délka hesla v bajtech UTF-8.
            customTimeControls:
              type: boolean
              description: Zda výzvy a soukromé partie smějí používat vlastní tempa hry.
            reportsPerDay:
              type: integer
              description: Kolik hlášení smí hráč podat za 24 hodin (`REPORTS_PER_DAY`).
            wsMaxMessageBytes:
              type: integer
              description: Největší zpráva WebSocketu, kterou smí klient poslat, v bajtech.
      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ální tempo hry.
      required:
        - id
        - baseSec
        - incSec
      properties:
        id:
          type: string
          pattern: '^[0-9]+\+[0-9]+$'
          description: 'ID kategorie: minuty a přídavek v sekundách (`3+2`).'
        baseSec:
          type: number
          minimum: 0
          description: Základní čas v sekundách.
        incSec:
          type: number
          minimum: 0
          description: Přídavek na tah v sekundách.
    AccountView:
      type: object
      description: Účet tak, jak ho vidí jeho hráč (`user` v odpovědích na přihlášení 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: Uživatelské jméno.
        email:
          type: string
          format: email
          description: E-mailová adresa.
        emailVerified:
          type: boolean
          description: Zda je adresa potvrzená.
        mfaEnabled:
          type: boolean
          description: Zda je zapnuté dvoufázové ověření.
        googleLinked:
          type: boolean
          description: Zda je propojen účet Google.
        hasPassword:
          type: boolean
          description: '`false` u účtu vytvořeného přes Google, který si ještě nenastavil heslo.'
        acceptChallenges:
          type: string
          enum:
            - all
            - none
          description: Zda se přijímají přímé výzvy podle jména (viz `PUT /account/preferences`).
        createdAt:
          type: integer
          format: int64
          description: Kdy byl účet vytvořen (ms od epochy Unixu).
        lastLoginAt:
          type:
            - integer
            - 'null'
          format: int64
          description: Poslední přihlášení (ms od epochy Unixu), nebo `null`.
        pendingEmail:
          type:
            - string
            - 'null'
          format: email
          description: Nová adresa ze změny e-mailu, která čeká na svůj odkaz, nebo `null`.
    SessionAnswer:
      type: object
      description: Nová relace.
      required:
        - token
        - expiresAt
        - user
      properties:
        token:
          type: string
          pattern: '^sct_[A-Za-z0-9_-]{43}$'
          description: Token relace pro hlavičku `Authorization` a pro WebSocket.
        expiresAt:
          type: integer
          format: int64
          description: Absolutní konec relace (ms od epochy Unixu); limit nečinnosti ji může ukončit dříve.
        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 přihlášení s dvoufázovým ověřením; pokračujte přes `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 tohoto kroku pro `POST /auth/login/mfa`.
        expiresIn:
          type: integer
          const: 300
          description: Počet sekund zbývajících na dokončení kroku.
    LoginAnswer:
      description: Relace, nebo druhý krok přihlášení s dvoufázovým ověřením.
      oneOf:
        - $ref: '#/components/schemas/SessionAnswer'
        - $ref: '#/components/schemas/MfaChallenge'
    SsoNeedsUsername:
      type: object
      description: První přihlášení přes Google; pokračujte přes `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 pro `POST /auth/sso/complete`, platný 10 minut.
        suggestedUsername:
          type: string
          description: Uživatelské jméno odvozené ze jména u Googlu nebo z adresy, nebo `""`, když nic nevyhovuje.
    SsoNeedsPassword:
      type: object
      description: Adresu používá účet s heslem; pokračujte přes `POST /auth/sso/google/link`. Zatím není nic propojeno.
      required:
        - needsPassword
        - linkTicket
        - username
        - expiresIn
      properties:
        needsPassword:
          type: boolean
          const: true
        linkTicket:
          type: string
          pattern: '^sso_[A-Za-z0-9_-]{43}$'
          description: Tiket pro `POST /auth/sso/google/link`.
        username:
          type: string
          description: Uživatelské jméno toho účtu (vidí ho jen ten, kdo adresu prokázal u Googlu).
        expiresIn:
          type: integer
          const: 600
          description: Počet sekund, po které tiket zůstává platný.
    SsoFinishAnswer:
      description: Odpověď `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 přihlášení přes Google.
      required:
        - attemptId
        - authUrl
        - state
        - expiresIn
      properties:
        attemptId:
          type: string
          pattern: '^sso_[A-Za-z0-9_-]{43}$'
          description: Pokus pro `POST /auth/sso/google/finish`.
        authUrl:
          type: string
          format: uri
          description: URL Googlu, kterou je po kontrole třeba otevřít v systémovém prohlížeči.
        state:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: '`state`, který musí Google poslat zpět.'
        expiresIn:
          type: integer
          const: 600
          description: Počet sekund, po které pokus zůstává 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: Uživatelské jméno; dále pravidla serveru (viz popis).
        email:
          type: string
          minLength: 1
          maxLength: 254
          description: E-mailová adresa. Ořízne se o mezery na začátku a na konci a uloží se malými písmeny.
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Heslo; dále pravidla pro hesla.
        pow:
          $ref: '#/components/schemas/PowAnswer'
    LoginRequest:
      type: object
      additionalProperties: false
      required:
        - login
        - password
      properties:
        login:
          type: string
          minLength: 1
          maxLength: 254
          description: Uživatelské jméno, nebo e-mailová adresa (jakýkoli text s `@`).
        password:
          type: string
          minLength: 1
          maxLength: 1024
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
        pow:
          $ref: '#/components/schemas/PowAnswer'
    ClientLabel:
      type: string
      maxLength: 64
      description: Nepovinné. Zobrazí se v seznamu přihlášených zařízení (například `Scacelith 1.4 (Windows)`).
    MfaLoginRequest:
      type: object
      additionalProperties: false
      required:
        - mfaToken
      properties:
        mfaToken:
          type: string
          minLength: 1
          maxLength: 64
          description: '`mfaToken` z odpovědi na přihlášení (nebo z odpovědi na přihlášení přes Google).'
        code:
          type: string
          maxLength: 32
          description: Nepovinné. Šestimístný kód z ověřovací aplikace, nebo záložní kód.
        recoveryCode:
          type: string
          maxLength: 32
          description: Nepovinné. Záložní kód. Je potřeba jedno z `code` a `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: Parametr `token` z odkazu pro obnovení hesla.
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Nové heslo; platí pravidla pro hesla z registrace.
    SsoStartRequest:
      type: object
      additionalProperties: false
      required:
        - codeChallenge
        - redirectPort
      properties:
        codeChallenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: Výzva PKCE S256 pro `codeVerifier` klienta (`BASE64URL(SHA-256(codeVerifier))`).
        redirectPort:
          type: integer
          minimum: 1024
          maximum: 65535
          description: Port naslouchacího procesu 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 pro `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í odpovídat výzvě předané do `start`.
        state:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: '`state` tak, jak ho Google poslal zpět.'
        code:
          type: string
          minLength: 1
          maxLength: 2048
          pattern: '^[\x21-\x7E]+$'
          description: '`code` tak, jak ho Google poslal zpět (tisknutelné ASCII bez mezer).'
        iss:
          type: string
          minLength: 1
          maxLength: 256
          description: Nepovinné. `iss` tak, jak ho Google poslal zpět, pokud 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 minut.'
        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 minut.'
        username:
          type: string
          minLength: 1
          maxLength: 64
          description: Uživatelské jméno; platí pravidla registrace.
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
    SessionList:
      type: object
      required:
        - sessions
      properties:
        sessions:
          type: array
          description: Aktivní relace, naposledy použité jako první.
          items:
            $ref: '#/components/schemas/SessionEntry'
    SessionEntry:
      type: object
      description: Aktivní relace (přihlášené zařízení).
      required:
        - id
        - createdAt
        - lastSeenAt
        - expiresAt
        - clientLabel
        - current
      properties:
        id:
          type: integer
          format: int64
          description: ID relace pro `DELETE /auth/sessions/{id}`.
        createdAt:
          type: integer
          format: int64
          description: Přihlášení (ms od epochy Unixu).
        lastSeenAt:
          type: integer
          format: int64
          description: Poslední použití (ms od epochy Unixu), aktualizované nejvýše jednou za 5 minut (když server výrazně nestíhá, i později).
        expiresAt:
          type: integer
          format: int64
          description: Absolutní konec (ms od epochy Unixu); limit nečinnosti může relaci ukončit dříve.
        clientLabel:
          type:
            - string
            - 'null'
          description: '`clientLabel` z přihlášení, nebo `null`, když žádný neposlalo.'
        current:
          type: boolean
          description: Zda jde o relaci, ze které požadavek přichází.
    AccountMe:
      type: object
      required:
        - user
        - ratings
        - sanctions
        - ban
      properties:
        user:
          $ref: '#/components/schemas/AccountView'
        ratings:
          type: array
          description: Jeden záznam pro každou kategorii, ve které hráč odehrál partie na rating.
          items:
            $ref: '#/components/schemas/RatingSummary'
        sanctions:
          type: array
          description: Aktivní sankce.
          items:
            $ref: '#/components/schemas/ActiveSanction'
        ban:
          description: '`{ until }`, dokud je účet pozastavený, jinak `null`.'
          oneOf:
            - $ref: '#/components/schemas/Ban'
            - type: 'null'
    RatingSummary:
      type: object
      description: Záznam ratingu jedné kategorie.
      required:
        - category
        - rating
        - games
        - wins
        - draws
        - losses
        - peak
        - provisional
      properties:
        category:
          type: string
          description: ID kategorie.
        rating:
          type: integer
          description: Rating.
        games:
          type: integer
          minimum: 0
          description: Partie odehrané v kategorii (započítané i nezapočítané).
        wins:
          type: integer
          minimum: 0
        draws:
          type: integer
          minimum: 0
        losses:
          type: integer
          minimum: 0
        peak:
          type: integer
          description: Nejvyšší dosažený rating.
        provisional:
          type: boolean
          description: '`true`, dokud je záznam ještě v úvodní fázi bez ratingu nebo má méně než `PROVISIONAL_GAMES` započítaných partií (hra ho zobrazuje jako `1510?`).'
    ActiveSanction:
      type: object
      required:
        - kind
        - reason
        - startsAt
        - endsAt
      properties:
        kind:
          type: string
          enum:
            - ban
            - mm_block
            - warning
          description: Druh sankce.
        reason:
          type:
            - string
            - 'null'
          description: Uvedený důvod, pokud nějaký je.
        startsAt:
          type: integer
          format: int64
          description: Začátek (ms od epochy Unixu).
        endsAt:
          type:
            - integer
            - 'null'
          format: int64
          description: Konec (ms od epochy Unixu), `null`, když je sankce trvalá.
    Ban:
      type: object
      required:
        - until
      properties:
        until:
          type:
            - integer
            - 'null'
          format: int64
          description: Konec pozastavení (ms od epochy Unixu), `null`, když je trvalé.
    Preferences:
      type: object
      additionalProperties: false
      required:
        - acceptChallenges
      properties:
        acceptChallenges:
          type: string
          enum:
            - all
            - none
          description: '`all` pro přijímání přímých výzev podle jména, `none` pro jejich odmítání.'
    PasswordChangeRequest:
      type: object
      additionalProperties: false
      required:
        - currentPassword
        - newPassword
      properties:
        currentPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Současné heslo.
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Nové heslo; platí pravidla pro hesla z registrace.
    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 čekajícího tajného klíče, přesně 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é; nutné při dvoufázovém ověření (nebo `recoveryCode`). Kód z ověřovací aplikace, nebo záložní kód.
        recoveryCode:
          type: string
          minLength: 1
          maxLength: 32
          description: Nepovinné. Záložní kód (`xxxx-xxxx-xx`; na velikosti písmen, mezerá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 ověřovací aplikace (záložní kód se odmítne).
    EmailChangeRequest:
      type: object
      additionalProperties: false
      required:
        - newEmail
        - password
      properties:
        newEmail:
          type: string
          minLength: 1
          maxLength: 254
          description: Nová adresa; ořízne se o mezery na začátku a na konci, převede na malá písmena a pak se kontroluje stejně jako adresa při registraci.
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Heslo účtu.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: Nepovinné; nutné při dvoufázovém ověření (nebo `recoveryCode`). Kód z ověřovací aplikace, nebo záložní kód.
        recoveryCode:
          type: string
          minLength: 1
          maxLength: 32
          description: Nepovinné. Záložní kód.
    TotpSetup:
      type: object
      description: Čekající tajný klíč pro ověřovací aplikaci.
      required:
        - secret
        - uri
        - algorithm
        - digits
        - period
      properties:
        secret:
          type: string
          pattern: '^[A-Z2-7]+$'
          description: Tajný klíč v base32 k zadání do aplikace.
        uri:
          type: string
          format: uri
          description: URI `otpauth://totp/...` k zobrazení jako QR kód.
        algorithm:
          type: string
          const: SHA1
        digits:
          type: integer
          const: 6
        period:
          type: integer
          const: 30
          description: Počet sekund na jeden krok.
    RecoveryCodeList:
      type: array
      description: 10 záložních kódů, zobrazených jen tentokrát. Každý funguje jednou.
      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: Jméno v záznamu partie (`deleted#<id>` u smazaného účtu).
        rating:
          type:
            - integer
            - 'null'
          description: Rating na začátku, `null`, když není znám.
        ratingAfter:
          type:
            - integer
            - 'null'
          description: Rating po partii. Partie na rating ho má vždy; `null` jen u partie, která se do ratingů nezapočítává (bez ratingu, s vlastním tempem, zrušená).
        ratingDiff:
          type:
            - integer
            - 'null'
          description: Změna ratingu, `0`, když pravidla ratingu nechají rating beze změny (nulové skóre podle pravidel FIDE, soupeř bez ratingu, první partie hráče bez ratingu); `null` stejně jako `ratingAfter`.
    GameSummary:
      type: object
      description: Souhrn uložené 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ální kategorie (`3+2`), nebo `custom`.
        rated:
          type: boolean
          description: Zda se partie započítává do ratingů.
        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` (viz kódy partií).'
        reason:
          type: integer
          minimum: 0
          maximum: 255
          description: Kód důvodu ukončení (viz kódy partií).
        result:
          type: string
          enum:
            - 1-0
            - 0-1
            - 1/2-1/2
            - '*'
          description: Výsledek, `*` u zrušené partie.
        termination:
          $ref: '#/components/schemas/Termination'
        plies:
          type: integer
          minimum: 0
          description: Počet půltahů.
        startedAt:
          type: integer
          format: int64
          description: Začátek (ms od epochy Unixu).
        endedAt:
          type: integer
          format: int64
          description: Konec (ms od epochy Unixu).
    Termination:
      type: string
      description: Název důvodu ukončení (viz kódy partií); `Unknown` u kódu, který protokol nezná.
      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: Souhrn partie z pohledu jednoho hráče.
      allOf:
        - $ref: '#/components/schemas/GameSummary'
        - type: object
          required:
            - color
          properties:
            color:
              type: string
              enum:
                - white
                - black
              description: Strana hráče.
    HistoryGameSummary:
      description: Partie z historie přihlášeného hráče.
      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: Přídavek v milisekundách.
            outcome:
              type: string
              enum:
                - win
                - loss
                - draw
                - aborted
              description: Výsledek z pohledu hráče.
    HistoryPage:
      type: object
      required:
        - games
        - next
        - total
      properties:
        games:
          type: array
          description: Partie na stránce, od nejnovějších.
          items:
            $ref: '#/components/schemas/HistoryGameSummary'
        next:
          type:
            - integer
            - 'null'
          format: int64
          description: ID, které je třeba předat jako `before` pro další stránku, nebo `null` na poslední stránce.
        total:
          type: integer
          minimum: 0
          description: Počet partií odpovídajících filtru napříč všemi stránkami.
    MoveRecord:
      type: object
      description: Jeden půltah partie.
      required:
        - uci
        - spentMs
        - clockMs
      properties:
        uci:
          type: string
          pattern: '^[a-h][1-8][a-h][1-8][nbrq]?$'
          description: Tah v notaci UCI, při proměně s přidaným `n`, `b`, `r` nebo `q`.
        spentMs:
          type:
            - integer
            - 'null'
          minimum: 0
          description: Čas započítaný za tah (ms), `null`, když ho záznam nemá.
        clockMs:
          type:
            - integer
            - 'null'
          minimum: 0
          description: Čas na hodinách táhnoucího hráče po tahu (ms), `null`, když ho záznam nemá.
    PgnTagsView:
      type: object
      description: Hlavní značky PGN pro zobrazení. `Termination` je zde název důvodu ukončení; soubor PGN používá standardní hodnoty PGN.
      required:
        - Event
        - Site
        - Date
        - Round
        - White
        - Black
        - Result
        - WhiteElo
        - BlackElo
        - TimeControl
        - Termination
        - PlyCount
      properties:
        Event:
          type: string
          description: '`<SERVER_NAME> rated <category>` nebo `<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: Datum začátku 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čátku, nebo `"-"`.
          oneOf:
            - type: integer
            - type: string
              const: '-'
        BlackElo:
          description: Rating na začátku, nebo `"-"`.
          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 tahy a stavem hodin. Má pole souhrnu 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: Přídavek v milisekundách.
            statusName:
              type: string
              enum:
                - WhiteWins
                - BlackWins
                - Draw
                - Aborted
              description: Název hodnoty `status`.
            rematchOf:
              type:
                - integer
                - 'null'
              format: int64
              description: ID partie, jejíž odvetou tato partie je, nebo `null`.
            moves:
              type: array
              description: Jedna položka na každý půltah.
              items:
                $ref: '#/components/schemas/MoveRecord'
            pgn:
              $ref: '#/components/schemas/PgnTagsView'
            you:
              type: string
              enum:
                - white
                - black
              description: Jen když hráč tokenu tuto partii hrál; jeho strana.
            reportable:
              type: boolean
              description: Jen když hráč tokenu tuto partii hrál; `true`, když by `POST /reports` teď přijal hlášení soupeře za tuto partii (partie skončila během posledních 7 dní, denní kvóta není vyčerpaná a soupeř ještě nebyl za tuto partii nahlášen).
    PlayerProfile:
      type: object
      required:
        - username
        - createdAt
        - ratings
        - games
      properties:
        username:
          type: string
          description: Uživatelské jméno tak, jak ho hráč zapsal.
        createdAt:
          type: integer
          format: int64
          description: Kdy byl účet vytvořen (ms od epochy Unixu).
        ratings:
          type: array
          description: Jen oficiální kategorie, v pořadí serveru.
          items:
            $ref: '#/components/schemas/RatingSummary'
        games:
          type: object
          required:
            - total
            - rated
            - wins
            - draws
            - losses
          properties:
            total:
              type: integer
              minimum: 0
              description: Všechny uložené partie včetně partií bez ratingu a zrušených.
            rated:
              type: integer
              minimum: 0
              description: Partie na rating, sečtené přes 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: Uživatelské jméno tak, jak ho hráč zapsal.
        games:
          type: array
          description: Partie na stránce, od nejnovějších.
          items:
            $ref: '#/components/schemas/PlayerGameSummary'
        next:
          type:
            - integer
            - 'null'
          format: int64
          description: ID poslední partie vždy, když je stránka plná (další stránka pak může být prázdná), jinak `null`.
    Leaderboard:
      type: object
      required:
        - category
        - minGames
        - updatedAt
        - players
      properties:
        category:
          type: string
          description: ID kategorie.
        minGames:
          type: integer
          minimum: 0
          description: Počet započítaných partií, který záznam potřebuje, aby byl uveden (`PROVISIONAL_GAMES`).
        updatedAt:
          type: integer
          format: int64
          description: Kdy byl žebříček vypočten (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, nejvýše 65 536 bajtů v UTF-8. Použije se jen první partie.
        size:
          type: string
          enum:
            - small
            - medium
            - large
          default: medium
          description: Nepovinné. Velikost obrázku.
        orientation:
          type: string
          enum:
            - white
            - black
          default: white
          description: Nepovinné. Strana dole na šachovnici.
        delayMs:
          type: integer
          minimum: 100
          maximum: 3000
          default: 500
          description: Nepovinné. Milisekundy na tah, číslo JSON s celočíselnou hodnotou.
        coords:
          type: boolean
          default: true
          description: Nepovinné. Zda se kolem šachovnice vykreslí písmena sloupců a čísla řad.
    ReportRequest:
      type: object
      required:
        - gameId
        - reported
        - category
      properties:
        gameId:
          description: Partie, jako celé číslo nebo řetězec o 1 až 16 číslicích.
          oneOf:
            - type: integer
              format: int64
              minimum: 1
              maximum: 9007199254740991
            - type: string
              pattern: '^[0-9]{1,16}$'
        reported:
          type: string
          minLength: 1
          maxLength: 24
          description: Uživatelské jméno soupeře, jak je uvedeno v záznamu partie nebo jak zní teď, bez ohledu na velikost písmen. Nesmí být prázdné.
        category:
          type: string
          enum:
            - cheating
            - abuse
            - other
          description: Čeho se hlášení týká.
        comment:
          type:
            - string
            - 'null'
          description: Nepovinné. Nejvýše 500 znaků po odstranění řídicích znaků (kromě tabulátoru a odřádkování) a oříznutí mezer na začátku a na konci.
    AccountExport:
      type: object
      description: Vše, co server o účtu uchovává (č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: Kdy byl export vytvořen.
        server:
          type: object
          required:
            - name
            - host
          properties:
            name:
              type: string
              description: '`SERVER_NAME`.'
            host:
              type: string
              description: '`SERVER_PUBLIC_HOST`.'
        notes:
          type: array
          description: Co soubor obsahuje a co vynechává, jednoduchou angličtinou pro hráče.
          items:
            type: string
        account:
          description: Pohled na účet z `GET /account/me`, s `googleEmail`.
          allOf:
            - $ref: '#/components/schemas/AccountView'
            - type: object
              required:
                - googleEmail
              properties:
                googleEmail:
                  type:
                    - string
                    - 'null'
                  description: Adresa propojeného účtu Google, nebo `null`.
        ratings:
          type: array
          description: Úplné záznamy ratingu.
          items:
            $ref: '#/components/schemas/ExportRating'
        ratingRefunds:
          type: array
          description: Body ratingu vrácené poté, co byl soupeř přistižen při podvádění, sečtené po dnech (UTC) a kategoriích, od nejnovějších. Neuvádějí se partie ani podvodníci.
          items:
            $ref: '#/components/schemas/RatingRefund'
        sessions:
          type: array
          description: Všechny uložené relace, od nejnovějších, bez jakéhokoli tokenu. Pravidelné mazání podle doby uchovávání odstraní vypršelou relaci a odhlášenou relaci den po odhlášení.
          items:
            $ref: '#/components/schemas/ExportSession'
        securityEvents:
          type: array
          description: Bezpečnostní události, od nejnovějších, uchovávané `RETENTION_SECURITY_DAYS` dní.
          items:
            $ref: '#/components/schemas/SecurityEvent'
        sanctions:
          type: array
          description: Všechny sankce včetně zrušených. Jméno moderátora se nikdy neuvádí.
          items:
            $ref: '#/components/schemas/ExportSanction'
        conduct:
          type: array
          description: Události chování zaznamenané u hráčových opuštěných, zrušených a nenastoupených partií, uchovávané 30 dní.
          items:
            $ref: '#/components/schemas/ConductEvent'
        reportsFiled:
          type: array
          description: Hlášení, která 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šechny partie od nejnovějších jako souhrny z `GET /account/games`. Tahy jsou v `GET /games/{id}` a PGN v `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: Zda už hráč opustil úvodní fázi bez ratingu.
            countedGames:
              type: integer
              minimum: 0
              description: Partie, které se započítaly do ratingu.
            updatedAt:
              type: integer
              format: int64
              description: Kdy se záznam naposledy změnil.
    RatingRefund:
      type: object
      required:
        - day
        - category
        - points
      properties:
        day:
          type: integer
          format: int64
          description: 00:00 UTC daného dne (ms od epochy Unixu).
        category:
          type: string
        points:
          type: integer
          description: Body vrácené toho dne v dané kategorii.
    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: Kdy byla relace odhlášena, nebo `null`.
        clientLabel:
          type:
            - string
            - 'null'
        ip:
          type:
            - string
            - 'null'
          description: Adresa, ze které proběhlo přihlášení, vymazaná po `RETENTION_IP_DAYS` dnech.
    SecurityEvent:
      type: object
      description: |-
        Bezpečnostní událost. `ip` se uvádí jen u akcí provedených po přihlášení, s heslem účtu (a druhým faktorem) nebo pomocí odkazu zaslaného 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ý jiný druh má `ip: null` a `ip` se vymaže po `RETENTION_IP_DAYS` dnech.

        `detail` uchovává jen tato pole: `login`: `method`; `sso_login`, `sso_account_created`: `provider`; `sso_linked`: `provider` a `method` (`password` nebo `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`. Událost `moderator_action` uchovává jen `{ action }`, a to u akcí `ban`, `unban`, `reset_mfa`, `verify_email` a `revoke_sessions` (ostatní akce moderátorů se vynechávají). Každý jiný druh má `detail: null`. Události `rating_refund` se vynechávají: jejich body jsou v `ratingRefunds`.
      required:
        - kind
        - at
        - ip
        - detail
      properties:
        kind:
          type: string
          description: Druh události (`login`, `password_changed`…).
        at:
          type: integer
          format: int64
          description: Kdy k ní došlo.
        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: Současné veřejné jméno nahlášeného hráče.
        category:
          type: string
          enum:
            - cheating
            - abuse
            - other
        comment:
          type:
            - string
            - 'null'
        createdAt:
          type: integer
          format: int64
        status:
          type: string
          enum:
            - open
            - closed
          description: Zda je hlášení stále otevřené (zda byl nahlášený hráč sankcionován, se neuvádí).
    LinkTokenForm:
      type: object
      additionalProperties: false
      required:
        - token
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: Token odkazu z e-mailu (skryté pole formuláře stránky).
    ResetPasswordForm:
      type: object
      additionalProperties: false
      required:
        - token
        - newPassword
        - confirmPassword
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: Token odkazu pro obnovení hesla (skryté pole formuláře).
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Nové heslo; platí pravidla pro hesla z registrace.
        confirmPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Nové heslo znovu; musí být stejné.
    HtmlDocument:
      type: string
      contentMediaType: text/html
      description: Stránka HTML v UTF-8 (`text/html; charset=utf-8`), bez JavaScriptu a externích zdrojů.
