openapi: 3.1.1
info:
  title: HTTPS API на сървъра Scacelith
  version: "1.1.0"
  summary: HTTPS API на всеки сървър Scacelith – както на официалния, така и на сървърите на общността.
  description: |-
    Всеки сървър Scacelith – официалният `caissa.scacelith.com` и всеки сървър на общността – обслужва този HTTPS API. Играта го използва за всичко извън самата партия: регистрация и вход (включително двуфакторно удостоверяване и вход с Google), страницата на акаунта, историята на партиите, изтеглянето на PGN и анимираните GIF на партиите, устройствата с активен вход, изтеглянето на данните, изтриването на акаунта и сигналите.

    Играта в реално време (търсене на противник, предизвикателства, ходове, часовници) минава през WebSocket на същия сървър (`wss://<host>/ws`), който се отваря с токен на сесия от този API; тя е описана в `PROTOCOL.md`, а не тук.

    Стойностите по подразбиране по-долу са тези на сървър с непроменена конфигурация; сървър на общността може да ги промени (`CONFIG.md` изброява всички настройки).

    ## Базов URL адрес, порт и транспорт

    - Официален сървър: `https://caissa.scacelith.com/api/v1`, TCP порт 443.
    - Сървър на общността: `https://<SERVER_PUBLIC_HOST>[:<port>]/api/v1`. По подразбиране `API_PORT` е 443; зад NAT или прокси портът, който използват играчите, е `PUBLIC_API_PORT`.
    - По подразбиране WebSocket на играта е на същия порт (`WS_PORT` го премества; `GET /info` казва на клиента къде се намира).
    - HTTP/1.1 върху TLS. При `TLS_MODE=proxy` TLS се терминира от обратен прокси пред сървъра. `TLS_MODE=off` (обикновен HTTP) е само за локална разработка.
    - HTML страниците, които се отварят от връзките в имейлите (`/verify-email`, `/reset-password`, `/confirm-email-change`), се намират в корена на сървъра, извън `/api/v1`. Крайните точки за проверка на състоянието отговарят както в корена, така и под `/api/v1`. Входът с Google няма страница на сървъра: Google връща браузъра към самата игра, на `127.0.0.1`, или към страница на уебсайта, който го е започнал (`GOOGLE_WEB_REDIRECT_URI`).
    - Наклонена черта в края се пренебрегва (`/api/v1/info/` е `/api/v1/info`), а параметрите на пътя се URL-декодират (на параметър, който не е валидно URL кодиране, отговорът е 400 `invalid_request`).

    ## Заявки

    - **JSON тела.** Заявките `POST`, `PUT` и `DELETE` носят JSON обект с `Content-Type: application/json`. `charset`, различен от UTF-8, се отказва, както и всеки друг тип съдържание (415 `unsupported_media_type`). Празно тяло се приема за `{}` – точно това приемат крайните точки без параметри (`POST /auth/logout`, `POST /auth/logout-all`, `DELETE /auth/sessions/{id}`).
    - **Размер и време.** Тялото е ограничено до `HTTP_BODY_LIMIT` байта (16 384 по подразбиране; `POST /gif` има собствен лимит от 135 168 байта): 413 `payload_too_large`. То трябва да пристигне до 10 секунди: 408 `request_timeout`. След всяка от двете грешки сървърът затваря връзката.
    - **Строги схеми.** Поле, което крайната точка не познава, се отказва, поле, което не е отбелязано като незадължително, е задължително, а типовете и дължините се проверяват. При всяко от тези нарушения отговорът е 400 `invalid_request`, като `field` посочва полето (с точки за вложено поле, например `pow.nonce`). Низовете не могат да съдържат управляващи знаци. Дължините се броят в кодови единици на UTF-16, така че знак извън основната многоезична равнина (например емоджи) се брои двойно. На тяло, което не е JSON, отговорът е 400 `invalid_json`. `POST /gif` и `POST /reports` сами проверяват телата си (вж. всяка крайна точка).
    - **Параметри на заявката (query string).** Важи първото срещане на даден параметър, непознатите параметри се пренебрегват, а `+` се декодира като интервал: крайните точки, които приемат категория контрола на времето, приемат както `3%2B2`, така и `3+2`.
    - **Методи.** `HEAD` е `GET` без тялото. На `OPTIONS` към съществуващ път отговорът е 204 със заглавка `Allow` (и заглавките на предварителна заявка CORS, вж. по-долу). На всеки друг метод, който пътят няма, отговорът е 405 `method_not_allowed` с `Allow`.
    - **Цел на заявката (request target).** Тя не може да надвишава 4096 знака (414 `uri_too_long`). Заявка, която изобщо не може да бъде разчетена или чиято начална част (редът на заявката и заглавките) е твърде голяма или пристига твърде бавно, получава празен отговор 400, 431 или 408 и връзката се затваря.
    - **CORS: списък на разрешените произходи, изключен по подразбиране.** API обслужва играта, която не изпраща `Origin`. При празен `CORS_ORIGINS` (по подразбиране) никога не се изпраща заглавка `Access-Control-*`, така че никоя уеб страница не може да прочете отговор. `CORS_ORIGINS` изброява уеб произходите (origins), чиито страници могат да извикват API от браузър, всеки точно както браузърите го изпращат в `Origin`: `https://host` или `https://host:port`, с малки букви, без път, без наклонена черта в края и без порта по подразбиране; `http://` само за `localhost`, `127.0.0.1` и `[::1]` (локална разработка); без заместващи знаци. Официалният сървър задава `CORS_ORIGINS=https://scacelith.com,https://www.scacelith.com` за своя уебсайт. Тогава:
      - Всеки отговор за път под `/api/v1` освен крайните точки за проверка на състоянието, както успешен, така и с грешка (включително 429 на слоя по адрес), носи `Vary: Origin`. Когато `Origin` на заявката съвпада байт по байт с един от изброените произходи, той носи и `Access-Control-Allow-Origin: <that origin>`, а освен в отговора на предварителна заявка – и `Access-Control-Expose-Headers: Retry-After, Content-Disposition`, така че страницата да може да чете кодовете на грешките, `Retry-After` и имената на файловете при изтегляне на PGN и GIF.
      - На предварителна заявка (`OPTIONS` с `Access-Control-Request-Method`) от изброен произход към съществуващ път отговорът е 204, без тяло, с `Allow`, `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods` (методите от `Allow`), `Access-Control-Allow-Headers: Authorization, Content-Type`, `Access-Control-Max-Age: 600` и `Vary: Origin`. Тя не изисква токен и не се брои нито в бюджет на акаунт, нито в лимит на крайна точка; като всяка заявка тя взема един токен от слоя по адрес. Браузърът преизползва предварителната заявка 10 минути, но само за същия точен URL адрес, включително параметрите на заявката. Заявка без `Authorization` и без JSON тяло, като `GET /info` или `GET /leaderboard`, не се нуждае от предварителна заявка.
      - Всеки друг отговор на предварителна заявка означава неуспешна предварителна заявка, дори с CORS заглавките: 404 за несъществуващ път или 429 на слоя по адрес. Тогава браузърът не изпраща заявката, а страницата вижда мрежова грешка, без отговора и без неговия `Retry-After`.
      - Никога не се изпраща нито `Access-Control-Allow-Credentials`, нито `Access-Control-Allow-Origin: *`: страницата изпраща токена на сесията си в `Authorization: Bearer`, както прави играта, а API не задава бисквитки. HTML страниците, крайните точки за проверка на състоянието и празните отговори на неразчетими заявки не носят CORS заглавка. WebSocket не използва CORS: `WS_ALLOWED_ORIGINS` изброява страниците, които могат да го отварят.
      - Страница от неизброен произход не може да прочете нищо, а нейните междусайтови заявки за запис никога не се изпращат: JSON телата и `Authorization` изискват предварителна заявка, чийто отговор няма `Access-Control-Allow-Origin` за този произход. `Cross-Origin-Resource-Policy: same-origin` остава във всеки отговор: тя се отнася само за заявки `no-cors` (друг сайт, който вгражда отговор), никога за заявките `cors` на изброена страница.

    ## Отговори и грешки

    - Отговорите са JSON в UTF-8, с изключение на изтеглянето на PGN (`application/x-chess-pgn`), анимираните GIF (`image/gif`) и HTML страниците. Моментите във времето са в милисекунди от 1970-01-01 UTC. Идентификаторите са цели числа. Идентификаторите на партиите имат до 16 цифри, но остават под 2^53, така че JSON число (double) ги представя точно.
    - Всеки отговор на API носи `Cache-Control: no-store`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`, `X-Frame-Options: DENY`, `Cross-Origin-Resource-Policy: same-origin` и `Content-Security-Policy: default-src 'none'; frame-ancestors 'none'` (HTML страниците имат собствена политика). При `TLS_MODE=native` той носи и `Strict-Transport-Security: max-age=31536000`. Отговорите, които слушателят пише сам, преди API (`GET` и `HEAD` на крайните точки за проверка на състоянието, 429 на слоя по адрес и 500 `internal_error`, когато API се срине изцяло), носят същите заглавки без `Cross-Origin-Resource-Policy`, а техният `Content-Type` е `application/json` без charset.
    - Отговорът трябва да бъде прочетен до 60 секунди от момента, в който сървърът го е подготвил, което има значение само за големите отговори (GIF, експортът на данните, дълъг PGN). Времето, което сървърът отделя за подготовка на отговора, не се брои нито в този срок, нито в 30-те секунди без входящ или изходящ байт, след които връзката се затваря.

    Грешките имат една и съща форма (схемата `Error`):

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

    `message` е предназначено за логовете и като резервен вариант; клиентът избира какво да покаже според `error`. `retryAfter` (в секунди) присъства само при откази, които отпадат с времето, и тогава отговорът носи и заглавка `Retry-After` със същата стойност; единственото изключение е 503 `busy` при четенето на историята, партиите, играчите и класацията, който има само полето. Някои грешки добавят полета: `field` (невалидни входни данни), `reason` (`weak_password`, `pow_required`), `pow` (`pow_required`), `until` (`banned`), `line` и `column` (`invalid_pgn`).

    Грешки, които може да върне всяка крайна точка:

    | Статус | `error` | Кога |
    |---|---|---|
    | 400 | `invalid_request` | Тялото нарушава схемата на крайната точка (`field` казва къде), целта на заявката или `Content-Length` е с грешен формат, параметър на пътя не е валидно URL кодиране или тялото е прекъснато. |
    | 400 | `invalid_json` | Тялото не е JSON. |
    | 401 | `unauthorized` | Липсва заглавка `Authorization` при крайна точка, която изисква сесия. |
    | 401 | `invalid_token` | Токенът е с грешен формат, изтекъл, отменен или принадлежи на изтрит акаунт; също и при крайни точки, където сесията не е задължителна. |
    | 404 | `not_found` | Няма такава крайна точка. Някои крайни точки също го използват: няма такава партия, такъв играч или такава сесия. |
    | 405 | `method_not_allowed` | Пътят съществува за други методи (вж. `Allow`). |
    | 408 | `request_timeout` | Тялото не пристигна до 10 секунди. |
    | 413 | `payload_too_large` | Тялото надвишава `HTTP_BODY_LIMIT` (135 168 байта за `POST /gif`). |
    | 414 | `uri_too_long` | Целта на заявката надвишава 4096 знака. |
    | 415 | `unsupported_media_type` | Тялото не е `application/json` или неговият charset не е UTF-8. |
    | 429 | `rate_limited` | Лимит на честотата (вж. по-долу): `retryAfter` плюс заглавка `Retry-After`. |
    | 500 | `internal_error` | Неочакван срив. Сървърът го записва в лога. |
    | 503 | `timeout` | Сървърът не отговори до 30 секунди (60 секунди за експорта, 45 секунди за GIF при настройките по подразбиране). |
    | 503 | `server_busy` | Търсене на сесия или промяна на акаунт завари базата данни заключена (`retryAfter` 1) или опашката за хеширане на пароли е пълна (вж. по-долу). Също така, с `retryAfter` от 2 до 5, когато сървърът сериозно изостава: за `POST`, `PUT`, `PATCH` или `DELETE` – базата данни има твърде много чакащи записи; за всяка заявка – твърде много по-ранни заявки все още се изпълняват след изтичането на времето си или токенът на сесията не можа да бъде проверен до 5 секунди. Заявката не е изпълнена и лимитите ѝ на честотата не са взели нищо. |

    Крайните точки за четене (история на партиите, партии, играчи, класация) и експортът отговарят с 503 `busy` с `retryAfter: 1`, когато базата данни остана заключена.

    Крайните точки, които проверяват или хешират парола, могат да отговорят и с една от следните грешки, и двете със случаен `retryAfter` от 5 до 15 секунди:

    - 503 `server_busy`: опашката на сървъра за хеширане на пароли (`PASSWORD_HASH_QUEUE_MAX`) е пълна или времето за чакане изтече.
    - 429 `rate_limited`: когато опашката е наполовина пълна, този клиент (IPv4 адрес или IPv6 /48) вече има `PASSWORD_HASH_WAITERS_PER_SOURCE` чакащи хеширания. Този 429 връща токените на лимитите на честотата, които заявката е взела.

    Нищо не е променено и не е отчетен неуспешен опит (освен при `POST /auth/sso/google/link`, чийто опит с билета и неуспех на акаунта са отчетени преди хеширането), а връзката за нулиране на паролата остава валидна.

    HTML страниците връщат грешките си като HTML страници със същите кодове на статуса.

    ## Удостоверяване

    Крайните точки, които изискват сесия, приемат bearer токен в заглавката `Authorization` (схемата `bearerAuth`):

    ```
    Authorization: Bearer sct_L_8GDd7uzfQ3QQWtqrsWXDTsFWzRwIvJcwIGHhjWPS8
    ```

    - **Получаване на токен.** Токен (`sct_`, последван от 43 знака base64url) се получава от `POST /auth/login`, след това от `POST /auth/login/mfa`, когато двуфакторното удостоверяване е включено, от `POST /auth/sso/google/finish` и `POST /auth/sso/google/link` (вход с Google) и от `POST /auth/sso/complete`. Всяка от тях отговаря с `{ token, expiresAt, user }`. Сървърът съхранява само SHA-256 хеш на токена.
    - **Задължителна сесия.** При липсваща заглавка отговорът е 401 `unauthorized` с `WWW-Authenticate: Bearer realm="scacelith"`. При невалиден токен отговорът е 401 `invalid_token` с `WWW-Authenticate: Bearer realm="scacelith", error="invalid_token"`. Клиентът трябва да забрави токен, получил `invalid_token`, и да влезе отново.
    - **Незадължителна сесия.** При `GET /games/{id}`, `GET /games/{id}/pgn`, `GET /players/{username}` и `GET /players/{username}/games` сесията не е задължителна. Без заглавката те връщат публичния изглед; ако бъде изпратена заглавка, токенът ѝ трябва да е валиден.
    - **Заето хранилище.** Когато базата данни остане заключена, докато се проверява токен, крайна точка, която изисква или приема сесия, отговаря с 503 `server_busy` с `retryAfter: 1` (и `Retry-After`); когато проверката продължи повече от 5 секунди, тя отговаря с 503 `server_busy` с `retryAfter` от 2 до 5. И в двата случая за токена не е взето решение: запазете го и изпратете заявката отново.
    - **Продължителност.** Сесията приключва при първото от двете: `SESSION_MAX_DAYS` (90) дни след входа (това е `expiresAt`) или `SESSION_IDLE_DAYS` (30) дни без използване. Всяко използване отлага лимита за неактивност; сървърът записва новата стойност най-много веднъж на 5 минути, във фонов режим. Докато базата му данни има много чакащи записи, той отлага този запис за по-късна заявка, освен в последния час преди лимита за неактивност, така че използвана сесия не изтича. Акаунтът пази най-много `MAX_SESSIONS_PER_USER` (10) сесии: нов вход прекратява най-старите над този брой.
    - **Прекратяване.** Излизането (`POST /auth/logout`, `POST /auth/logout-all`, `DELETE /auth/sessions/{id}`) прекратява сесии; същото правят смяната на паролата (другите сесии), нулирането на паролата и изтриването на акаунта (всички сесии), както и администратор. Прекратяването през API влиза в сила веднага, а WebSocket връзката, отворена с прекратена сесия, се затваря. Смяната или нулирането на паролата и изтриването на акаунта прекратяват сесиите в същата транзакция като самата промяна: ако някоя от тях се провали, не е прекратена нито една сесия и нищо друго не е променено. Сървърът кешира търсенията на сесии за 30 секунди, така че прекратяване чрез административната команда, която е отделен процес, влиза в сила до 30 секунди.
    - **Обхват.** Токенът принадлежи на един сървър и отваря и неговия WebSocket. Никога не го изпращайте към друг сървър.

    ## Лимити на честотата и други ограничения

    Лимитите се прилагат в един от два обхвата. **Клиент:** IPv4 адрес или IPv6 /64 (в режим на прокси адресът идва от `X-Forwarded-For`, изпратен от адрес от `TRUSTED_PROXIES`); някои лимити броят и всяка IPv6 /48 като цяло, в допълнение към всяка от нейните /64 мрежи. **Играч:** акаунтът с активен вход, независимо от адреса му; лимит, броен по играч, при крайна точка с незадължителна сесия се брои по клиент за заявка без токен.

    Всеки лимит е кофа с токени (token bucket), която побира `limit` заявки и се пълни непрекъснато със скорост `limit / window`; `retryAfter` е времето до следващия токен. Лимитите, отбелязани като *споделен*, се броят и в плъзгащ се прозорец със същата дължина, така че никой прозорец да не съдържа много повече от `limit` заявки. Всеки лимит се брои за целия сървър. Когато един от лимитите на крайна точка откаже заявка, токените, които другите ѝ лимити са взели за тази заявка, се връщат. Отказът е отговор 429 `rate_limited` с `retryAfter` и `Retry-After`.

    - **Слой по адрес.** Всяка заявка (всеки път и метод, включително крайните точки за проверка на състоянието и преминаванията към WebSocket) първо взема един токен от бюджета на своя клиент: `HTTP_RATE_PER_IP` (600) в минута с допустим пик от половин минута и `HTTP_RATE_PER_PREFIX` (4 x `HTTP_RATE_PER_IP`) за IPv6 /48. Клиентът може също да има най-много `IP_MAX_INFLIGHT` (32 x `WORKERS`) заявки в изпълнение (над това – `retryAfter` 1). Клиент, който продължава след отказите си, се блокира: `ABUSE_BLOCK_REFUSALS_PER_MIN` (600) отказа за една минута го блокират за 1 минута, след това за 4, 16 и 60 минути при всяко ново блокиране в рамките на 6 часа. Отказ от лимитите `auth`, `auth_*` и `reauth` се брои за 5 отказа; отказите от лимити, броени по играч, никога не се броят. Адресите в `ABUSE_EXEMPT` пропускат този слой, но не и бюджета на акаунта или лимитите на крайните точки.
    - **Бюджет на акаунта.** Всяка заявка, която носи валиден токен на сесия, се брои и срещу своя акаунт: `USER_RATE_PER_MIN` (120) в минута за всички крайни точки заедно, независимо от адреса, с допустим пик от половин минута.
    - **Лимити на крайните точки:**

    | Лимит | По подразбиране | Брои се по | Крайни точки |
    |---|---|---|---|
    | `auth` | `AUTH_RATE_PER_IP` (20) / 10 мин, споделен | клиент и `AUTH_RATE_PER_PREFIX` (5 x `AUTH_RATE_PER_IP`) на 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) / час, споделен | клиент, 3 пъти повече на IPv6 /48 | `POST /auth/register` |
    | `auth_mail` | `AUTH_MAIL_PER_HOUR` (10) / час, споделен | клиент, 3 пъти повече на IPv6 /48 | `POST /auth/verify-email/resend` |
    | `auth_forgot` | `AUTH_FORGOT_PER_HOUR` (3) / час, споделен | клиент, 3 пъти повече на IPv6 /48 | `POST /auth/password/forgot` |
    | `auth_forgot_day` | `AUTH_FORGOT_PER_DAY` (10) / 24 часа, споделен | клиент, 3 пъти повече на IPv6 /48 | `POST /auth/password/forgot` |
    | `auth_reset` | `AUTH_RESET_PER_HOUR` (10) / час, споделен | клиент, 3 пъти повече на IPv6 /48 | `POST /auth/password/reset`, `POST /reset-password` |
    | `reauth` | числата на `auth`, в собствена кофа, споделен | клиент и 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 мин, споделен | играч | същите крайни точки като `reauth` |
    | `account` | 60 / мин | играч | `GET /account/me`, `PUT /account/preferences` |
    | `account_games` | 60 / мин | играч | `GET /account/games` |
    | `account_export` | 5 / час, споделен | играч | `POST /account/export` (проверява се преди `reauth`; всеки опит се брои) |
    | `sessions` | 60 / мин | играч | `POST /auth/logout`, `/auth/logout-all`, `GET /auth/sessions`, `DELETE /auth/sessions/{id}` |
    | `public_read` | 60 / мин | играч (клиент, ако няма токен) | `GET /players/{username}`, `/players/{username}/games`, `/games/{id}`, `/games/{id}/pgn` (една кофа за четирите) |
    | `gif` | 30 / мин | играч | `GET /games/{id}/gif`, `POST /gif` (една кофа за двете) |
    | `gif_user_min`, `gif_user_hour` | `GIF_USER_RENDERS_PER_MIN` (4) / мин и `GIF_USER_RENDERS_PER_HOUR` (30) / час, споделени | играч | същите две, само когато GIF трябва да бъде създаден (не е от кеша) |
    | `gif_ip_min`, `gif_ip_hour` | `GIF_IP_RENDERS_PER_MIN` (12) / мин и `GIF_IP_RENDERS_PER_HOUR` (120) / час, споделени | клиент (всичките му акаунти заедно), 3 пъти повече на IPv6 /48 | същите, както по-горе |
    | `reports` | 30 / час | играч | `POST /reports` |
    | `sso_start` | 30 / 10 мин, споделен | клиент и 90 на IPv6 /48 | `POST /auth/sso/google/start`, `POST /auth/sso/google/web/start` (заедно) |
    | `sso_finish` | 30 / мин | клиент | `POST /auth/sso/google/finish` |
    | `page` | 60 / мин | клиент | `GET /verify-email`, `/reset-password`, `/confirm-email-change` |

    `GET /info` и `GET /leaderboard` нямат собствен лимит: важи само слоят по адрес. Крайна точка с няколко лимита ги проверява в реда, посочен в описанието ѝ.

    Сървърът обработва заявката в следния ред: слоят по адрес, след това крайните точки за проверка на състоянието; съпоставянето с крайна точка; проверките, че сървърът не изостава сериозно (503 `server_busy`): твърде много по-ранни заявки, които още се изпълняват след изтичането на времето си, и за `POST`, `PUT`, `PATCH` или `DELETE` – натрупаните чакащи записи в базата данни; удостоверяването, до 5 секунди (над това – 503 `server_busy`); бюджетът на акаунта, когато заявката носи сесия; лимитите на крайната точка; тялото; самата крайна точка (крайните точки за GIF вземат лимитите си за рендериране само когато имат GIF за създаване). Така заявка, отказана заради токена си, не изразходва нито един от токените на крайната точка, а заявка с невалидно тяло ги изразходва.

    Други ограничения, които прилагат самите крайни точки:

    - **Неуспешни опити за вход с едно име за вход** (потребителско име или имейл): от `AUTH_FAILURES_PER_ACCOUNT` (5) неуспеха нататък всеки опит трябва да чака двойно по-дълго от предишния (2 s, 4 s и т.н., до 15 минути): 429 `too_many_attempts` с `retryAfter`. Броячът се нулира след един час без неуспехи.
    - **Неуспешни втори фактори при вход:** същото правило от 5-ия грешен код на акаунта. Една стъпка на вход приема най-много 5 грешни кода.
    - **Кодове за втори фактор на един акаунт:** най-много `AUTH_MFA_PER_ACCOUNT` (10) кода (от приложение за удостоверяване или за възстановяване, верни или грешни) за 15 минути, от всеки адрес, при вход и при повторни удостоверявания; над това – 429 `too_many_attempts` преди проверката на кода, така че кодът за възстановяване не се изразходва.
    - **Неуспешни повторни удостоверявания на акаунт** (грешна парола или код): същото правило (429 `too_many_attempts`), общо за всички крайни точки с повторно удостоверяване.
    - **Имейли:** един имейл за потвърждение или нулиране на адрес на всеки 5 минути (отговорът остава същият); едно известие „някой се опита да използва адреса ви“ на адрес на час.
    - **Сигнали:** `REPORTS_PER_DAY` (5) на играч за 24 часа: 429 `report_limit`.

    ## Доказателство за работа

    `POST /auth/register` винаги изисква доказателство за работа, когато `POW_REGISTER_BITS` е над 0 (18 по подразбиране; `GET /info` го дава като `pow.register`). `POST /auth/login` и `POST /auth/sso/google/link` (един вид предизвикателство и за двете) го изискват само в продължение на 5 минути, след като сървърът забележи вълна от неуспешни опити за вход (`POW_LOGIN_TRIGGER_PER_MIN`, след това `POW_LOGIN_BITS`); клиентът разбира това от отговора.

    1. На заявката без доказателство (или с отказано доказателство) отговорът е 428 `pow_required` с `reason` и предизвикателство `pow` `{ challenge, bits, expiresAt }`.
    2. Намерете nonce: десетичен низ от най-много 20 цифри, такъв че `SHA-256(challenge + ":" + nonce)` да започва с `bits` нулеви бита (първо най-старшият бит на първия байт). 18 бита изискват средно около 260 000 хеширания.
    3. Изпратете същата заявка отново с `"pow": { "challenge": "...", "nonce": "123456" }` в тялото.

    Предизвикателството е валидно 2 минути и само веднъж, за една крайна точка и една клиентска мрежа (IPv4 адрес или IPv6 /64). То е подписано, така че сървърът не пази нищо за него, докато не се върне. `reason` казва защо доказателството е отказано: `required`, `malformed`, `signature`, `endpoint`, `network`, `expired`, `bits`, `work` или `replayed`.

    ## Повторно удостоверяване

    Промените в акаунта изискват паролата отново, а когато двуфакторното удостоверяване е включено – и втори фактор:

    - само паролата: `POST /account/password` и `POST /account/mfa/totp/setup`;
    - паролата и код от приложението за удостоверяване, като код за възстановяване се отказва: `POST /account/mfa/recovery-codes`;
    - паролата и код от приложението за удостоверяване или код за възстановяване: `POST /account/mfa/totp/disable`, `POST /account/email`, `POST /account/export` и `POST /account/delete`.

    В телата `code` съдържа 6-цифрен код от приложението за удостоверяване, а `recoveryCode` – код за възстановяване (`xxxx-xxxx-xx`; главните и малките букви, интервалите и тиретата нямат значение). Код за възстановяване може да се изпрати и в `code` там, където кодовете за възстановяване се приемат. Всеки код работи веднъж: използван код от приложението за удостоверяване се отказва до следващата 30-секундна стъпка, а кодът за възстановяване изчезва, след като бъде използван.

    | Статус | `error` | Кога |
    |---|---|---|
    | 403 | `invalid_password` | Грешна парола или паролата е нулирана или сменена, докато заявката се е изпълнявала (нищо не е променено). |
    | 403 | `mfa_code_required` | Двуфакторното удостоверяване е включено и не е изпратен нито `code`, нито `recoveryCode`. |
    | 403 | `invalid_code` | Грешен или вече използван код. |
    | 400 | `password_not_set` | Акаунт само с вход с Google още няма парола („Забравена парола?“ задава такава). |
    | 429 | `too_many_attempts` | Твърде много неуспехи или твърде много опитани кодове за този акаунт. |
    | 503 / 429 | `server_busy` / `rate_limited` | Опашката за хеширане на пароли е претоварена. |

    Неуспешните пароли и кодове се броят в брояча на неуспехите на акаунта и се записват като събития, свързани със сигурността.

    ## Порт за метрики

    Освен на порта на API сървърът отговаря с обикновен HTTP на порта за метрики (`METRICS_PORT`, 9464, свързан към `METRICS_BIND`, по подразбиране 127.0.0.1; не го правете публично достъпен). Той не е част от този API: `GET /healthz` отговаря с `ok`; `GET /readyz` отговаря с `ready`, след като стартирането приключи (всеки шард е възпроизвел журнала си и слушателите са свързани към портовете си) и докато не започне спирането, иначе с 503 `not ready`; `GET /metrics` предоставя метриките за Prometheus и, когато е зададен `METRICS_TOKEN`, изисква `Authorization: Bearer <token>` с точно този токен.
  contact:
    name: Scacelith
    url: https://github.com/DarkCenobyte/scacelith-chess-server
  license:
    name: GPL-3.0-or-later
    identifier: GPL-3.0-or-later
externalDocs:
  description: Изходният код на сървъра Scacelith и справочната му документация (API.md, PROTOCOL.md, CONFIG.md).
  url: https://github.com/DarkCenobyte/scacelith-chess-server
servers:
  - url: https://caissa.scacelith.com/api/v1
    description: Официалният сървър.
  - url: https://{host}:{port}/api/v1
    description: Всеки сървър Scacelith, например сървър на общността.
    variables:
      host:
        default: caissa.scacelith.com
        description: Публичното име на хоста на сървъра (`SERVER_PUBLIC_HOST`).
      port:
        default: "443"
        description: Публичният порт на API (`PUBLIC_API_PORT`, иначе `API_PORT`).
tags:
  - name: server-info
    x-displayName: Информация за сървъра
    description: Какво трябва да знае клиентът, преди да влезе или да се свърже.
  - name: health
    x-displayName: Проверка на състоянието
    description: |-
      Дали сървърът работи (liveness) и дали е готов (readiness), за мониторинг. Тези крайни точки отговарят на порта на API преди удостоверяването и всички лимити на крайните точки, но като всяка заявка вземат един токен от слоя по адрес; хост за мониторинг може да бъде посочен в `ABUSE_EXEMPT`. За всяка крайна точка работят и двата пътя: в корена на сървъра и под `/api/v1`.

      Портът за метрики има собствени крайни точки за проверка на състоянието, описани във въведението.
  - name: auth
    x-displayName: Регистрация и вход
    description: |-
      Създаване на акаунт, вход с парола (и втори фактор), имейли за потвърждение и за нулиране на паролата.

      `POST /auth/login`, `POST /auth/login/mfa`, `POST /auth/sso/google/finish`, `POST /auth/sso/google/link` и `POST /auth/sso/complete` отговарят със сесия `{ token, expiresAt, user }` (или, при първите от тях, с втора стъпка).
  - name: google-sign-in
    x-displayName: Вход с Google
    description: |-
      Предлага се, когато `GET /info` съобщава `sso.google: true`; иначе всяка крайна точка по-долу отговаря с 404 `sso_disabled`. Уебсайт влиза по същия начин със собствен клиент, когато `sso.googleWeb` е true (по-долу, **На уебсайт**). Играта влиза през системния браузър чрез потока за инсталирани приложения от RFC 8252 (клиент от тип „Desktop app“): Google връща браузъра към слушател на играта на `127.0.0.1`, никога към този сървър, и играта никога не вижда идентификационни данни за Google.

      1. Клиентът слуша на `127.0.0.1:0` (системата избира порта) и създава PKCE двойка: `codeVerifier` от 43 до 128 знака `[A-Za-z0-9._~-]` и `codeChallenge = BASE64URL(SHA-256(codeVerifier))`, който има 43 знака и няма допълване (padding).
      2. `POST /auth/sso/google/start` с предизвикателството и порта връща URL адреса на Google и `state` на опита. Клиентът проверява URL адреса (вж. по-долу) и го отваря в браузъра.
      3. Google изпраща браузъра към `http://127.0.0.1:<port>/oauth2/google/<tag>?code=...&state=...`. Клиентът приема само `state` от отговора на началната заявка и не изпраща нищо след пренасочване с `error=`.
      4. `POST /auth/sso/google/finish` с идентификатора на опита, `codeVerifier`, `state` и `code` (и `iss`, когато Google го е изпратил).
      5. Отговорът е сесия, стъпка на двуфакторно удостоверяване (продължете с `POST /auth/login/mfa`), `needsUsername` за нов играч (продължете с `POST /auth/sso/complete`) или `needsPassword`, когато акаунт с парола използва адреса (продължете с `POST /auth/sso/google/link`).

      **Етикетът на произхода.** URI адресът за пренасочване носи етикет на сървъра, който играчът е добавил, така че играта да отказва URL адрес, който друг сървър е получил за своите играчи. Произходът е хостът с малки букви (IPv6 литерал в квадратни скоби), `:` и десетичният порт на API, винаги изписан, включително 443: сървърът взема `SERVER_PUBLIC_HOST` и своя публичен порт на API, а играта – адреса, към който е свързана. Етикетът е първите 22 знака от base64url (без допълване) на SHA-256(UTF-8 `"scacelith-sso-origin-v1\n"` + произход). URI адресът за пренасочване е `"http://127.0.0.1:" + port + "/oauth2/google/" + tag`: винаги IPv4 литералът и портът на слушателя на играта (от 1024 до 65535). Сървърът никога не взема URI, хост или път от клиента.

      | Произход | Етикет |
      |---|---|
      | `play.scacelith.example:443` | `IhcScoV7eDOzTEcSnqPUPt` |
      | `localhost:8443` | `TFGx7zQ_8QlGZW5zpqznCr` |
      | `[::1]:8443` | `XToJm0DG5PjciEVmZa9Cho` |
      | `127.0.0.1:50443` | `3r653wM5ZjYsHcAJljmCwY` |

      Затова входът с Google работи само за играчи, които са добавили сървъра точно под `SERVER_PUBLIC_HOST` и неговия публичен порт на API.

      **Какво проверява играта, преди да отвори браузъра.** `authUrl` започва точно с `https://accounts.google.com/o/oauth2/v2/auth?`, състои се от печатими ASCII знаци и е под 4096 знака, а параметрите на заявката в него съдържат точно един `response_type=code`, точно един `redirect_uri`, равен на URI адреса, който играта изчислява от своя порт и своя етикет на произхода, точно един `state`, равен на `state` от отговора, `code_challenge_method=S256` и `code_challenge` от 43 знака. Иначе играта спира слушателя си и не отваря нищо. Тя изпраща `finish` и `link` само към сървъра, който е отговорил на `start`.

      **На уебсайт.** Уебсайт (официалният: <https://scacelith.com/>) влиза с клиент от тип „Web application“ от същия проект в Google (`GOOGLE_WEB_CLIENT_ID`). Google връща браузъра към една страница на сайта, конфигурирана от оператора (`GOOGLE_WEB_REDIRECT_URI`, посочена в „Authorized redirect URIs“ на клиента), и тази страница изпраща кода към `finish`; и тук сървърът не взема URI адрес за пренасочване от клиента.

      1. Страницата създава PKCE двойка както по-горе, пази верификатора в текущия раздел на браузъра (`sessionStorage`) и изпраща `POST /auth/sso/google/web/start` с предизвикателството.
      2. Тя проверява `authUrl` както играта, с тази разлика, че неговият `redirect_uri` трябва да е страница от собствения ѝ произход, след което преминава към този адрес. Не се зарежда скрипт на Google.
      3. Google изпраща браузъра към `GOOGLE_WEB_REDIRECT_URI?code=...&state=...`. Страницата приема само `state` от отговора на началната заявка и не изпраща нищо след пренасочване с `error=`.
      4. `finish`, `link` и `complete` следват както при играта. Опитът помни, че е започнат от уебсайт: кодът се обменя с клиента и страницата на уебсайта, а ID токенът трябва да посочва този клиент (код или ID токен на клиента на играта се проваля с 502 `sso_failed`, и обратното).

      Един акаунт в Google води до един и същ акаунт в Scacelith и от играта, и от уебсайта: свързването съхранява субекта на Google (`sub`), който идентифицира акаунта в Google независимо от клиента. Произходът на уебсайта трябва да е в `CORS_ORIGINS` (иначе `check-config` предупреждава).

      **До кой акаунт води входът с Google:** до акаунта, който вече е свързан с този акаунт в Google; иначе до активен акаунт с адреса, потвърден от Google (ако има парола, `finish` отговаря с `needsPassword` и свързването се съхранява едва след като паролата му, а след нея и вторият му фактор, ако е включен, бъдат приети; ако няма парола – 409 `sso_account_exists`); иначе до нов акаунт (`needsUsername`). Акаунт в Google никога не се свързва със съществуващ акаунт само по адреса му. Адресът на акаунта получава имейл, когато входът с Google създаде акаунт и когато бъде добавен към съществуващ акаунт.
  - name: sessions
    x-displayName: Сесии
    description: Устройствата с активен вход в акаунта и излизането.
  - name: account
    x-displayName: Акаунт
    description: Изгледът на акаунта, предпочитанията му и паролата му.
  - name: two-step-verification
    x-displayName: Двуфакторно удостоверяване
    description: Приложения за удостоверяване (TOTP, RFC 6238) с SHA-1, 6 цифри, 30-секундни стъпки и толеранс от една стъпка в двете посоки, плюс еднократни кодове за възстановяване.
  - name: email-change
    x-displayName: Смяна на имейл адреса
    description: Смяна на адреса на акаунта, потвърдена с връзка, изпратена до новия адрес.
  - name: data-export
    x-displayName: Експорт на данните
    description: Всичко, което сървърът пази за акаунта, в един JSON файл.
  - name: account-deletion
    x-displayName: Изтриване на акаунта
    description: Окончателно изтриване на акаунта.
  - name: game-history
    x-displayName: История на партиите
    description: Собствените партии на играча с активен вход, с филтри и странициране.
  - name: games
    x-displayName: Партии и PGN
    description: |-
      Записи на партиите с ходовете и часовниците им, както и PGN файловете им.

      **Кодове на партиите.** `status`: 1 `WhiteWins`, 2 `BlackWins`, 3 `Draw`, 4 `Aborted` (съхраняват се само завършени партии). `result`: `1-0`, `0-1`, `1/2-1/2` или `*` (анулирана). `reason` – с името си (`termination`) и думите, с които завършва текстът на ходовете в PGN файла; кодовете 7, 14 и 21 са реми (противникът на играча, чието време е изтекло, който се е предал или е напуснал, не е можел да даде мат: FIDE 6.9 и 5.1.2; правилото е дадено в `DESIGN.md` 6.3), а сървърът никога не завършва партия с кодовете 4 и 13 (те принадлежат към общия списък с причини):

      | Код | `termination` | Думи в PGN |
      |---|---|---|
      | 1 | `Checkmate` | Checkmate (мат) |
      | 2 | `Resignation` | Resignation (предаване) |
      | 3 | `Timeout` | Loss on time (загуба по време) |
      | 4 | `IllegalMoves` | Second illegal move (forfeit) (втори неправилен ход, служебна загуба) |
      | 5 | `Stalemate` | Stalemate (пат) |
      | 6 | `InsufficientMaterial` | Dead position (insufficient material) (мъртва позиция, недостатъчен материал) |
      | 7 | `TimeoutVsInsufficient` | Flag fall, but the opponent cannot checkmate (падна флагчето, но противникът не може да даде мат) |
      | 8 | `FivefoldRepetition` | Fivefold repetition (петкратно повторение) |
      | 9 | `SeventyFiveMoves` | 75-move rule (правилото на 75-те хода) |
      | 10 | `ThreefoldClaim` | Threefold repetition (claimed) (трикратно повторение, по искане) |
      | 11 | `FiftyMoveClaim` | 50-move rule (claimed) (правилото на 50-те хода, по искане) |
      | 12 | `Agreement` | Draw by agreement (реми по взаимно съгласие) |
      | 13 | `IllegalMovesVsInsufficient` | Second illegal move, but the opponent cannot checkmate (втори неправилен ход, но противникът не може да даде мат) |
      | 14 | `ResignationVsInsufficient` | Resignation, but the opponent cannot checkmate (предаване, но противникът не може да даде мат) |
      | 20 | `Abandonment` | Abandoned (disconnected for too long) (напускане на партията, твърде дълго без връзка) |
      | 21 | `AbandonmentVsInsufficient` | Abandoned, but the opponent cannot checkmate (напускане на партията, но противникът не може да даде мат) |
      | 22 | `Aborted` | Game aborted (анулирана партия) |
      | 23 | `NoShow` | Aborted: first move not played in time (анулирана: първият ход не е изигран навреме) |
      | 24 | `Forfeit` | Forfeit (fair play violation) (служебна загуба, нарушение на честната игра) |
      | 25 | `ServerAborted` | Aborted by the server (анулирана от сървъра) |
      | 26 | `BothDisconnected` | Aborted: both players disconnected (анулирана: и двамата играчи прекъснаха връзката) |

      Код 14 е нов в минорната версия 1 на протокола: клиент, написан преди нея, трябва да показва непознат код като обикновен край на партията с нейния `result`.
  - name: gifs
    x-displayName: Анимирани GIF
    description: |-
      Партия като анимиран GIF, за запазване или споделяне: дъската, гледана отгоре, по един кадър на позиция от началната до крайната позиция, имената и рейтингите на играчите над дъската, последният ход под нея, а на последния кадър – резултатът и начинът, по който е завършила партията.

      **Цена, кеш и квоти.** GIF се създава в собствена нишка за рендериране, никога в нишките, които изпълняват партиите, и с най-нисък приоритет на процесора: `GIF_THREADS` нишки (по подразбиране `WORKERS`), стартирани с първия GIF и спрени след минута без нито един. До `GIF_QUEUE_MAX` (4 x `WORKERS`) GIF чакат за нишка, всеки най-много `GIF_QUEUE_TIMEOUT_MS` (10 s); едно рендериране може да трае до `GIF_RENDER_TIMEOUT_MS` (30 s). Сървърът пази създадените GIF в кеш от `GIF_CACHE_MB` MB (32 x `WORKERS`), като най-отдавна използваните се премахват първи; GIF от кеша или такъв, който в момента се създава за друга заявка, не струва рендериране. Ключът на кеша обхваща всичко, което променя картината, включително имената.

      Всяка заявка се брои в `gif` (30 в минута на играч за двете крайни точки). GIF, който трябва да бъде създаден, се брои и в лимитите за рендериране: на играч `GIF_USER_RENDERS_PER_MIN` (4) в минута и `GIF_USER_RENDERS_PER_HOUR` (30) в час; на клиент, за всичките му акаунти заедно, `GIF_IP_RENDERS_PER_MIN` (12) в минута и `GIF_IP_RENDERS_PER_HOUR` (120) в час, и 3 пъти повече на IPv6 /48. Клиентът трябва да пази изтегления файл, вместо да го иска отново, и да изчаква `retryAfter` след 429 или 503.

      Размери: `small` (полета от 32 px: 284 x 350 пиксела), `medium` (48 px: 424 x 515), `large` (72 px: 628 x 762); без координати – 268 x 342, 400 x 503 и 600 x 748. Началната позиция се показва поне 1 s (или колкото е забавянето, когато то е по-дълго), крайната позиция – 3 s, и GIF се повтаря в цикъл; след първия кадър се съхранява само частта от картината, която се променя. Партия от 40 хода заема около 135, 205 и 325 KiB (small, medium, large), а партия от 150 хода – 0,5; 0,8 и 1,2 MiB.
  - name: players
    x-displayName: Играчи
    description: Публични профили и последни партии. Само публични данни – никога имейл адрес, сесия, санкция или ниво на доверие. Изтрит акаунт няма профил.
  - name: leaderboard
    x-displayName: Класация
    description: Най-добрите играчи във всяка официална категория.
  - name: reports
    x-displayName: Сигнали
    description: Подаване на сигнал до модераторите за противника от скорошна партия.
  - name: pages
    x-displayName: HTML страници
    description: |-
      Страници за браузър, които се отварят от връзките в имейлите. Връзките им сочат към `https://<SERVER_PUBLIC_HOST>` (с `:<PUBLIC_API_PORT>`, когато той не е 443), извън `/api/v1`.

      - Страниците не изпълняват JavaScript и не зареждат външни ресурси. Те се предоставят с `Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'`.
      - `GET` само показва бутон или формуляр, така че скенер на пощата, който отвори връзката, да не я изразходва. Промяната става при `POST`: формуляр, изпратен като `application/x-www-form-urlencoded` (приема се и JSON тяло), с токена на връзката в скрито поле. Поле, подадено два пъти, се отказва.
      - Грешките (лимити на честотата, невалидни полета) също са HTML страници, със заглавие „Request refused“ (заявката е отказана) или, от 500 нагоре, „Server error“ (грешка на сървъра). Само неуспехите, установени преди съпоставянето със страница (твърде дълга цел на заявката или такава с грешен формат, слоят по адрес), получават отговор в JSON.
x-tagGroups:
  - name: server
    x-displayName: Сървър
    tags:
      - server-info
      - health
  - name: accounts
    x-displayName: Акаунти
    tags:
      - auth
      - google-sign-in
      - sessions
      - account
      - two-step-verification
      - email-change
      - data-export
      - account-deletion
  - name: games
    x-displayName: Партии
    tags:
      - game-history
      - games
      - gifs
  - name: community
    x-displayName: Играчи и сигнали
    tags:
      - players
      - leaderboard
      - reports
  - name: browser-pages
    x-displayName: Страници за браузъра
    tags:
      - pages
paths:
  /info:
    get:
      operationId: getServerInfo
      tags:
        - server-info
      summary: Получаване на името, версиите, портовете и правилата за регистрация на сървъра
      description: |-
        Каквото е нужно на клиента, преди да влезе или да се свърже: името и идентификаторът на сървъра, версиите на протокола WebSocket и къде се намира WebSocket, правилата за регистрация, официалните контроли на времето и ограниченията, които клиентът може да провери, преди да изпрати формуляр.

        **Лимити:** само слоят по адрес.
      security: []
      responses:
        '200':
          description: Описанието на сървъра.
          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`: слоят по адрес.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /auth/register:
    post:
      operationId: registerAccount
      tags:
        - auth
      summary: Създаване на акаунт
      description: |-
        Създава акаунт: след като имейл адресът му бъде потвърден чрез изпратената до него връзка, или веднага, ако няма потвърждение по имейл.

        **С потвърждение по имейл** (`REQUIRE_EMAIL_VERIFICATION`, по подразбиране): 202 `verification_sent`. Акаунт все още не съществува: регистрацията чака 24 часа и междувременно резервира потребителското си име. До адреса се изпраща връзка, валидна през тези 24 часа, най-много една на адрес на всеки 5 минути (`POST /auth/verify-email/resend` изпраща нова); акаунтът се създава с вече потвърден адрес, когато връзката бъде използвана (бутонът на страницата `/verify-email`), и тогава играчът може да влезе. Дотогава при опит за вход с това потребителско име отговорът е `invalid_credentials`, както при непознат акаунт, а публичният профил не съществува. Нова регистрация със същия адрес заменя чакащата. Отговорът е същият, когато друг акаунт вече използва адреса: тогава не се изпраща връзка, а собственикът му получава известие (най-много едно на час), и потребителското име се резервира по същия начин, така че нищо да не издава дали адресът има акаунт. Регистрация, чиято връзка не е използвана, отпада след 24 часа и потребителското ѝ име отново става свободно.

        **Без потвърждение по имейл** (`REQUIRE_EMAIL_VERIFICATION=false`): 201 `ready`, акаунтът се създава веднага и с него може да се влезе.

        **Правила.** `username`: от `USERNAME_MIN` до `USERNAME_MAX` знака (от 3 до 20), букви, цифри, `_` и `-`, като започва с буква или цифра (`GET /info` ги дава като `limits`); запазените имена (`admin`, `moderator`, `deleted`…) и някои префикси се отказват; уникално без значение от главните и малките букви. `email`: без интервали в началото и края и съхраняван с малки букви, само ASCII, с домейн, съдържащ точка. `password`: поне `PASSWORD_MIN_LENGTH` (10) знака и най-много 256 байта в UTF-8; не трябва да съдържа потребителското име или локалната част на имейла и не трябва да е често срещана парола.

        **Грешки**, проверявани в този ред: 403 `registration_closed`; 400 `invalid_username`; 400 `invalid_email`; 400 `weak_password`; 409 `username_taken`; 428 `pow_required`; грешките на опашката за хеширане на пароли; 409 `email_taken` (само без потвърждение по имейл).

        **Лимити:** `auth`, след това `auth_register` (10 регистрации на час на клиент). **Доказателство за работа:** винаги, когато `POW_REGISTER_BITS` е над 0.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
            examples:
              first:
                summary: Първи опит, без доказателство за работа
                value:
                  username: alice
                  email: alice@example.org
                  password: correct horse battery
              withPow:
                summary: Повторно изпращане с доказателството за работа
                value:
                  username: alice
                  email: alice@example.org
                  password: correct horse battery
                  pow:
                    challenge: eyJ2IjoxLCJlIjoicmVnaXN0ZXIiLCJiIjoxOCwieCI6MTc5MDg4Mjk5MTIwMH0.6Ku_D7kw3S69OYxj-9KxsXszRCwz6ECnOQ-B6-v2erM
                    nonce: '123456'
      responses:
        '201':
          description: Акаунтът е създаден и с него може да се влезе (на този сървър няма потвърждение по имейл).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    const: ready
              example:
                status: ready
        '202':
          description: Регистрацията чака връзката си за потвърждение (или адресът вече има акаунт; отговорът не издава кое от двете).
          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` (с `field`), `invalid_json`, `invalid_username`, `invalid_email` или `weak_password` с `reason`: `too_short`, `too_long`, `contains_username`, `contains_email` или `too_common`.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`registration_closed`: регистрацията на този сървър е затворена (`GET /info` съобщава `registration: closed`).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`username_taken`: потребителското име принадлежи на акаунт или е резервирано от чакаща регистрация с друг адрес. `email_taken`: друг акаунт използва адреса (само без потвърждение по имейл).'
        '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`: лимитът `auth` или `auth_register`, или опашката за хеширане на пароли.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (опашката за хеширане на пароли или базата данни остана заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /auth/login:
    post:
      operationId: logIn
      tags:
        - auth
      summary: Вход с парола
      description: |-
        Извършва вход с потребителско име или имейл адрес и парола. Възможни са два отговора, и двата със статус 200: сесия или, когато двуфакторното удостоверяване е включено, втора стъпка, която трябва да бъде завършена до 5 минути с `POST /auth/login/mfa`.

        Непознат акаунт, грешна парола и акаунт без парола получават един и същ отговор след едно и също време: 401 `invalid_credentials`. Проверките на акаунта (`banned`, `email_unverified`) идват едва след вярна парола. От `AUTH_FAILURES_PER_ACCOUNT` (5) неуспеха с едно име за вход нататък всеки опит трябва да изчака (429 `too_many_attempts`).

        **Лимит:** `auth`. **Доказателство за работа:** само по време на вълна от неуспешни опити за вход (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: Сесия или втората стъпка на вход с двуфакторно удостоверяване.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginAnswer'
              examples:
                session:
                  summary: Успешен вход
                  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: Двуфакторното удостоверяване е включено
                  value:
                    mfaRequired: true
                    mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                    expiresIn: 300
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_credentials`: непознат акаунт, грешна парола или акаунт без парола.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: 'Само след вярна парола: `banned` с `until` (мс от Unix епохата, `null` при постоянно блокиране) или `email_unverified` (по-стар акаунт, чийто адрес още не е потвърден).'
        '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` (забавянето след неуспешни опити за това име за вход) или `rate_limited` (лимитът `auth`, опашката за хеширане на пароли).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (опашката за хеширане на пароли или базата данни остана заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /auth/login/mfa:
    post:
      operationId: logInWithSecondFactor
      tags:
        - auth
      summary: Завършване на вход с втори фактор
      description: |-
        Втората стъпка на вход с двуфакторно удостоверяване, след като `POST /auth/login` или вход с Google (`POST /auth/sso/google/finish` или `POST /auth/sso/google/link`) е отговорил с `mfaRequired`. Изпратете `code` (6-цифрен код от приложението за удостоверяване или код за възстановяване) или `recoveryCode`. Код за възстановяване, използван тук, се изразходва.

        Една стъпка приема най-много 5 грешни кода; стъпката приключва и когато паролата бъде нулирана или сменена. След стъпката с паролата при свързване с Google свързването се съхранява само ако кодът бъде приет тук.

        **Лимити:** `auth` и най-много `AUTH_MFA_PER_ACCOUNT` (10) кода за 15 минути за акаунта, от всеки адрес; над това кодът не се проверява, така че кодът за възстановяване не се изразходва.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MfaLoginRequest'
            examples:
              authenticator:
                summary: Код от приложението за удостоверяване
                value:
                  mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                  code: '123456'
              recovery:
                summary: Код за възстановяване
                value:
                  mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                  recoveryCode: j7v5-3ezx-zn
      responses:
        '200':
          description: Успешен вход.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionAnswer'
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_request`: не е изпратен нито `code`, нито `recoveryCode`, или тялото нарушава схемата; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_mfa_token`: стъпката е изтекла, вече е използвана или е приключила след 5 грешни кода, или паролата е нулирана или сменена след първата стъпка (влезте отново). `invalid_code`: грешен или вече използван код.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`banned` (с `until`), `email_unverified`.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_already_linked`: само след стъпката с паролата при свързване с Google – междувременно акаунтът в Google е свързан с друг акаунт.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: само след стъпката с паролата при свързване с Google – междувременно акаунтът се е променил; започнете входа отначало.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts`: забавянето след неуспешни опити на акаунта или неговите `AUTH_MFA_PER_ACCOUNT` кода за последните 15 минути са изчерпани. `rate_limited`: лимитът `auth`.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` с `retryAfter: 1`: базата данни остана заключена (след стъпка на свързване с Google нищо не е свързано: започнете входа отначало). `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /auth/logout:
    post:
      operationId: logOut
      tags:
        - sessions
      summary: Изход от тази сесия
      description: |-
        Прекратява сесията на използвания токен. WebSocket връзката, отворена с него, се затваря. Без тяло (или `{}`).

        **Лимит:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Успешен изход.
          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`: тяло, различно от `{}`; `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`: лимитът `sessions` или бюджетът на акаунта.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (базата данни остана заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /auth/logout-all:
    post:
      operationId: logOutEverywhere
      tags:
        - sessions
      summary: Изход от всички сесии
      description: |-
        Прекратява всички сесии на акаунта, включително тази. WebSocket връзките, отворени с тях, се затварят. Без тяло (или `{}`).

        **Лимит:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Успешен изход от всички сесии.
          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`: тяло, различно от `{}`; `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`: лимитът `sessions` или бюджетът на акаунта.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (базата данни остана заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /auth/verify-email/resend:
    post:
      operationId: resendVerificationEmail
      tags:
        - auth
      summary: Повторно изпращане на връзката за потвърждение
      description: |-
        Изпраща отново връзката за потвърждение на имейла. Отговорът е 202 `accepted` независимо от адреса, така че никога да не издава дали акаунт или регистрация го използва.

        Заявката има действие най-много веднъж на 5 минути за даден адрес. Чакаща регистрация с този адрес получава отново своите 24 часа, независимо дали друг акаунт използва адреса, така че потребителското ѝ име да остава резервирано еднакво дълго и в двата случая. Връзка се изпраща само за тази регистрация, когато адресът няма акаунт (нова връзка, валидна 24 часа, заменя предишната), или за активен непотвърден акаунт с този адрес.

        **Лимити:** `auth`, след това `auth_mail` (10 на час на клиент).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmailRequest'
            example:
              email: alice@example.org
      responses:
        '202':
          description: Заявката е приета (независимо от адреса).
          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`: лимитът `auth` или `auth_mail` (независимо от адреса).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` с `retryAfter: 1`: базата данни остана заключена, докато се търсеше акаунтът или се съхраняваше връзката на непотвърден акаунт (подновяването на чакаща регистрация се прави по възможност и никога не проваля заявката); 5-те минути на адреса въпреки това започват да текат, така че нова заявка в рамките им не прави нищо. `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /auth/password/forgot:
    post:
      operationId: requestPasswordReset
      tags:
        - auth
      summary: Изпращане на връзка за нулиране на паролата
      description: |-
        Изпраща връзка за нулиране на паролата, валидна един час, която отваря страницата `/reset-password`. Отговорът е 202 `accepted` независимо от адреса. Връзка се изпраща само до активен акаунт, най-много веднъж на 5 минути за даден адрес. Акаунт само с вход с Google задава първата си парола по този начин.

        Възстановяването на паролата има най-строгите лимити в API, всички броени за целия сървър: 3 заявки на час (`AUTH_FORGOT_PER_HOUR`) и 10 за 24 часа (`AUTH_FORGOT_PER_DAY`) на клиент (IPv4 адрес или IPv6 /64), 3 пъти повече на IPv6 /48, в допълнение към лимита `auth` (20 за 10 минути) и към единия имейл на адрес на всеки 5 минути. Нито 202, нито 429 издава дали акаунт използва адреса. Задаването на новата парола има собствен лимит (`auth_reset`).

        **Лимити:** `auth`, `auth_forgot`, след това `auth_forgot_day`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmailRequest'
            example:
              email: alice@example.org
      responses:
        '202':
          description: Заявката е приета (независимо от адреса).
          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`: лимитът `auth`, `auth_forgot` или `auth_forgot_day` (независимо от адреса).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` с `retryAfter: 1`: базата данни остана заключена, докато се търсеше акаунтът или се съхраняваше връзката; 5-те минути на адреса въпреки това започват да текат, така че нова заявка в рамките им не изпраща връзка. `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /auth/password/reset:
    post:
      operationId: resetPassword
      tags:
        - auth
      summary: Задаване на нова парола с токена от връзка за нулиране
      description: |-
        Задава нова парола с токена от връзка за нулиране (страницата `/reset-password` прави същото). Новата парола следва правилата за регистрация.

        Нулирането има следните последици, в една транзакция (всички или нито една): прекратява всички сесии и отменя чакаща смяна на имейла; другите връзки за нулиране на акаунта спират да работят; адресът се смята за потвърден (връзката го е доказала); собственикът получава имейл. Двуфакторното удостоверяване не се засяга. След грешка на опашката за хеширане на пароли, 503 или 500 нищо не е променено: старата парола и сесиите остават, а връзката остава валидна.

        **Лимити:** `auth`, след това `auth_reset` (10 на час на клиент, 30 на IPv6 /48, общ със страницата: всеки опит хешира парола).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasswordResetRequest'
            example:
              token: A0ATfmXCDKFC8UkSxLJCD85K7au5nMIrZ2P3K-1VWQg
              newPassword: a much better passphrase
      responses:
        '200':
          description: Паролата е сменена и е извършен изход от всички сесии.
          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`: връзката е невалидна, използвана или изтекла, или е изпратена до адрес, който акаунтът вече няма. `weak_password` (с `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`: лимитът `auth` или `auth_reset`, или опашката за хеширане на пароли.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: опашката за хеширане на пароли или базата данни остана заключена (`retryAfter: 1`, нищо не е променено). `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /auth/sso/google/start:
    post:
      operationId: startGoogleSignIn
      tags:
        - google-sign-in
      summary: Започване на вход с Google
      description: |-
        Започва опит за вход с Google за PKCE предизвикателството и порта на слушателя на играта на `127.0.0.1`. Отговорът дава URL адреса на Google, който да се отвори в системния браузър, и `state` на опита; опитът е валиден 10 минути.

        `authUrl` съдържа `client_id`, `redirect_uri` (съставен от `redirectPort` и етикета на произхода на сървъра), `response_type=code`, `scope=openid email profile`, `state`, `nonce`, `code_challenge` (S256 на собствения верификатор на сървъра за Google) с `code_challenge_method=S256` и `prompt=select_account`. Играта го проверява, преди да го отвори (вж. описанието на раздела).

        **Лимит:** `sso_start`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SsoStartRequest'
            example:
              codeChallenge: 6e7diXEYxG7OTYw7STfNOltEeLxAilPthC_txzaE0xA
              redirectPort: 51234
      responses:
        '200':
          description: Опитът.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SsoStartAnswer'
              example:
                attemptId: sso_ogvVEL75n7q90NIFsh-4zXVt-izc3WRbghsd6HDOtNQ
                authUrl: https://accounts.google.com/o/oauth2/v2/auth?client_id=1234567890-abc.apps.googleusercontent.com&redirect_uri=http%3A%2F%2F127.0.0.1%3A51234%2Foauth2%2Fgoogle%2FIhcScoV7eDOzTEcSnqPUPt&response_type=code&scope=openid%20email%20profile&state=yBjjEhGgqHdeqhUAzyx9jE2IhQ56bec53iRA7RvKRzQ&nonce=gYIKKhwnz1AqWfFxOC3R-ZvCACw06q74fEMtGEOJQWU&code_challenge=ZfEI6iZjePTHbnbevupaD9O7GjBCzcL-GJu2XGOdFGc&code_challenge_method=S256&prompt=select_account
                state: yBjjEhGgqHdeqhUAzyx9jE2IhQ56bec53iRA7RvKRzQ
                expiresIn: 600
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: входът с Google не се предлага на този сървър.'
        '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`: лимитът `sso_start`.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (базата данни остана заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /auth/sso/google/web/start:
    post:
      operationId: startGoogleWebSignIn
      tags:
        - google-sign-in
      summary: Започване на вход с Google на уебсайт
      description: |-
        Започва опит за вход с Google от уебсайт за PKCE предизвикателството на неговата страница. Отговорът е същият като за играта: URL адресът на Google, към който да се премине, и `state` на опита; опитът е валиден 10 минути. Предлага се, когато `GET /info` съобщава `sso.googleWeb: true`.

        `authUrl` съдържа `client_id` на уебсайта и `redirect_uri=GOOGLE_WEB_REDIRECT_URI`, а останалите параметри са същите като за играта. Страницата го проверява, преди да премине към него (вж. **На уебсайт** в описанието на раздела).

        **Лимит:** `sso_start`, общ с началната заявка на играта.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SsoWebStartRequest'
            example:
              codeChallenge: 6e7diXEYxG7OTYw7STfNOltEeLxAilPthC_txzaE0xA
      responses:
        '200':
          description: Опитът.
          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`: липсващ `codeChallenge` или такъв с грешен формат, или което и да е друго поле (включително `redirectPort`).'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: входът с Google не се предлага на този сървър или не се предлага на уебсайтове.'
        '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`: лимитът `sso_start` (общ с началната заявка на играта).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (базата данни остана заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /auth/sso/google/finish:
    post:
      operationId: finishGoogleSignIn
      tags:
        - google-sign-in
      summary: Предаване на отговора на Google на сървъра
      description: |-
        Предава на сървъра `code` и `state`, които Google е изпратил до слушателя на играта или до страницата на уебсайта, заедно с идентификатора на опита и PKCE верификатора. Идентификаторът на опита е безполезен без верификатора, а кодът е безполезен без собствения PKCE верификатор и клиентската тайна на сървъра.

        Сървърът проверява опита (непознат, използван или изтекъл: 410), след това верификатора (грешен верификатор оставя опита използваем), след това използва опита еднократно, проверява `state` и `iss`, обменя кода с Google и проверява ID токена. Отговорът (200) е един от следните:

        - `{ token, expiresAt, user }`: успешен вход в свързания акаунт;
        - `{ mfaRequired, mfaToken, expiresIn }`: продължете с `POST /auth/login/mfa`;
        - `{ needsUsername, ssoTicket, suggestedUsername }`: нов акаунт; продължете с `POST /auth/sso/complete` до 10 минути. `suggestedUsername` идва от името в Google или от адреса и е `""`, когато нищо не е подходящо;
        - `{ needsPassword, linkTicket, username, expiresIn }`: акаунтът с този адрес има парола; продължете с `POST /auth/sso/google/link`. Все още нищо не е свързано.

        **Лимит:** `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: Сесия, втора стъпка или следващата стъпка на първи вход с Google.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SsoFinishAnswer'
              examples:
                session:
                  summary: Успешен вход в свързания акаунт
                  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: Двуфакторното удостоверяване е включено
                  value:
                    mfaRequired: true
                    mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                    expiresIn: 300
                needsUsername:
                  summary: Нов играч
                  value:
                    needsUsername: true
                    ssoTicket: sso_gYIKKhwnz1AqWfFxOC3R-ZvCACw06q74fEMtGEOJQWU
                    suggestedUsername: alice
                needsPassword:
                  summary: Акаунт с парола използва адреса
                  value:
                    needsPassword: true
                    linkTicket: sso_ZfEI6iZjePTHbnbevupaD9O7GjBCzcL-GJu2XGOdFGc
                    username: alice
                    expiresIn: 600
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_verifier`: верификаторът не съответства на предизвикателството на опита (опитът остава използваем). `sso_email_unverified`: Google не е потвърдил адреса. `registration_closed`. `account_disabled`. Само за свързан акаунт: `banned` (с `until`), `email_unverified`.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: входът с Google не се предлага на този сървър.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_account_exists`: активен акаунт без парола използва адреса (той не се посочва).'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: непознат, използван или изтекъл опит.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: лимитът `sso_finish`.'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/BadGateway'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (базата данни остана заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /auth/sso/google/link:
    post:
      operationId: linkGoogleAccount
      tags:
        - google-sign-in
      summary: Свързване на Google със съществуващ акаунт чрез паролата му
      description: |-
        След като `finish` е отговорил с `needsPassword`: свързва Google със съществуващия акаунт чрез паролата на този акаунт, въведена в играта. Отговорът (200) е сесия (свързването се съхранява и играчът влиза) или, когато двуфакторното удостоверяване е включено, `{ mfaRequired, mfaToken, expiresIn }`: продължете с `POST /auth/login/mfa`, като свързването се съхранява само когато там бъде приет код.

        Грешна парола оставя билета използваем, общо за 5 опита. Опит се отчита точно преди проверката на паролата: 429 `too_many_attempts` или 428 `pow_required` не отнемат опит, докато отказ от опашката за хеширане на пароли (503 `server_busy`, 429 `rate_limited`) вече е отнел един и се брои като неуспех на акаунта. Забавянето след неуспешни опити използва същия брояч като `POST /auth/login`.

        **Лимит:** `auth` (заедно с броенето му по мрежа IPv6 /48). **Доказателство за работа:** както при `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: Сесия или втората стъпка на входа.
          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`: грешна парола; билетът се запазва – общо за 5 опита.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`banned` (с `until`), само след правилна парола.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: входът с Google не се предлага на този сървър.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_already_linked`: междувременно акаунтът в Google е бил свързан с друг акаунт.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: непознат, използван или изтекъл билет, 5-ата грешна парола, акаунт, чийто статус или адрес се е променил след `finish`, или акаунт, чийто статус, адрес, парола или двуфакторно удостоверяване са се променили, докато свързването се съхраняваше. Започнете входа отначало.'
        '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` (забавянето след неуспешни опити на акаунта) или `rate_limited` (лимитът `auth`, опашката за хеширане на пароли).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: опашката за хеширане на пароли или базата данни остана заключена (`retryAfter: 1`, нищо не е свързано). `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /auth/sso/complete:
    post:
      operationId: completeGoogleSignUp
      tags:
        - google-sign-in
      summary: Създаване на акаунта при първи вход с Google
      description: |-
        След като `finish` е отговорил с `needsUsername`: създава акаунта с избраното потребителско име (важат правилата за регистрация) и извършва вход в него. Акаунтът няма парола: `hasPassword` е `false`, а „Забравена парола?“ (`POST /auth/password/forgot`) му задава парола.

        **Лимит:** `auth` (заедно с броенето му по мрежа 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: Акаунтът е създаден и входът е успешен.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionAnswer'
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_username`, `invalid_request`, `invalid_json`.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`registration_closed`.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: входът с Google не се предлага на този сървър.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`username_taken` (някой акаунт има това потребителско име или чакаща регистрация на друг адрес го е запазила), `sso_already_linked`, `email_taken`.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: непознат, използван или изтекъл билет.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: лимитът `auth`.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (базата данни остана заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /auth/sessions:
    get:
      operationId: listSessions
      tags:
        - sessions
      summary: Списък на устройствата с активен вход
      description: |-
        Активните сесии на акаунта (устройствата с активен вход), като най-скоро използваната е първа. `current` отбелязва сесията, от която идва заявката. `lastSeenAt` се обновява най-много веднъж на 5 минути (по-късно, докато сървърът сериозно изостава); `expiresAt` е абсолютният край, а лимитът за неактивност може да прекрати сесията по-рано.

        **Лимит:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Активните сесии.
          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`: лимитът `sessions` или бюджетът на акаунта.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (базата данни остана заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /auth/sessions/{id}:
    delete:
      operationId: revokeSession
      tags:
        - sessions
      summary: Изход от едно устройство
      description: |-
        Извършва изход от една сесия на акаунта; това може да е и текущата сесия. WebSocket връзката, отворена с нея, се затваря. Без тяло (или `{}`).

        **Лимит:** `sessions`.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/SessionId'
      responses:
        '200':
          description: Успешен изход от сесията.
          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`: тяло, различно от `{}`, или `id`, което не е валидно URL кодиране; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: в този акаунт няма активна сесия с този идентификатор.'
        '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`: лимитът `sessions` или бюджетът на акаунта.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (базата данни остана заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /account/me:
    get:
      operationId: getAccount
      tags:
        - account
      summary: Получаване на акаунта, рейтингите му и активните санкции
      description: |-
        Акаунтът такъв, какъвто го вижда неговият играч: изгледът на акаунта (също `user` във всеки отговор при вход), по един рейтингов запис за всяка категория, в която играчът е играл партии с рейтинг, активните санкции и блокирането. Нивото на доверие от античийт системата никога не се показва.

        **Лимит:** `account`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Акаунтът.
          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` или `invalid_token` (и когато акаунтът е изтрит).'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: лимитът `account` или бюджетът на акаунта.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (базата данни остана заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /account/preferences:
    put:
      operationId: updatePreferences
      tags:
        - account
      summary: Приемане или отказ на преки предизвикателства
      description: |-
        Задава дали другите играчи могат да предизвикват този играч по име. При `none` преките предизвикателства се отказват: на предизвикващия се съобщава, че играчът не е на разположение. Без повторно удостоверяване.

        **Лимит:** `account`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Preferences'
            example:
              acceptChallenges: none
      responses:
        '200':
          description: Предпочитанията, които са в сила сега.
          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`: лимитът `account` или бюджетът на акаунта.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (базата данни остана заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /account/password:
    post:
      operationId: changePassword
      tags:
        - account
      summary: Смяна на паролата
      description: |-
        Сменя паролата. Нужна е текущата парола, но не и втори фактор, дори при включено двуфакторно удостоверяване. Новата парола следва правилата за регистрация (проверява се след текущата парола).

        Промяната има следните последици в една транзакция (всички или нито една): прекратява всички други сесии (тази остава с активен вход), отменя чакаща смяна на имейла, връзките за нулиране на паролата на акаунта престават да работят и собственикът получава имейл.

        **Повторно удостоверяване:** само паролата. **Лимити:** `reauth`, след това `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: Паролата е сменена.
          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` (акаунт само с вход с Google), `weak_password` (с `reason`), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password`: грешна текуща парола или нулиране или смяна на паролата, настъпили, докато заявката се проверяваше.'
        '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` (неуспешни повторни удостоверявания на акаунта) или `rate_limited` (лимитът `reauth` или `reauth_user`, бюджетът на акаунта, опашката за хеширане на пароли).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: опашката за хеширане на пароли или базата данни остана заключена (`retryAfter: 1`, нищо не е променено). `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /account/mfa/totp/setup:
    post:
      operationId: startTotpSetup
      tags:
        - two-step-verification
      summary: Започване на включването на двуфакторното удостоверяване
      description: |-
        Записва нов чакащ таен ключ за приложението за удостоверяване, който заменя предишния чакащ ключ, ако има такъв, и го връща за приложението (като текст и като URI `otpauth://`, който да се покаже като QR код). Двуфакторното удостоверяване още не е включено: `POST /account/mfa/totp/enable` го включва с код от този таен ключ.

        **Повторно удостоверяване:** само паролата. **Лимити:** `reauth`, след това `reauth_user`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasswordOnlyRequest'
            example:
              password: correct horse battery
      responses:
        '200':
          description: Чакащият таен ключ.
          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` (акаунт само с вход с 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` (проверява се преди паролата).'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (неуспешни повторни удостоверявания на акаунта) или `rate_limited` (лимитът `reauth` или `reauth_user`, бюджетът на акаунта, опашката за хеширане на пароли).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (опашката за хеширане на пароли или базата данни остана заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /account/mfa/totp/enable:
    post:
      operationId: enableTotp
      tags:
        - two-step-verification
      summary: Завършване на включването на двуфакторното удостоверяване и получаване на кодове за възстановяване
      description: |-
        Включва двуфакторното удостоверяване с код от чакащия таен ключ (точно 6 цифри). Тук не се иска парола; тя е въведена при настройката. Отговорът съдържа 10 кода за възстановяване, показани само този път.

        **Лимити:** `reauth`, след това `reauth_user`; грешен код се брои към неуспешните повторни удостоверявания на акаунта.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TotpEnableRequest'
            example:
              code: '123456'
      responses:
        '200':
          description: Двуфакторното удостоверяване е включено.
          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` не е точно 6 цифри или тялото нарушава схемата; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_code`: грешен код (проверете часа на устройството).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`mfa_already_enabled`; `mfa_setup_required`: няма чакащ таен ключ (първо извикайте `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` (неуспешни повторни удостоверявания на акаунта) или `rate_limited` (лимитът `reauth` или `reauth_user`, бюджетът на акаунта).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (базата данни остана заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /account/mfa/totp/disable:
    post:
      operationId: disableTotp
      tags:
        - two-step-verification
      summary: Изключване на двуфакторното удостоверяване
      description: |-
        Изключва двуфакторното удостоверяване. Тайният ключ и кодовете за възстановяване се изтриват, а собственикът получава имейл.

        **Повторно удостоверяване:** паролата и код от приложението за удостоверяване или код за възстановяване (задължително е едно от `code` или `recoveryCode`). **Лимити:** `reauth`, след това `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: Двуфакторното удостоверяване е изключено.
          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` (акаунт само с вход с Google), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`mfa_code_required` (нито `code`, нито `recoveryCode`; проверява се преди паролата), `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` (неуспешни повторни удостоверявания или твърде много опитани кодове за този акаунт) или `rate_limited` (лимитът `reauth` или `reauth_user`, бюджетът на акаунта, опашката за хеширане на пароли).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (опашката за хеширане на пароли или базата данни остана заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /account/mfa/recovery-codes:
    post:
      operationId: regenerateRecoveryCodes
      tags:
        - two-step-verification
      summary: Подмяна на кодовете за възстановяване
      description: |-
        Заменя кодовете за възстановяване с 10 нови; старите престават да работят.

        **Повторно удостоверяване:** паролата и код от приложението за удостоверяване в `code` (код за възстановяване се отказва с 403 `invalid_code`). **Лимити:** `reauth`, след това `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: Новите кодове за възстановяване, показани само този път.
          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` (акаунт само с вход с 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'
        '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` (неуспешни повторни удостоверявания или твърде много опитани кодове за този акаунт) или `rate_limited` (лимитът `reauth` или `reauth_user`, бюджетът на акаунта, опашката за хеширане на пароли).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (опашката за хеширане на пароли или базата данни остана заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /account/email:
    post:
      operationId: changeEmail
      tags:
        - email-change
      summary: Смяна на имейл адреса
      description: |-
        Сменя имейл адреса на акаунта. От `newEmail` се премахват интервалите в началото и в края и буквите му стават малки, след което той се проверява като адрес при регистрация.

        **С потвърждение по имейл** (по подразбиране): 202 `verification_sent`. Връзка, валидна 24 часа, се изпраща на новия адрес; тя отваря `/confirm-email-change`, а адресът се сменя едва когато играчът натисне бутона на тази страница. Дотогава `GET /account/me` показва новия адрес като `pendingEmail`. Нова заявка заменя чакащата; смяна или нулиране на паролата я отменя. На даден нов адрес се изпраща най-много една връзка на 5 минути, независимо кой я иска (заявка за вече чакащата смяна запазва изпратената по-рано връзка, която остава валидна); отговорът е същият. Текущият адрес получава известие, че е поискана смяна към маскиран адрес (`a***@example.org`). Отговорът и `pendingEmail` са същите, когато друг акаунт вече използва новия адрес: тогава не се изпраща връзка, така че тази смяна никога не завършва, а собственикът на този адрес вместо това получава известие (най-много едно на час).

        Когато връзката бъде потвърдена, адресът се сменя и се смята за потвърден, устройствата остават с активен вход, изпратените по-рано връзки (потвърждение, нулиране на паролата, други смени) престават да работят, а на предишния адрес се изпраща известие, в което новият адрес е маскиран.

        **Без потвърждение по имейл** (`REQUIRE_EMAIL_VERIFICATION=false`): адресът се сменя веднага (200 `email_changed`), а на предишния адрес се изпраща известие. Ако друг акаунт използва адреса, отговорът е 409 `email_taken`, а собственикът на този адрес получава известието.

        **Повторно удостоверяване:** паролата и, при двуфакторно удостоверяване, код от приложението за удостоверяване или код за възстановяване. `invalid_email` и `same_email` се проверяват преди паролата, така че не се броят за неуспех. **Лимити:** `reauth`, след това `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: Адресът е сменен веднага (на този сървър няма потвърждение по имейл).
          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: Новият адрес, както е записан.
              example:
                status: email_changed
                email: alice.new@example.org
        '202':
          description: Изпратена е връзка за потвърждение до новия адрес (или адресът принадлежи на друг акаунт; отговорът не казва кое от двете).
          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` (текущият адрес на акаунта), `password_not_set` (акаунт само с вход с 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'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`email_taken`: само без потвърждение по имейл.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (неуспешни повторни удостоверявания или твърде много опитани кодове за този акаунт) или `rate_limited` (лимитът `reauth` или `reauth_user`, бюджетът на акаунта, опашката за хеширане на пароли).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: опашката за хеширане на пароли или базата данни остана заключена (`retryAfter: 1`: нищо не е променено и същата заявка може да се изпрати отново). `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /account/export:
    post:
      operationId: exportAccountData
      tags:
        - data-export
      summary: Изтегляне на данните на акаунта
      description: |-
        Всичко, което сървърът пази за акаунта, като един JSON файл за записване (`format` `scacelith-account-export`, `version` 1). Експортът записва събитие по сигурността (`account_exported`). Масивът `notes` на документа казва на играча на прост английски какво е изключено.

        **Никога не влиза в експорта:** хешът на паролата, тайният ключ за двуфакторното удостоверяване и кодовете за възстановяване; който и да е токен на сесия или връзка или хеш на такъв токен; данните на античийт системата (нивото и оценката на доверие, аномалиите, анализът на партиите, тежестта на даден сигнал); сигналите, подадени от други играчи срещу играча; самоличността на модераторите; личните данни на други играчи (противниците се появяват с публичното си име и рейтинг, нищо не показва дали друг играч е бил санкциониран и не се включва IP адрес, който може да е на друг човек).

        **Повторно удостоверяване:** паролата и, при двуфакторно удостоверяване, код от приложението за удостоверяване или код за възстановяване. **Лимити:** `account_export` (5 на час на играч, за целия сървър; брои се всеки опит, включително неуспешните; проверява се първи), след това `reauth` и `reauth_user`. **Лимит за време:** 60 секунди.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReauthCredentials'
            example:
              password: correct horse battery
              code: '123456'
      responses:
        '200':
          description: Документът на експорта като прикачен файл.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            Content-Disposition:
              description: '`attachment; filename="scacelith-account-<username>.json"`. Знаците в потребителското име, различни от букви, цифри, `_`, `.` и `-`, стават `_`.'
              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` (акаунт само с вход с 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` (неуспешни повторни удостоверявания или твърде много опитани кодове за този акаунт) или `rate_limited` (лимитът `account_export`, `reauth` или `reauth_user`, бюджетът на акаунта, опашката за хеширане на пароли).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (базата данни остана заключена, със заглавка `Retry-After: 1`), `server_busy` (опашката за хеширане на пароли), `timeout` (60 секунди). Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /account/delete:
    post:
      operationId: deleteAccount
      tags:
        - account-deletion
      summary: Изтриване на акаунта
      description: |-
        Изтрива акаунта; това не може да бъде отменено.

        - Всички сесии се прекратяват веднага: оттук нататък токенът получава 401 `invalid_token`.
        - Потребителското име става `deleted#<id>` в акаунта и във всеки запис на партия.
        - Изтриват се: имейл адресът, хешът на паролата, тайният ключ за двуфакторното удостоверяване и кодовете за възстановяване, сесиите и токените на връзките, свързването с Google, записът за доверие от античийт системата и IP адресите, записани със събитията по сигурността.
        - Рейтингите и партиите се запазват. Партиите остават достъпни за четене под анонимното име, а `GET /players/{username}` отговаря с 404 за предишното име.

        **Повторно удостоверяване:** паролата и, при двуфакторно удостоверяване, код от приложението за удостоверяване или код за възстановяване. **Лимити:** `reauth`, след това `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: Акаунтът е изтрит.
          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` (акаунт само с вход с 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` (неуспешни повторни удостоверявания или твърде много опитани кодове за този акаунт) или `rate_limited` (лимитът `reauth` или `reauth_user`, бюджетът на акаунта, опашката за хеширане на пароли).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (опашката за хеширане на пароли или базата данни остана заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /account/games:
    get:
      operationId: listAccountGames
      tags:
        - game-history
      summary: Списък на партиите на играча, с филтриране и страниране
      description: |-
        Партиите на влезлия играч, от най-новата, с филтриране и страниране, заедно с броя на партиите, които отговарят на филтъра. Всички параметри на заявката са незадължителни, а празна стойност се смята за липсваща.

        Страниране: подайте `next` на предишната страница като `before`; на последната страница `next` е `null`. `total` брои партиите, които отговарят на филтъра, във всички страници.

        **Лимит:** `account_games`.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/BeforeQuery'
        - $ref: '#/components/parameters/LimitQuery'
        - name: category
          in: query
          required: false
          description: Идентификатор на официална категория (`3+2` или `3%2B2`) или `custom` за всички партии с друга контрола на времето.
          schema:
            type: string
            pattern: '^\s*([0-9]+[+ ][0-9]+|custom)\s*$'
          example: '3+2'
        - name: rated
          in: query
          required: false
          description: Само партии с рейтинг (`true`) или само приятелски партии (`false`).
          schema:
            type: string
            enum:
              - 'true'
              - 'false'
        - name: result
          in: query
          required: false
          description: Само спечелените, загубените или завършилите реми партии, от гледна точка на играча. Анулираните партии се появяват само без този филтър.
          schema:
            type: string
            enum:
              - win
              - loss
              - draw
      responses:
        '200':
          description: Една страница от историята.
          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` не е идентификатор на партия), `invalid_limit` или `invalid_filter` (`category`, `rated` или `result`), всяко с `field`, което посочва параметъра.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: лимитът `account_games` или бюджетът на акаунта.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (базата данни остана заключена; `retryAfter: 1` в тялото, без заглавка `Retry-After`), `server_busy` (търсенето на сесията завари базата данни заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /games/{id}:
    get:
      operationId: getGame
      tags:
        - games
      summary: Получаване на запис на партия с ходовете и часовниците
      description: |-
        Един запис на партия с ходовете и часовниците ѝ. Без токен или с токена на играч, който не е играл партията, отговорът е публичният. Когато играчът на токена е играл партията, отговорът добавя `you` и `reportable`.

        **Лимит:** `public_read` (по играч с токен, по клиент без токен).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
      responses:
        '200':
          description: Записът на партията.
          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`: не е положително цяло число от най-много 16 цифри (под 2^53); `invalid_request`: невалидно URL кодиране.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: изпратен е токен и той не е валиден.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: няма такава партия.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: лимитът `public_read` или бюджетът на акаунта.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (базата данни остана заключена; `retryAfter: 1` в тялото, без заглавка `Retry-After`), `server_busy` (търсенето на сесията завари базата данни заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /games/{id}/pgn:
    get:
      operationId: getGamePgn
      tags:
        - games
      summary: Изтегляне на партия като PGN файл
      description: |-
        Същата партия като PGN файл: една партия с краища на редовете `\n` и текст на ходовете в редове под 80 колони. Отговорът е същият със или без токен.

        Тагове, в този ред: `Event` (`<SERVER_NAME> rated <category>` или `<SERVER_NAME> casual <category>`), `Site` (`SERVER_PUBLIC_HOST`), `Date` (датата на началото по UTC), `Round`, `White`, `Black`, `Result` (`*` за анулирана партия), `UTCDate` и `UTCTime` (началото), `WhiteElo` и `BlackElo` (рейтингите в началото или `-`), `WhiteRatingDiff` и `BlackRatingDiff` (промените, например `+10` и `-10`, във всяка партия с рейтинг, `+0`, когато правилата за рейтинга оставят рейтинга непроменен; приятелска партия, партия с контрола по избор или анулирана партия нямат нито едното, нито другото), `TimeControl` (в секунди), `Termination` (стандартна стойност на PGN: `normal`; `time forfeit` – падане на флагчето, и когато партията завършва реми; `abandoned`; `rules infraction` – втори неправилен ход или служебна загуба заради нарушение на честната игра; `unterminated` – анулирана партия), `PlyCount`, `ScacelithGameId` (десетичният идентификатор).

        Всеки ход носи `{[%clk h:mm:ss.f] [%emt h:mm:ss.f]}`: показанието на часовника на играча, направил хода, след хода и времето, отчетено за хода, в десети от секундата (с отрязване). Стойност се пропуска, когато записът не я съдържа. След последния ход следва причината за края с думи, а след това резултатът.

        **Лимит:** `public_read` (по играч с токен, по клиент без токен).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
      responses:
        '200':
          description: 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: 'Текстът на PGN, в 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`: не е положително цяло число от най-много 16 цифри (под 2^53); `invalid_request`: невалидно URL кодиране.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: изпратен е токен и той не е валиден.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: няма такава партия.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: лимитът `public_read` или бюджетът на акаунта.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`internal_error`: наред с други случаи, записаните ходове не могат да бъдат изиграни отново до записания край (сървърът го записва в лога).'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (базата данни остана заключена; `retryAfter: 1` в тялото, без заглавка `Retry-After`), `server_busy` (търсенето на сесията завари базата данни заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /games/{id}/gif:
    get:
      operationId: getGameGif
      tags:
        - gifs
      summary: Изтегляне на партия от този сървър като анимиран GIF
      description: |-
        Партията като анимиран GIF. Имената и рейтингите са тези от записа на партията (рейтингите в началото, изтрит акаунт като `deleted#<id>`), както и резултатът и краят (`Resignation`, `Loss on time`…).

        Проверките са в този ред: `gif_disabled`, опциите на изображението (`size`, `orientation`, `delay`, `coords`), идентификаторът на партията, партията, дължината ѝ.

        **Изисква се сесия:** квотите се броят по акаунт. **Лимити:** `gif` при всяка заявка; `gif_user_min`, `gif_user_hour`, `gif_ip_min` и `gif_ip_hour` само когато GIF файлът трябва да бъде създаден (вж. описанието на раздела). **Лимит за време:** `GIF_QUEUE_TIMEOUT_MS` + `GIF_RENDER_TIMEOUT_MS` + 5 секунди (45 секунди по подразбиране).
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
        - name: size
          in: query
          required: false
          description: 'Размерът на изображението: `small` (полета от 32 px), `medium` (48 px) или `large` (72 px).'
          schema:
            type: string
            enum:
              - small
              - medium
              - large
            default: medium
        - name: orientation
          in: query
          required: false
          description: Страната в долната част на дъската.
          schema:
            type: string
            enum:
              - white
              - black
            default: white
        - name: delay
          in: query
          required: false
          description: Милисекунди на ход (от 1 до 6 десетични цифри).
          schema:
            type: integer
            minimum: 100
            maximum: 3000
            default: 500
        - name: coords
          in: query
          required: false
          description: Дали буквите на вертикалите и цифрите на хоризонталите се изобразяват около дъската (`1`) или не (`0`).
          schema:
            type: string
            enum:
              - '1'
              - '0'
            default: '1'
      responses:
        '200':
          $ref: '#/components/responses/GifFile'
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_option` с `field` (`size`, `orientation`, `delay` или `coords`): стойност извън позволените. `invalid_game_id`. `invalid_request`: невалидно URL кодиране.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: няма такава партия. `gif_disabled`: сървърът е изключил GIF файловете (`GIF_ENABLED=false`; токенът от лимита `gif` се връща).'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '422':
          $ref: '#/components/responses/UnprocessableContent'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: лимитът `gif`, лимит за рендериране или бюджетът на акаунта.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`render_failed`: GIF файлът не можа да бъде създаден (сървърът записва причината в лога). `internal_error`.'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` с `retryAfter` (от 3 до 10 секунди) и `Retry-After`: опашката за рендериране е пълна или GIF файлът е чакал `GIF_QUEUE_TIMEOUT_MS` (10 секунди) за свободна нишка; всички токени на лимити, взети от заявката, се връщат. `busy`: базата данни остана заключена (`retryAfter: 1` само в тялото). `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /gif:
    post:
      operationId: renderPgnGif
      tags:
        - gifs
      summary: Създаване на анимиран GIF от произволна партия, изпратена като PGN
      description: |-
        Същото изображение като `GET /games/{id}/gif` за произволна партия, изпратена като PGN текст: партия, записана от играта, експорт от друг сайт, партия, въведена на ръка. Използва се само първата партия от текста.

        - PGN четецът приема това, което приема собственият четец на играта: всеки PGN, който този сървър пише, и обичайните експорти от други сайтове (коментарите, вариантите, NAG и анотациите на часовника се пропускат; номерата на ходовете и SAN се четат толерантно; таг `FEN` задава началната позиция, освен ако `SetUp` е `"0"`).
        - Имената и рейтингите идват от таговете `White`, `Black`, `WhiteElo` и `BlackElo`. Буквите с диакритични знаци губят знаците си, другите знаци извън печатимия ASCII стават `?`, а дългите имена се съкращават (до 48 знака). Резултатът идва от тага `Result`, иначе от края на текста на ходовете. Тагът `Termination` се показва, освен ако е `normal`: тогава финалната позиция говори сама (мат, пат).
        - Тази крайна точка сама проверява тялото си: при непознато поле или когато `pgn` липсва или не е низ, отговорът е 400 `invalid_request` с `field`; при грешна опция отговорът е 400 `invalid_option` (`delayMs` трябва да е JSON число, `coords` – булева стойност; `null` се отказва).

        **Изисква се сесия:** квотите се броят по акаунт. **Лимити:** както за `GET /games/{id}/gif`. **Лимит за размера на тялото:** 135 168 байта, независимо от `HTTP_BODY_LIMIT` (PGN като JSON низ, включително екраниращите последователности). **Лимит за време:** 45 секунди по подразбиране.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GifRequest'
            examples:
              short:
                summary: Кратка партия, въведена на ръка, без координати
                value:
                  pgn: 1. f3 e5 2. g4 Qh4# 0-1
                  coords: false
              options:
                summary: PGN файл с всички опции
                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` с `field` (непознато поле или `pgn` липсва или не е низ; без `field`, когато тялото не е обект); `invalid_json`; `invalid_option` с `field` (`size`, `orientation`, `delayMs` или `coords`); `invalid_pgn` с `line` и `column` (от 1, колоните са в знаци) и `message` на четеца: неправилен или нееднозначен ход, повреден таг, непозната разновидност на шаха, повече от 65 536 байта.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`gif_disabled`: сървърът е изключил GIF файловете (`GIF_ENABLED=false`; токенът от лимита `gif` се връща).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
          description: '`payload_too_large`: тяло над 135 168 байта.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '422':
          $ref: '#/components/responses/UnprocessableContent'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: лимитът `gif`, лимит за рендериране или бюджетът на акаунта.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`render_failed`: GIF файлът не можа да бъде създаден (сървърът записва причината в лога). `internal_error`.'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` с `retryAfter` (от 3 до 10 секунди) и `Retry-After`: опашката за рендериране е пълна или GIF файлът е чакал твърде дълго за свободна нишка; всички токени на лимити, взети от заявката, се връщат. `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /players/{username}:
    get:
      operationId: getPlayer
      tags:
        - players
      summary: Получаване на публичния профил на играч
      description: |-
        Публичният профил на играч: рейтингите в официалните категории (в реда на сървъра) и броят на партиите. `games.total` брои всички записани партии, включително приятелските и анулираните; `games.rated`, `wins`, `draws` и `losses` се сумират от рейтинговите записи, следователно броят само партиите с рейтинг. Отговорът е същият със или без токен.

        **Лимит:** `public_read` (по играч с токен, по клиент без токен).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Username'
      responses:
        '200':
          description: Профилът.
          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`: не е от 2 до 24 знака от `[A-Za-z0-9_.-]`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: изпратен е токен и той не е валиден.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: няма такъв играч или акаунтът е изтрит.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: лимитът `public_read` или бюджетът на акаунта.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (базата данни остана заключена; `retryAfter: 1` в тялото, без заглавка `Retry-After`), `server_busy` (търсенето на сесията завари базата данни заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /players/{username}/games:
    get:
      operationId: listPlayerGames
      tags:
        - players
      summary: Списък на последните партии на играч
      description: |-
        Последните партии на играча, от най-новата, на страници като историята, но без филтри и без общ брой. `color` е страната на този играч. `next` е идентификаторът на последната партия винаги когато страницата е пълна, така че следващата страница може да е празна. Първо се търси играчът: непознат играч дава 404, каквито и да са параметрите на заявката.

        **Лимит:** `public_read` (по играч с токен, по клиент без токен).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Username'
        - $ref: '#/components/parameters/BeforeQuery'
        - $ref: '#/components/parameters/LimitQuery'
      responses:
        '200':
          description: Една страница от партиите на играча.
          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` не е идентификатор на партия), `invalid_limit` (последните две без `field`).'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: изпратен е токен и той не е валиден.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: няма такъв играч или акаунтът е изтрит.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: лимитът `public_read` или бюджетът на акаунта.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (базата данни остана заключена; `retryAfter: 1` в тялото, без заглавка `Retry-After`), `server_busy` (търсенето на сесията завари базата данни заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /leaderboard:
    get:
      operationId: getLeaderboard
      tags:
        - leaderboard
      summary: Получаване на най-добрите играчи в категория
      description: |-
        Първите 100 рейтингови записа на официална категория с поне `minGames` (`PROVISIONAL_GAMES`) зачетени партии, без изтритите акаунти и потвърдените измамници. Сървърът изчислява всяка класация наново най-много веднъж на 10 секунди; `updatedAt` казва кога. За дадена категория се изпълнява само едно изчисление наведнъж: междувременно другите заявки получават предишния списък или изчакват изчислението, когато още няма такъв.

        **Лимити:** само слоят по адрес.
      security: []
      parameters:
        - name: category
          in: query
          required: true
          description: Идентификатор на официална категория (`3+2` или `3%2B2`).
          schema:
            type: string
            pattern: '^\s*[0-9]+[+ ][0-9]+\s*$'
          example: '3+2'
        - name: limit
          in: query
          required: false
          description: Колко играчи, от 1 до 100. По-голямо число от най-много 3 цифри се смята за 100; празна стойност се смята за липсваща.
          schema:
            type: integer
            minimum: 1
            maximum: 999
            default: 100
      responses:
        '200':
          description: Класацията.
          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` (липсва или не е официална категория), `invalid_limit`.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: слоят по адрес.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (базата данни остана заключена; `retryAfter: 1` в тялото, без заглавка `Retry-After`), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /reports:
    post:
      operationId: reportPlayer
      tags:
        - reports
      summary: Подаване на сигнал срещу противника от скорошна партия
      description: |-
        Подава сигнал срещу противника от една от собствените партии на играча, завършила през последните 7 дни. Сигналът сам по себе си никога не променя рейтинг, санкция или ниво на доверие. Той повишава приоритета за преглед, който модераторите виждат, и, освен при `abuse`, изисква анализ на партията от шахматна програма.

        Сигнал срещу същия противник за същата партия получава същия отговор и не променя нищо; отговорът никога не казва нищо за акаунта, срещу който е подаден сигналът. `GET /games/{id}` казва предварително на играчите от партията дали сигналът би бил приет (`reportable`).

        Тази крайна точка сама проверява тялото си, в реда `gameId`, `reported`, `category`, `comment`: при неуспех отговорът е 400 `invalid_request` без `field`. Полетата, които не познава, се пренебрегват.

        **Лимити:** `reports` (по играч), след това `REPORTS_PER_DAY` (5) сигнала на играч за 24 часа (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: Сигналът е получен (или вече е бил подаден).
          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` (без `field`), `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`report_not_allowed`: посоченият играч не е противник на подателя на сигнала в партия, завършила през последните 7 дни.'
        '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`, със заглавка `Retry-After`): `REPORTS_PER_DAY` сигнала през последните 24 часа. `rate_limited`: лимитът `reports` или бюджетът на акаунта.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (търсенето на сесията завари базата данни заключена), `timeout`. Също `server_busy` с `retryAfter` от 2 до 5, когато сървърът сериозно изостава (вж. въведението): заявката не е изпълнена.'
  /verify-email:
    servers:
      - url: https://caissa.scacelith.com
        description: Официалният сървър (страниците са в корена, извън `/api/v1`).
      - url: https://{host}:{port}
        description: Всеки сървър Scacelith (страниците са в корена, извън `/api/v1`).
        variables:
          host:
            default: caissa.scacelith.com
            description: Публичното име на хоста на сървъра (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Публичният порт на API (`PUBLIC_API_PORT`, иначе `API_PORT`).
    get:
      operationId: showVerifyEmailPage
      tags:
        - pages
      summary: Показване на страницата за потвърждение на имейла
      description: |-
        Страницата на връзка за потвърждение на имейла (при регистрация или от повторно изпратен имейл за потвърждение). Тя показва само бутон „Confirm my e-mail address“ („Потвърди имейл адреса ми“), така че скенер на пощата, който отвори връзката, да не я изразходва; бутонът изпраща формуляра до `POST /verify-email`.

        **Лимит:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Страницата с бутона за потвърждение.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Връзката е невалидна или изтекла (HTML страница).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Лимитът `page` (HTML страница) или слоят по адрес (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitVerifyEmailPage
      tags:
        - pages
      summary: Потвърждаване на имейл адреса
      description: |-
        Формулярът на страницата за потвърждение. Потвърждава адреса; при нова регистрация тогава създава акаунта и играчът може да влезе.

        **Лимит:** `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: Адресът е потвърден (а при нова регистрация акаунтът е създаден).
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Връзката е невалидна, използвана или изтекла, или формулярът е невалиден (HTML страница).
        '408':
          $ref: '#/components/responses/HtmlRequestTimeout'
        '409':
          $ref: '#/components/responses/HtmlConflict'
          description: Връзката на нова регистрация, чието потребителско име или адрес междувременно е взел друг акаунт; не се създава акаунт (HTML страница).
        '413':
          $ref: '#/components/responses/HtmlPayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/HtmlUnsupportedMediaType'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Лимитът `auth` (HTML страница) или слоят по адрес (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Базата данни остана заключена (`Retry-After: 1`) или сървърът сериозно изостава (`Retry-After` от 2 до 5, вж. въведението): нищо не е променено и връзката продължава да работи. Също и изтичане на времето на обработчика.'
  /reset-password:
    servers:
      - url: https://caissa.scacelith.com
        description: Официалният сървър (страниците са в корена, извън `/api/v1`).
      - url: https://{host}:{port}
        description: Всеки сървър Scacelith (страниците са в корена, извън `/api/v1`).
        variables:
          host:
            default: caissa.scacelith.com
            description: Публичното име на хоста на сървъра (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Публичният порт на API (`PUBLIC_API_PORT`, иначе `API_PORT`).
    get:
      operationId: showResetPasswordPage
      tags:
        - pages
      summary: Показване на формуляра за нулиране на паролата
      description: |-
        Страницата на връзка за нулиране на паролата: формулярът за нова парола (паролата два пъти). Формулярът се изпраща до `POST /reset-password`.

        **Лимит:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Формулярът за нова парола.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Връзката е невалидна (и когато е била изпратена на адрес, който акаунтът вече няма).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Лимитът `page` (HTML страница) или слоят по адрес (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitResetPasswordPage
      tags:
        - pages
      summary: Задаване на нова парола от формуляра за нулиране
      description: |-
        Формулярът на страницата за нулиране; прави същото като `POST /auth/password/reset`, в една транзакция: всички устройства излизат от акаунта, чакаща смяна на имейла се отменя, другите връзки за нулиране престават да работят, адресът се смята за потвърден и собственикът получава имейл.

        Първо се проверява връзката, след това дали двете пароли са еднакви, след това правилата за паролата. Когато сървърът е зает, формулярът се връща с `Retry-After` и връзката остава валидна.

        **Лимити:** `auth`, след това `auth_reset` (споделен с `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: Паролата е сменена и всички устройства са излезли от акаунта.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Формулярът отново с грешката (паролите се различават, слаба парола), връзката е невалидна или формулярът е невалиден (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: Лимитът `auth` или `auth_reset` (HTML страница), формулярът отново с `Retry-After`, когато този клиент има твърде много чакащи хеширания на пароли (връзката остава валидна), или слоят по адрес (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Формулярът отново с `Retry-After`, когато сървърът е зает (опашката за хеширане на пароли или базата данни остана заключена); връзката остава валидна. Когато сървърът сериозно изостава (вж. въведението), вместо това страница „Server error“ („Грешка в сървъра“) с `Retry-After` (от 2 до 5): нищо не е изпълнено и връзката остава валидна. Също и изтичане на времето на обработчика.'
  /confirm-email-change:
    servers:
      - url: https://caissa.scacelith.com
        description: Официалният сървър (страниците са в корена, извън `/api/v1`).
      - url: https://{host}:{port}
        description: Всеки сървър Scacelith (страниците са в корена, извън `/api/v1`).
        variables:
          host:
            default: caissa.scacelith.com
            description: Публичното име на хоста на сървъра (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Публичният порт на API (`PUBLIC_API_PORT`, иначе `API_PORT`).
    get:
      operationId: showConfirmEmailChangePage
      tags:
        - pages
      summary: Показване на страницата за потвърждение на смяната на имейла
      description: |-
        Страницата на връзка за смяна на имейла: показва новия адрес и името на акаунта, с бутон „Use this e-mail address“ („Използвай този имейл адрес“), който изпраща формуляра до `POST /confirm-email-change`.

        **Лимит:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Страницата с новия адрес и бутона му за потвърждение.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Връзката е невалидна или изтекла (и когато адресът на акаунта се е променил след заявката).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Лимитът `page` (HTML страница) или слоят по адрес (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitConfirmEmailChangePage
      tags:
        - pages
      summary: Потвърждаване на новия имейл адрес
      description: |-
        Формулярът на страницата за смяна на имейла. Адресът се сменя и се смята за потвърден; устройствата остават с активен вход; изпратените по-рано връзки (потвърждение, нулиране на паролата, други смени) престават да работят; на предишния адрес се изпраща известие, в което новият адрес е маскиран.

        **Лимит:** `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: Адресът е сменен.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Връзката е невалидна, използвана или изтекла, или формулярът е невалиден (HTML страница).
        '408':
          $ref: '#/components/responses/HtmlRequestTimeout'
        '409':
          $ref: '#/components/responses/HtmlConflict'
          description: Междувременно друг акаунт е заел адреса (HTML страница).
        '413':
          $ref: '#/components/responses/HtmlPayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/HtmlUnsupportedMediaType'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Лимитът `auth` (HTML страница) или слоят по адрес (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Базата данни остана заключена (`Retry-After: 1`) или сървърът сериозно изостава (`Retry-After` от 2 до 5, вж. въведението): нищо не е променено и връзката продължава да работи. Също и изтичане на времето на обработчика.'
  /healthz:
    servers:
      - url: https://caissa.scacelith.com
        description: Официалният сървър, в корена.
      - url: https://caissa.scacelith.com/api/v1
        description: Официалният сървър, под `/api/v1`.
      - url: https://{host}:{port}
        description: Всеки сървър Scacelith, в корена.
        variables:
          host:
            default: caissa.scacelith.com
            description: Публичното име на хоста на сървъра (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Публичният порт на API (`PUBLIC_API_PORT`, иначе `API_PORT`).
      - url: https://{host}:{port}/api/v1
        description: Всеки сървър Scacelith, под `/api/v1`.
        variables:
          host:
            default: caissa.scacelith.com
            description: Публичното име на хоста на сървъра (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Публичният порт на API (`PUBLIC_API_PORT`, иначе `API_PORT`).
    get:
      operationId: getLiveness
      tags:
        - health
      summary: Проверка дали процесът работи
      description: |-
        Отговаря с 200 `ok`, докато процесът работи. `HEAD` също работи. На всеки друг метод отговорът е 405 `method_not_allowed` с `Allow: GET, HEAD` (включително на `OPTIONS`).

        **Лимити:** само слоят по адрес.
      security: []
      responses:
        '200':
          description: Процесът работи.
          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`: слоят по адрес.'
  /readyz:
    servers:
      - url: https://caissa.scacelith.com
        description: Официалният сървър, в корена.
      - url: https://caissa.scacelith.com/api/v1
        description: Официалният сървър, под `/api/v1`.
      - url: https://{host}:{port}
        description: Всеки сървър Scacelith, в корена.
        variables:
          host:
            default: caissa.scacelith.com
            description: Публичното име на хоста на сървъра (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Публичният порт на API (`PUBLIC_API_PORT`, иначе `API_PORT`).
      - url: https://{host}:{port}/api/v1
        description: Всеки сървър Scacelith, под `/api/v1`.
        variables:
          host:
            default: caissa.scacelith.com
            description: Публичното име на хоста на сървъра (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Публичният порт на API (`PUBLIC_API_PORT`, иначе `API_PORT`).
    get:
      operationId: getReadiness
      tags:
        - health
      summary: Проверка дали сървърът приема играчи
      description: |-
        Отговаря с 200 `ready`, когато сървърът приема играчи, и с 503 `not_ready`, докато стартира или спира. `HEAD` също работи. На всеки друг метод отговорът е 405 `method_not_allowed` с `Allow: GET, HEAD` (включително на `OPTIONS`).

        **Лимити:** само слоят по адрес.
      security: []
      responses:
        '200':
          description: Сървърът приема играчи.
          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`: слоят по адрес.'
        '503':
          description: Сървърът стартира или спира.
          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: |-
        Токен на сесия (`sct_`, последван от 43 знака base64url) от отговор при вход, в заглавката `Authorization`: `Authorization: Bearer <token>`. Префиксът е точно `Bearer` (чувствителен към регистъра) и един интервал; на стойност без него или на токен, който не е от 1 до 512 печатими ASCII знака, отговорът е 401 `invalid_token`, както за всеки друг невалиден токен. Празна заглавка се смята за липсваща.
  parameters:
    GameId:
      name: id
      in: path
      required: true
      description: Идентификаторът на партията – положително десетично цяло число от най-много 16 цифри без водещи нули, под 2^53.
      schema:
        type: integer
        format: int64
        minimum: 1
        maximum: 9007199254740991
      example: 4100000000001
    SessionId:
      name: id
      in: path
      required: true
      description: Идентификаторът на сесията, както го дава `GET /auth/sessions`, записан без водещи нули (`03` не е сесия 3).
      schema:
        type: integer
        format: int64
        minimum: 1
      example: 3
    Username:
      name: username
      in: path
      required: true
      description: Потребителското име на играча, без значение от регистъра на буквите. Сървърът приема всяко име от 2 до 24 знака от `[A-Za-z0-9_.-]` (най-широкото правило, което някога е позволявал).
      schema:
        type: string
        pattern: '^[A-Za-z0-9_.-]{2,24}$'
      example: alice
    BeforeQuery:
      name: before
      in: query
      required: false
      description: Идентификатор на партия; изброяват се само по-старите партии. Подайте `next` на предишната страница. Празна стойност се смята за липсваща.
      schema:
        type: integer
        format: int64
        minimum: 1
        maximum: 9007199254740991
      example: 4100000000002
    LimitQuery:
      name: limit
      in: query
      required: false
      description: Размерът на страницата, от 1 до 50. По-голямо число от най-много 3 цифри се смята за 50; празна стойност се смята за липсваща.
      schema:
        type: integer
        minimum: 1
        maximum: 999
        default: 20
    LinkTokenQuery:
      name: token
      in: query
      required: false
      description: Токенът на връзката от имейла (43 знака base64url). При липсващ или грешен токен се показва страницата „link invalid or expired“ („връзката е невалидна или изтекла“).
      schema:
        type: string
        pattern: '^[A-Za-z0-9_-]{43}$'
      example: asfioO4P0r_Zn5DxWzcQuF6Drgxu7wXGjCyS-UFbTaw
  headers:
    CacheControl:
      description: Всеки отговор на сървъра забранява кеширането.
      schema:
        type: string
        const: no-store
    RetryAfter:
      description: Секунди за изчакване преди нов опит; същата стойност като `retryAfter` в тялото.
      schema:
        type: integer
        minimum: 1
      example: 30
    WwwAuthenticate:
      description: Предизвикателството за удостоверяване bearer, с `error="invalid_token"` за невалиден токен.
      schema:
        type: string
        enum:
          - Bearer realm="scacelith"
          - Bearer realm="scacelith", error="invalid_token"
    ConnectionClose:
      description: Сървърът затваря връзката след този отговор.
      schema:
        type: string
        const: close
  responses:
    BadRequest:
      description: '`invalid_request` (с `field`, когато грешката е в поле от тялото) или `invalid_json`.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalidRequest:
              summary: Поле нарушава схемата
              value:
                error: invalid_request
                message: '"password" is required'
                field: password
            invalidJson:
              summary: Тялото не е JSON
              value:
                error: invalid_json
                message: The body is not valid JSON.
            weakPassword:
              summary: Слаба парола
              value:
                error: weak_password
                message: The password must have at least 10 characters.
                reason: too_short
            invalidPgn:
              summary: PGN, който не може да бъде прочетен
              value:
                error: invalid_pgn
                message: illegal move 'Ke3'
                line: 1
                column: 13
    Unauthorized:
      description: '`unauthorized` (няма заглавка `Authorization`) или `invalid_token` (токенът е с грешен формат, изтекъл, отменен или принадлежи на изтрит акаунт).'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        WWW-Authenticate:
          $ref: '#/components/headers/WwwAuthenticate'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            unauthorized:
              summary: Няма сесия
              value:
                error: unauthorized
                message: Log in first.
            invalidToken:
              summary: Невалиден токен
              value:
                error: invalid_token
                message: The session is invalid or has expired; log in again.
    Forbidden:
      description: Заявката е отказана.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalidPassword:
              summary: Грешна парола при повторното удостоверяване
              value:
                error: invalid_password
                message: Wrong password.
            banned:
              summary: Блокиран акаунт
              value:
                error: banned
                message: This account is banned.
                until: 1791487639708
    NotFound:
      description: '`not_found` или функция, която този сървър е изключил.'
      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`: тялото не пристигна в рамките на 10 секунди. Сървърът затваря връзката.'
      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: Заявката е в конфликт със състоянието на сървъра.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: username_taken
            message: This username is already taken.
    Gone:
      description: '`sso_expired`: опитът за вход с Google или билетът е непознат, използван или изтекъл; започнете входа отначало.'
      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`: тялото надвишава `HTTP_BODY_LIMIT` (16 384 байта по подразбиране). Сървърът затваря връзката.'
      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`: целта на заявката надвишава 4096 знака.'
      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`: тялото не е `application/json` или кодировката му не е 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`: партията има повече от `GIF_MAX_PLIES` (600) полухода.'
      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`: решете предизвикателството `pow` и изпратете отново същата заявка с `pow: { challenge, nonce }`. `reason` казва защо: `required`, `malformed`, `signature`, `endpoint`, `network`, `expired`, `bits`, `work` или `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` (лимит на честотата, бюджетът на акаунта или опашката за хеширане на пароли) или `too_many_attempts` (ограничение на акаунта), с `retryAfter` и заглавка `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: Лимит на честотата
              value:
                error: rate_limited
                message: Too many requests; try again later.
                retryAfter: 30
            tooManyAttempts:
              summary: Твърде много неуспехи в този акаунт
              value:
                error: too_many_attempts
                message: Too many attempts; wait before trying again.
                retryAfter: 4
    InternalError:
      description: '`internal_error`: неочакван срив. Сървърът го записва в лога.'
      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`: грешен `state` или `iss`, или Google е отказал кода или е изпратил ID токен, който не минава проверката (включително код или ID токен на другия клиент – на играта или на уебсайта). Никога не съдържа текста на Google.'
      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` или `timeout`: опитайте отново след `retryAfter`, когато е дадено.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            serverBusy:
              summary: Сървърът е зает
              value:
                error: server_busy
                message: The server is busy; try again in a few seconds.
                retryAfter: 9
            busy:
              summary: Базата данни остана заключена (четене)
              value:
                error: busy
                message: Try again shortly.
                retryAfter: 1
            timeout:
              summary: Сървърът се забави твърде много
              value:
                error: timeout
                message: The server took too long to answer; try again.
    GifFile:
      description: Анимираният GIF като прикачен файл.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Content-Disposition:
          description: '`attachment; filename="scacelith-<id>.gif"` за партия от този сървър, `attachment; filename="scacelith-game.gif"` за PGN, изпратен с `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: Размерът на файла в байтове.
          schema:
            type: integer
            minimum: 1
      content:
        image/gif:
          schema:
            type: string
            format: binary
    HtmlPage:
      description: HTML страница.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlBadRequest:
      description: HTML страница, която обяснява отказа.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlRequestTimeout:
      description: Формулярът не пристигна в рамките на 10 секунди (HTML страница). Сървърът затваря връзката.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Connection:
          $ref: '#/components/headers/ConnectionClose'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlConflict:
      description: HTML страница, която обяснява конфликта.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlPayloadTooLarge:
      description: Формулярът надвишава `HTTP_BODY_LIMIT` (HTML страница). Сървърът затваря връзката.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Connection:
          $ref: '#/components/headers/ConnectionClose'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlUnsupportedMediaType:
      description: Тялото не е нито формуляр (`application/x-www-form-urlencoded`), нито JSON, или кодировката му не е UTF-8 (HTML страница).
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlTooManyRequests:
      description: Лимит на честотата (HTML страница), с `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: Неочакван срив (HTML страница със заглавие „Server error“). Сървърът го записва в лога.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlServiceUnavailable:
      description: Сървърът е зает (базата данни остана заключена или сървърът сериозно изостава; с `Retry-After`) или се забави твърде много (HTML страница със заглавие „Server error“).
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
  schemas:
    Error:
      type: object
      description: Единствената форма на всеки отговор с грешка на JSON API.
      required:
        - error
        - message
      properties:
        error:
          type: string
          pattern: '^[a-z][a-z0-9_]*$'
          description: Кодът на грешката в `snake_case`. По него клиентът избира какво да покаже.
          examples:
            - rate_limited
        message:
          type: string
          description: Изречение на английски, предназначено за логовете и като резервен вариант.
        retryAfter:
          type: integer
          minimum: 1
          description: Секунди за изчакване преди нов опит; присъства само при откази, които отпадат с времето. Тогава отговорът носи и заглавка `Retry-After` със същата стойност, с изключение на 503 `busy` при четенето на историята, на партиите, на играчите и на класацията.
        field:
          type: string
          description: Полето или параметърът на заявката, в който е грешката (`invalid_request`, `invalid_option`, както и `invalid_cursor`, `invalid_limit` и `invalid_filter` на `GET /account/games`). Вложено поле се записва с точка (`pow.nonce`).
        reason:
          type: string
          description: 'Защо: правилото, което `weak_password` нарушава, или защо е поискано или отказано доказателство за работа (`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: 'Само при `banned`: краят на блокирането (мс от Unix епохата), `null` при постоянно блокиране.'
        line:
          type: integer
          minimum: 1
          description: 'Само при `invalid_pgn`: редът на грешката, от 1.'
        column:
          type: integer
          minimum: 1
          description: 'Само при `invalid_pgn`: колоната на грешката, от 1, в знаци.'
    PowChallenge:
      type: object
      description: Предизвикателство за доказателство за работа (`pow` на 428 `pow_required`).
      required:
        - challenge
        - bits
        - expiresAt
      properties:
        challenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{16,400}\.[A-Za-z0-9_-]{43}$'
          description: Подписаното предизвикателство, което се връща такова, каквото е.
        bits:
          type: integer
          minimum: 1
          maximum: 26
          description: Броят на водещите нулеви битове, които `SHA-256(challenge + ":" + nonce)` трябва да има.
        expiresAt:
          type: integer
          format: int64
          description: Кога изтича предизвикателството (мс от Unix епохата) – 2 минути след издаването му.
    PowAnswer:
      type: object
      description: Отговорът на предизвикателство за доказателство за работа.
      additionalProperties: false
      required:
        - challenge
        - nonce
      properties:
        challenge:
          type: string
          minLength: 16
          maxLength: 512
          description: '`challenge` от отговора 428, точно както е в него.'
        nonce:
          type: string
          pattern: '^[0-9]{1,20}$'
          description: Десетичен низ от най-много 20 цифри, такъв, че `SHA-256(challenge + ":" + nonce)` да започва с `bits` нулеви бита.
    ServerInfo:
      type: object
      description: Какво е нужно на клиента, преди да влезе или да се свърже.
      required:
        - name
        - serverId
        - motd
        - protocol
        - wsPort
        - wsPath
        - registration
        - emailVerification
        - sso
        - mfa
        - pow
        - categories
        - limits
      properties:
        name:
          type: string
          description: Името на сървъра (`SERVER_NAME`).
        serverId:
          type:
            - string
            - 'null'
          format: uuid
          description: UUID, който базата данни получава при първото си стартиране. Той остава същият при рестартиране. Той е `null`, когато базата данни не може да го даде.
        motd:
          type: string
          description: Съобщението на деня (`SERVER_MOTD`), което може да е празно.
        protocol:
          type: object
          description: Протоколът на WebSocket (`PROTOCOL.md`).
          required:
            - min
            - max
            - schema
            - subprotocol
          properties:
            min:
              type: integer
              minimum: 1
              description: Най-старата версия на протокола, която сървърът поддържа.
            max:
              type: integer
              minimum: 1
              description: Най-новата версия на протокола, която сървърът поддържа.
            schema:
              type: integer
              format: int64
              minimum: 0
              maximum: 4294967295
              description: 'Отпечатъкът на схемата на протокола (първите 4 байта от SHA-256 на каноничната ѝ форма, като цяло число без знак: `0xc649140f`, 3326678031, за минорна версия 2 на протокола); информативен, никога не се сравнява.'
            subprotocol:
              type: string
              description: Токенът на подпротокола на WebSocket (`Sec-WebSocket-Protocol`).
        wsPort:
          type: integer
          minimum: 0
          maximum: 65535
          description: Портът на WebSocket, който играчите използват (`PUBLIC_WS_PORT`, иначе `WS_PORT`, иначе `API_PORT`).
        wsPath:
          type: string
          const: /ws
          description: Пътят на WebSocket.
        registration:
          type: string
          enum:
            - open
            - closed
          description: Дали могат да се създават нови акаунти.
        emailVerification:
          type: boolean
          description: Дали новите акаунти потвърждават адреса си (`REQUIRE_EMAIL_VERIFICATION`).
        sso:
          type: object
          required:
            - google
            - googleWeb
          properties:
            google:
              type: boolean
              description: Дали на играта се предлага вход с Google.
            googleWeb:
              type: boolean
              description: Дали входът с Google се предлага и на уебсайт (`POST /auth/sso/google/web/start`).
        mfa:
          type: boolean
          const: true
          description: Двуфакторното удостоверяване е винаги достъпно.
        pow:
          type: object
          required:
            - register
          properties:
            register:
              type: integer
              minimum: 0
              maximum: 26
              description: Битовете на доказателството за работа, които регистрацията изисква (0, ако не изисква).
        categories:
          type: array
          description: Официалните контроли на времето, тези с рейтинг (`RATED_CATEGORIES`), в реда на сървъра. Всяка друга контрола на времето е `custom`.
          items:
            $ref: '#/components/schemas/Category'
        limits:
          type: object
          description: Правилата, които клиентът може да провери, преди да изпрати формуляр.
          required:
            - usernameMin
            - usernameMax
            - usernamePattern
            - passwordMinLength
            - passwordMaxBytes
            - customTimeControls
            - reportsPerDay
            - wsMaxMessageBytes
          properties:
            usernameMin:
              type: integer
              description: Минималната дължина на потребителското име (`USERNAME_MIN`).
            usernameMax:
              type: integer
              description: Максималната дължина на потребителското име (`USERNAME_MAX`).
            usernamePattern:
              type: string
              description: Регулярният израз, на който трябва да отговаря новото потребителско име.
            passwordMinLength:
              type: integer
              description: Минималната дължина на паролата, в знаци (`PASSWORD_MIN_LENGTH`).
            passwordMaxBytes:
              type: integer
              description: Максималната дължина на паролата, в байтове UTF-8.
            customTimeControls:
              type: boolean
              description: Дали предизвикателствата и частните партии могат да използват контроли на времето по избор.
            reportsPerDay:
              type: integer
              description: Колко сигнала може да подаде играч за 24 часа (`REPORTS_PER_DAY`).
            wsMaxMessageBytes:
              type: integer
              description: Най-голямото съобщение по WebSocket, което клиентът може да изпрати, в байтове.
      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: Официална контрола на времето.
      required:
        - id
        - baseSec
        - incSec
      properties:
        id:
          type: string
          pattern: '^[0-9]+\+[0-9]+$'
          description: 'Идентификаторът на категорията: минути и секунди добавка (`3+2`).'
        baseSec:
          type: number
          minimum: 0
          description: Основното време, в секунди.
        incSec:
          type: number
          minimum: 0
          description: Добавката за ход, в секунди.
    AccountView:
      type: object
      description: Акаунтът такъв, какъвто го вижда неговият играч (`user` в отговорите при вход и в `GET /account/me`).
      required:
        - id
        - username
        - email
        - emailVerified
        - mfaEnabled
        - googleLinked
        - hasPassword
        - acceptChallenges
        - createdAt
        - lastLoginAt
        - pendingEmail
      properties:
        id:
          type: integer
          format: int64
          description: Идентификаторът на акаунта.
        username:
          type: string
          description: Потребителското име.
        email:
          type: string
          format: email
          description: Имейл адресът.
        emailVerified:
          type: boolean
          description: Дали адресът е потвърден.
        mfaEnabled:
          type: boolean
          description: Дали двуфакторното удостоверяване е включено.
        googleLinked:
          type: boolean
          description: Дали е свързан акаунт в Google.
        hasPassword:
          type: boolean
          description: '`false` за акаунт, създаден чрез Google, който още не е задал парола.'
        acceptChallenges:
          type: string
          enum:
            - all
            - none
          description: Дали се приемат преки предизвикателства по име (вж. `PUT /account/preferences`).
        createdAt:
          type: integer
          format: int64
          description: Кога е създаден акаунтът (мс от Unix епохата).
        lastLoginAt:
          type:
            - integer
            - 'null'
          format: int64
          description: Последният вход (мс от Unix епохата) или `null`.
        pendingEmail:
          type:
            - string
            - 'null'
          format: email
          description: Новият адрес при смяна на имейла, която чака своята връзка, или `null`.
    SessionAnswer:
      type: object
      description: Нова сесия.
      required:
        - token
        - expiresAt
        - user
      properties:
        token:
          type: string
          pattern: '^sct_[A-Za-z0-9_-]{43}$'
          description: Токенът на сесията, за заглавката `Authorization` и за WebSocket.
        expiresAt:
          type: integer
          format: int64
          description: Абсолютният край на сесията (мс от Unix епохата); лимитът за неактивност може да я прекрати по-рано.
        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: Втората стъпка на вход с двуфакторно удостоверяване; продължете с `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: Токенът на стъпката, за `POST /auth/login/mfa`.
        expiresIn:
          type: integer
          const: 300
          description: Оставащите секунди за завършване на стъпката.
    LoginAnswer:
      description: Сесия или втората стъпка на вход с двуфакторно удостоверяване.
      oneOf:
        - $ref: '#/components/schemas/SessionAnswer'
        - $ref: '#/components/schemas/MfaChallenge'
    SsoNeedsUsername:
      type: object
      description: Първи вход с Google; продължете с `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: Билетът за `POST /auth/sso/complete`, валиден 10 минути.
        suggestedUsername:
          type: string
          description: Потребителско име, образувано от името в Google или от адреса, или `""`, когато нищо не пасва.
    SsoNeedsPassword:
      type: object
      description: Акаунт с парола използва адреса; продължете с `POST /auth/sso/google/link`. Още нищо не е свързано.
      required:
        - needsPassword
        - linkTicket
        - username
        - expiresIn
      properties:
        needsPassword:
          type: boolean
          const: true
        linkTicket:
          type: string
          pattern: '^sso_[A-Za-z0-9_-]{43}$'
          description: Билетът за `POST /auth/sso/google/link`.
        username:
          type: string
          description: Потребителското име на този акаунт (вижда го само този, който е доказал адреса пред Google).
        expiresIn:
          type: integer
          const: 600
          description: Секундите, през които билетът остава валиден.
    SsoFinishAnswer:
      description: Отговорът на `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: Опит за вход с Google.
      required:
        - attemptId
        - authUrl
        - state
        - expiresIn
      properties:
        attemptId:
          type: string
          pattern: '^sso_[A-Za-z0-9_-]{43}$'
          description: Опитът, за `POST /auth/sso/google/finish`.
        authUrl:
          type: string
          format: uri
          description: URL адресът на Google, който да се отвори в системния браузър, след като бъде проверен.
        state:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: Стойността `state`, която Google трябва да върне.
        expiresIn:
          type: integer
          const: 600
          description: Секундите, през които опитът остава валиден.
    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: Потребителското име; след това се прилагат правилата на сървъра (вж. описанието).
        email:
          type: string
          minLength: 1
          maxLength: 254
          description: Имейл адресът. Интервалите в началото и в края се премахват и той се записва с малки букви.
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Паролата; след това се прилагат правилата за паролата.
        pow:
          $ref: '#/components/schemas/PowAnswer'
    LoginRequest:
      type: object
      additionalProperties: false
      required:
        - login
        - password
      properties:
        login:
          type: string
          minLength: 1
          maxLength: 254
          description: Потребителското име или имейл адресът (всеки текст с `@`).
        password:
          type: string
          minLength: 1
          maxLength: 1024
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
        pow:
          $ref: '#/components/schemas/PowAnswer'
    ClientLabel:
      type: string
      maxLength: 64
      description: Незадължително. Показва се в списъка на устройствата с активен вход (например `Scacelith 1.4 (Windows)`).
    MfaLoginRequest:
      type: object
      additionalProperties: false
      required:
        - mfaToken
      properties:
        mfaToken:
          type: string
          minLength: 1
          maxLength: 64
          description: '`mfaToken` от отговора при вход (или от отговора при вход с Google).'
        code:
          type: string
          maxLength: 32
          description: Незадължително. 6-цифрен код от приложението за удостоверяване или код за възстановяване.
        recoveryCode:
          type: string
          maxLength: 32
          description: Незадължително. Код за възстановяване. Нужно е едно от `code` и `recoveryCode`.
    EmailRequest:
      type: object
      additionalProperties: false
      required:
        - email
      properties:
        email:
          type: string
          minLength: 1
          maxLength: 254
          description: Имейл адресът.
    PasswordResetRequest:
      type: object
      additionalProperties: false
      required:
        - token
        - newPassword
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: Параметърът `token` на връзката за нулиране.
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Новата парола; прилагат се правилата за паролата при регистрация.
    SsoStartRequest:
      type: object
      additionalProperties: false
      required:
        - codeChallenge
        - redirectPort
      properties:
        codeChallenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: PKCE предизвикателството S256 на `codeVerifier` на клиента (`BASE64URL(SHA-256(codeVerifier))`).
        redirectPort:
          type: integer
          minimum: 1024
          maximum: 65535
          description: Портът на слушателя на клиента на `127.0.0.1`.
    SsoWebStartRequest:
      type: object
      additionalProperties: false
      required:
        - codeChallenge
      properties:
        codeChallenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: PKCE предизвикателството S256 на `codeVerifier` на страницата (`BASE64URL(SHA-256(codeVerifier))`).
    SsoFinishRequest:
      type: object
      additionalProperties: false
      required:
        - attemptId
        - codeVerifier
        - state
        - code
      properties:
        attemptId:
          type: string
          minLength: 1
          maxLength: 64
          description: '`attemptId` от `POST /auth/sso/google/start`.'
        codeVerifier:
          type: string
          pattern: '^[A-Za-z0-9._~-]{43,128}$'
          description: PKCE верификаторът; неговият SHA-256 трябва да съвпада с предизвикателството, дадено на `start`.
        state:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: '`state` във вида, в който Google го е върнал.'
        code:
          type: string
          minLength: 1
          maxLength: 2048
          pattern: '^[\x21-\x7E]+$'
          description: '`code` във вида, в който Google го е върнал (печатими ASCII знаци без интервали).'
        iss:
          type: string
          minLength: 1
          maxLength: 256
          description: Незадължително. `iss` във вида, в който Google го е върнал, ако го е върнал.
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
    SsoLinkRequest:
      type: object
      additionalProperties: false
      required:
        - linkTicket
        - password
      properties:
        linkTicket:
          type: string
          minLength: 1
          maxLength: 64
          description: '`linkTicket` от `finish`, валиден 10 минути.'
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Паролата на акаунта.
        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` от `finish`, валиден 10 минути.'
        username:
          type: string
          minLength: 1
          maxLength: 64
          description: Потребителското име; прилагат се правилата за регистрация.
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
    SessionList:
      type: object
      required:
        - sessions
      properties:
        sessions:
          type: array
          description: Активните сесии, като най-скоро използваната е първа.
          items:
            $ref: '#/components/schemas/SessionEntry'
    SessionEntry:
      type: object
      description: Активна сесия (устройство с активен вход).
      required:
        - id
        - createdAt
        - lastSeenAt
        - expiresAt
        - clientLabel
        - current
      properties:
        id:
          type: integer
          format: int64
          description: Идентификаторът на сесията, за `DELETE /auth/sessions/{id}`.
        createdAt:
          type: integer
          format: int64
          description: Входът (мс от Unix епохата).
        lastSeenAt:
          type: integer
          format: int64
          description: Последното използване (мс от Unix епохата), обновявано най-много веднъж на 5 минути (по-късно, докато сървърът сериозно изостава).
        expiresAt:
          type: integer
          format: int64
          description: Абсолютният край (мс от Unix епохата); лимитът за неактивност може да прекрати сесията по-рано.
        clientLabel:
          type:
            - string
            - 'null'
          description: '`clientLabel` при входа или `null`, когато не е изпратен такъв.'
        current:
          type: boolean
          description: Дали това е сесията, от която идва заявката.
    AccountMe:
      type: object
      required:
        - user
        - ratings
        - sanctions
        - ban
      properties:
        user:
          $ref: '#/components/schemas/AccountView'
        ratings:
          type: array
          description: По един запис за всяка категория, в която играчът е играл партии с рейтинг.
          items:
            $ref: '#/components/schemas/RatingSummary'
        sanctions:
          type: array
          description: Активните санкции.
          items:
            $ref: '#/components/schemas/ActiveSanction'
        ban:
          description: '`{ until }`, докато акаунтът е блокиран, иначе `null`.'
          oneOf:
            - $ref: '#/components/schemas/Ban'
            - type: 'null'
    RatingSummary:
      type: object
      description: Рейтинговият запис на една категория.
      required:
        - category
        - rating
        - games
        - wins
        - draws
        - losses
        - peak
        - provisional
      properties:
        category:
          type: string
          description: Идентификаторът на категорията.
        rating:
          type: integer
          description: Рейтингът.
        games:
          type: integer
          minimum: 0
          description: Партиите, изиграни в категорията (зачетени или не).
        wins:
          type: integer
          minimum: 0
        draws:
          type: integer
          minimum: 0
        losses:
          type: integer
          minimum: 0
        peak:
          type: integer
          description: Най-високият достигнат рейтинг.
        provisional:
          type: boolean
          description: '`true`, докато рейтингът още не е установен или има по-малко от `PROVISIONAL_GAMES` зачетени партии (играта го показва като `1510?`).'
    ActiveSanction:
      type: object
      required:
        - kind
        - reason
        - startsAt
        - endsAt
      properties:
        kind:
          type: string
          enum:
            - ban
            - mm_block
            - warning
          description: Видът на санкцията.
        reason:
          type:
            - string
            - 'null'
          description: Посочената причина, ако има такава.
        startsAt:
          type: integer
          format: int64
          description: Началото (мс от Unix епохата).
        endsAt:
          type:
            - integer
            - 'null'
          format: int64
          description: Краят (мс от Unix епохата), `null`, когато санкцията е постоянна.
    Ban:
      type: object
      required:
        - until
      properties:
        until:
          type:
            - integer
            - 'null'
          format: int64
          description: Краят на блокирането (мс от Unix епохата), `null`, когато е постоянно.
    Preferences:
      type: object
      additionalProperties: false
      required:
        - acceptChallenges
      properties:
        acceptChallenges:
          type: string
          enum:
            - all
            - none
          description: '`all`, за да се приемат преки предизвикателства по име, `none`, за да се отказват.'
    PasswordChangeRequest:
      type: object
      additionalProperties: false
      required:
        - currentPassword
        - newPassword
      properties:
        currentPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Текущата парола.
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Новата парола; прилагат се правилата за паролата при регистрация.
    PasswordOnlyRequest:
      type: object
      additionalProperties: false
      required:
        - password
      properties:
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Паролата на акаунта.
    TotpEnableRequest:
      type: object
      additionalProperties: false
      required:
        - code
      properties:
        code:
          type: string
          pattern: '^[0-9]{6}$'
          description: Код от чакащия таен ключ, точно 6 цифри.
    ReauthCredentials:
      type: object
      additionalProperties: false
      required:
        - password
      properties:
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Паролата на акаунта.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: Незадължително; нужно при двуфакторно удостоверяване (или `recoveryCode`). Код от приложението за удостоверяване или код за възстановяване.
        recoveryCode:
          type: string
          minLength: 1
          maxLength: 32
          description: Незадължително. Код за възстановяване (`xxxx-xxxx-xx`; главните и малките букви, интервалите и тиретата нямат значение).
    RecoveryCodesRequest:
      type: object
      additionalProperties: false
      required:
        - password
        - code
      properties:
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Паролата на акаунта.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: Код от приложението за удостоверяване (код за възстановяване се отказва).
    EmailChangeRequest:
      type: object
      additionalProperties: false
      required:
        - newEmail
        - password
      properties:
        newEmail:
          type: string
          minLength: 1
          maxLength: 254
          description: Новият адрес; интервалите в началото и в края се премахват и буквите стават малки, след което той се проверява като адрес при регистрация.
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Паролата на акаунта.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: Незадължително; нужно при двуфакторно удостоверяване (или `recoveryCode`). Код от приложението за удостоверяване или код за възстановяване.
        recoveryCode:
          type: string
          minLength: 1
          maxLength: 32
          description: Незадължително. Код за възстановяване.
    TotpSetup:
      type: object
      description: Чакащият таен ключ за приложението за удостоверяване.
      required:
        - secret
        - uri
        - algorithm
        - digits
        - period
      properties:
        secret:
          type: string
          pattern: '^[A-Z2-7]+$'
          description: Тайният ключ в base32, за въвеждане в приложението.
        uri:
          type: string
          format: uri
          description: URI `otpauth://totp/...`, който да се покаже като QR код.
        algorithm:
          type: string
          const: SHA1
        digits:
          type: integer
          const: 6
        period:
          type: integer
          const: 30
          description: Секунди на стъпка.
    RecoveryCodeList:
      type: array
      description: 10-те кода за възстановяване, показани само този път. Всеки от тях работи веднъж.
      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: Една от страните в партия.
      required:
        - name
        - rating
        - ratingAfter
        - ratingDiff
      properties:
        name:
          type: string
          description: Името в записа на партията (`deleted#<id>` за изтрит акаунт).
        rating:
          type:
            - integer
            - 'null'
          description: Рейтингът в началото, `null`, когато е неизвестен.
        ratingAfter:
          type:
            - integer
            - 'null'
          description: Рейтингът след партията. Партия с рейтинг винаги го има; `null` само за партия, която не се брои за рейтингите (приятелска, с контрола по избор, анулирана).
        ratingDiff:
          type:
            - integer
            - 'null'
          description: Промяната на рейтинга, `0`, когато правилата за рейтинга оставят рейтинга непроменен (нулевият резултат по правилата на FIDE, противник без рейтинг, първите партии на играч без рейтинг); `null` както при `ratingAfter`.
    GameSummary:
      type: object
      description: Обобщението на записана партия.
      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: Идентификаторът на партията.
        category:
          type: string
          description: Идентификаторът на официалната категория (`3+2`) или `custom`.
        rated:
          type: boolean
          description: Дали партията се брои за рейтингите.
        timeControl:
          type: string
          pattern: '^[0-9]+\+[0-9]+$'
          description: Контролата на времето в секунди (`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` (вж. кодовете на партиите).'
        reason:
          type: integer
          minimum: 0
          maximum: 255
          description: Кодът на причината за края (вж. кодовете на партиите).
        result:
          type: string
          enum:
            - 1-0
            - 0-1
            - 1/2-1/2
            - '*'
          description: Резултатът, `*` за анулирана партия.
        termination:
          $ref: '#/components/schemas/Termination'
        plies:
          type: integer
          minimum: 0
          description: Броят на полуходовете.
        startedAt:
          type: integer
          format: int64
          description: Началото (мс от Unix епохата).
        endedAt:
          type: integer
          format: int64
          description: Краят (мс от Unix епохата).
    Termination:
      type: string
      description: Името на причината за края (вж. кодовете на партиите); `Unknown` за код, който протоколът не познава.
      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: Обобщение на партия от гледна точка на един играч.
      allOf:
        - $ref: '#/components/schemas/GameSummary'
        - type: object
          required:
            - color
          properties:
            color:
              type: string
              enum:
                - white
                - black
              description: Страната на играча.
    HistoryGameSummary:
      description: Партия от историята на влезлия играч.
      allOf:
        - $ref: '#/components/schemas/PlayerGameSummary'
        - type: object
          required:
            - baseMs
            - incMs
            - outcome
          properties:
            baseMs:
              type: integer
              format: int64
              minimum: 0
              description: Основното време в милисекунди.
            incMs:
              type: integer
              format: int64
              minimum: 0
              description: Добавката в милисекунди.
            outcome:
              type: string
              enum:
                - win
                - loss
                - draw
                - aborted
              description: Резултатът от гледна точка на играча.
    HistoryPage:
      type: object
      required:
        - games
        - next
        - total
      properties:
        games:
          type: array
          description: Партиите на страницата, от най-новата.
          items:
            $ref: '#/components/schemas/HistoryGameSummary'
        next:
          type:
            - integer
            - 'null'
          format: int64
          description: Идентификаторът, който да се подаде като `before` за следващата страница, или `null` на последната страница.
        total:
          type: integer
          minimum: 0
          description: Броят на партиите, които отговарят на филтъра, във всички страници.
    MoveRecord:
      type: object
      description: Един полуход от партия.
      required:
        - uci
        - spentMs
        - clockMs
      properties:
        uci:
          type: string
          pattern: '^[a-h][1-8][a-h][1-8][nbrq]?$'
          description: Ходът в нотация UCI, с добавено `n`, `b`, `r` или `q` при произвеждане.
        spentMs:
          type:
            - integer
            - 'null'
          minimum: 0
          description: Времето, отчетено за хода (мс), `null`, когато записът не го съдържа.
        clockMs:
          type:
            - integer
            - 'null'
          minimum: 0
          description: Часовникът на играча, направил хода, след хода (мс), `null`, когато записът не го съдържа.
    PgnTagsView:
      type: object
      description: Основните PGN тагове, за показване. `Termination` тук е името на причината за края; PGN файлът използва стандартните стойности на PGN.
      required:
        - Event
        - Site
        - Date
        - Round
        - White
        - Black
        - Result
        - WhiteElo
        - BlackElo
        - TimeControl
        - Termination
        - PlyCount
      properties:
        Event:
          type: string
          description: '`<SERVER_NAME> rated <category>` или `<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: Датата на началото по 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: Рейтингът в началото или `"-"`.
          oneOf:
            - type: integer
            - type: string
              const: '-'
        BlackElo:
          description: Рейтингът в началото или `"-"`.
          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: Запис на партия с ходовете и часовниците ѝ. Има полетата на обобщението на партия, без `color` и `outcome`.
      allOf:
        - $ref: '#/components/schemas/GameSummary'
        - type: object
          required:
            - baseMs
            - incMs
            - statusName
            - rematchOf
            - moves
            - pgn
          properties:
            baseMs:
              type: integer
              format: int64
              minimum: 0
              description: Основното време в милисекунди.
            incMs:
              type: integer
              format: int64
              minimum: 0
              description: Добавката в милисекунди.
            statusName:
              type: string
              enum:
                - WhiteWins
                - BlackWins
                - Draw
                - Aborted
              description: Името на `status`.
            rematchOf:
              type:
                - integer
                - 'null'
              format: int64
              description: Идентификаторът на партията, на която тази е реванш, или `null`.
            moves:
              type: array
              description: По един елемент за всеки полуход.
              items:
                $ref: '#/components/schemas/MoveRecord'
            pgn:
              $ref: '#/components/schemas/PgnTagsView'
            you:
              type: string
              enum:
                - white
                - black
              description: Само когато играчът на токена е играл тази партия; неговата страна.
            reportable:
              type: boolean
              description: Само когато играчът на токена е играл тази партия; `true`, когато `POST /reports` би приел сега сигнал срещу противника за тази партия (партията е завършила през последните 7 дни, дневната квота не е изчерпана и срещу противника още не е подаден сигнал за тази партия).
    PlayerProfile:
      type: object
      required:
        - username
        - createdAt
        - ratings
        - games
      properties:
        username:
          type: string
          description: Потребителското име, както го е написал играчът.
        createdAt:
          type: integer
          format: int64
          description: Кога е създаден акаунтът (мс от Unix епохата).
        ratings:
          type: array
          description: Само официалните категории, в реда на сървъра.
          items:
            $ref: '#/components/schemas/RatingSummary'
        games:
          type: object
          required:
            - total
            - rated
            - wins
            - draws
            - losses
          properties:
            total:
              type: integer
              minimum: 0
              description: Всички записани партии, включително приятелските и анулираните.
            rated:
              type: integer
              minimum: 0
              description: Партиите с рейтинг, сумирани от рейтинговите записи.
            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: Потребителското име, както го е написал играчът.
        games:
          type: array
          description: Партиите на страницата, от най-новата.
          items:
            $ref: '#/components/schemas/PlayerGameSummary'
        next:
          type:
            - integer
            - 'null'
          format: int64
          description: Идентификаторът на последната партия винаги когато страницата е пълна (тогава следващата страница може да е празна), иначе `null`.
    Leaderboard:
      type: object
      required:
        - category
        - minGames
        - updatedAt
        - players
      properties:
        category:
          type: string
          description: Идентификаторът на категорията.
        minGames:
          type: integer
          minimum: 0
          description: Зачетените партии, които са нужни на един запис, за да влезе в списъка (`PROVISIONAL_GAMES`).
        updatedAt:
          type: integer
          format: int64
          description: Кога е изчислена класацията (мс от Unix епохата).
        players:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/LeaderboardEntry'
    LeaderboardEntry:
      type: object
      required:
        - rank
        - username
        - rating
        - games
        - wins
        - draws
        - losses
        - peak
      properties:
        rank:
          type: integer
          minimum: 1
        username:
          type: string
        rating:
          type: integer
        games:
          type: integer
          minimum: 0
        wins:
          type: integer
          minimum: 0
        draws:
          type: integer
          minimum: 0
        losses:
          type: integer
          minimum: 0
        peak:
          type: integer
    GifRequest:
      type: object
      additionalProperties: false
      required:
        - pgn
      properties:
        pgn:
          type: string
          description: Текстът на PGN, най-много 65 536 байта UTF-8. Използва се само първата партия.
        size:
          type: string
          enum:
            - small
            - medium
            - large
          default: medium
          description: Незадължително. Размерът на изображението.
        orientation:
          type: string
          enum:
            - white
            - black
          default: white
          description: Незадължително. Страната в долната част на дъската.
        delayMs:
          type: integer
          minimum: 100
          maximum: 3000
          default: 500
          description: Незадължително. Милисекунди на ход, JSON число с цяла стойност.
        coords:
          type: boolean
          default: true
          description: Незадължително. Дали буквите на вертикалите и цифрите на хоризонталите се изобразяват около дъската.
    ReportRequest:
      type: object
      required:
        - gameId
        - reported
        - category
      properties:
        gameId:
          description: Партията, като цяло число или като низ от 1 до 16 цифри.
          oneOf:
            - type: integer
              format: int64
              minimum: 1
              maximum: 9007199254740991
            - type: string
              pattern: '^[0-9]{1,16}$'
        reported:
          type: string
          minLength: 1
          maxLength: 24
          description: Потребителското име на противника, както е в записа на партията или както е сега, без значение от регистъра на буквите. Не може да е празно.
        category:
          type: string
          enum:
            - cheating
            - abuse
            - other
          description: За какво е сигналът.
        comment:
          type:
            - string
            - 'null'
          description: Незадължително. Най-много 500 знака, след като се премахнат управляващите знаци (освен табулацията и новия ред) и интервалите в началото и в края на текста.
    AccountExport:
      type: object
      description: Всичко, което сървърът пази за акаунта (времената са в мс от Unix епохата).
      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: Кога е направен експортът.
        server:
          type: object
          required:
            - name
            - host
          properties:
            name:
              type: string
              description: '`SERVER_NAME`.'
            host:
              type: string
              description: '`SERVER_PUBLIC_HOST`.'
        notes:
          type: array
          description: Какво съдържа файлът и какво изключва, на прост английски за играча.
          items:
            type: string
        account:
          description: Изгледът на акаунта от `GET /account/me`, с `googleEmail`.
          allOf:
            - $ref: '#/components/schemas/AccountView'
            - type: object
              required:
                - googleEmail
              properties:
                googleEmail:
                  type:
                    - string
                    - 'null'
                  description: Адресът на свързания акаунт в Google или `null`.
        ratings:
          type: array
          description: Пълните рейтингови записи.
          items:
            $ref: '#/components/schemas/ExportRating'
        ratingRefunds:
          type: array
          description: Рейтинговите точки, възстановени, след като е установено, че противник е мамил, сумирани по UTC ден и по категория, от най-новите. Не се посочват нито партиите, нито измамниците.
          items:
            $ref: '#/components/schemas/RatingRefund'
        sessions:
          type: array
          description: Всички записани сесии, от най-новата, без никакъв токен. Изчистването при изтичане на срока за съхранение изтрива изтеклата сесия, а сесията с извършен изход – ден след изхода.
          items:
            $ref: '#/components/schemas/ExportSession'
        securityEvents:
          type: array
          description: Събитията по сигурността, от най-новото, пазени `RETENTION_SECURITY_DAYS` дни.
          items:
            $ref: '#/components/schemas/SecurityEvent'
        sanctions:
          type: array
          description: Всички санкции, включително отменените. Името на модератора никога не се включва.
          items:
            $ref: '#/components/schemas/ExportSanction'
        conduct:
          type: array
          description: Събитията за поведението, записани за партиите на играча с изоставяне, анулиране или неявяване, пазени 30 дни.
          items:
            $ref: '#/components/schemas/ConductEvent'
        reportsFiled:
          type: array
          description: Сигналите, подадени от играча.
          items:
            $ref: '#/components/schemas/FiledReport'
        games:
          type: object
          required:
            - total
            - list
          properties:
            total:
              type: integer
              minimum: 0
              description: Броят на партиите.
            list:
              type: array
              description: Всички партии, от най-новата, като обобщения от `GET /account/games`. Ходовете са в `GET /games/{id}`, а PGN – в `GET /games/{id}/pgn`.
              items:
                $ref: '#/components/schemas/HistoryGameSummary'
    ExportRating:
      description: Пълен рейтингов запис.
      allOf:
        - $ref: '#/components/schemas/RatingSummary'
        - type: object
          required:
            - rated
            - countedGames
            - updatedAt
          properties:
            rated:
              type: boolean
              description: Дали играчът е излязъл от фазата без рейтинг.
            countedGames:
              type: integer
              minimum: 0
              description: Партиите, които са влезли в рейтинга.
            updatedAt:
              type: integer
              format: int64
              description: Кога записът е променен за последен път.
    RatingRefund:
      type: object
      required:
        - day
        - category
        - points
      properties:
        day:
          type: integer
          format: int64
          description: 00:00 UTC на деня (мс от Unix епохата).
        category:
          type: string
        points:
          type: integer
          description: Точките, възстановени през този ден в тази категория.
    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: Кога е извършен изходът от сесията, или `null`.
        clientLabel:
          type:
            - string
            - 'null'
        ip:
          type:
            - string
            - 'null'
          description: Адресът, от който е извършен входът, изтриван след `RETENTION_IP_DAYS` дни.
    SecurityEvent:
      type: object
      description: |-
        Събитие по сигурността. `ip` се дава само за действия, извършени с активен вход, с паролата на акаунта (и втория фактор) или с връзка, изпратена на адреса му: `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` и `account_exported`; всеки друг вид има `ip: null`, а `ip` се изтрива след `RETENTION_IP_DAYS` дни.

        `detail` пази само тези полета: `login`: `method`; `sso_login`, `sso_account_created`: `provider`; `sso_linked`: `provider` и `method` (`password` или `password+totp`); `login_failed`: `failures`; `login_lockout`: `retryAfterMs`; `mfa_failed`: `attempts`; `recovery_code_used`: `remaining`; `reauth_failed`: `factor`; `session_revoked` и `sessions_revoked_all`: `reason`; `email_change_refused`: `reason`; `sanction_auto`: `kind`, `gameId`, `until`. Събитие `moderator_action` пази само `{ action }`, за действията `ban`, `unban`, `reset_mfa`, `verify_email` и `revoke_sessions` (другите действия на модераторите са изключени). Всеки друг вид има `detail: null`. Събитията `rating_refund` са изключени: точките им са в `ratingRefunds`.
      required:
        - kind
        - at
        - ip
        - detail
      properties:
        kind:
          type: string
          description: Видът на събитието (`login`, `password_changed`…).
        at:
          type: integer
          format: int64
          description: Кога се е случило.
        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: Текущото публично име на играча, срещу когото е подаден сигналът.
        category:
          type: string
          enum:
            - cheating
            - abuse
            - other
        comment:
          type:
            - string
            - 'null'
        createdAt:
          type: integer
          format: int64
        status:
          type: string
          enum:
            - open
            - closed
          description: Дали сигналът е все още отворен (не се казва дали играчът, срещу когото е подаден, е бил санкциониран).
    LinkTokenForm:
      type: object
      additionalProperties: false
      required:
        - token
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: Токенът на връзката от имейла (скритото поле на формуляра на страницата).
    ResetPasswordForm:
      type: object
      additionalProperties: false
      required:
        - token
        - newPassword
        - confirmPassword
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: Токенът на връзката за нулиране (скритото поле на формуляра).
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Новата парола; прилагат се правилата за паролата при регистрация.
        confirmPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Новата парола отново; трябва да е същата.
    HtmlDocument:
      type: string
      contentMediaType: text/html
      description: HTML страница в UTF-8 (`text/html; charset=utf-8`), без JavaScript и без външни ресурси.
