openapi: 3.1.1
info:
  title: A Scacelith-szerver HTTPS API-ja
  version: "1.1.0"
  summary: Minden Scacelith-szerver HTTPS API-ja, a hivatalosé és a közösségi szervereké egyaránt.
  description: |-
    Minden Scacelith-szerver, a hivatalos `caissa.scacelith.com` és bármely közösségi szerver is, ezt a HTTPS API-t szolgálja ki. A játék mindenre ezt használja, ami nem maga a játszma: a regisztrációra és a bejelentkezésre (a kétlépcsős azonosítást és a Google-t is beleértve), a fiókoldalra, a korábbi játszmákra, a PGN-letöltésekre és a játszmák animált GIF-jeire, a bejelentkezett eszközökre, az adatok letöltésére, a fiók törlésére és a jelentésekre.

    Az élő játék (párosítás, kihívások, lépések, órák) ugyanannak a szervernek a WebSocket-kapcsolatán (`wss://<host>/ws`) zajlik, amely ennek az API-nak egy munkamenet-tokenjével nyílik meg; leírása a `PROTOCOL.md` fájlban található, nem itt.

    Az alábbi alapértelmezett értékek egy módosítatlan konfigurációjú szerverre érvényesek; egy közösségi szerver megváltoztathatja őket (a `CONFIG.md` minden beállítást megnevez).

    ## Alap-URL, port és átvitel

    - Hivatalos szerver: `https://caissa.scacelith.com/api/v1`, 443-as TCP-port.
    - Közösségi szerver: `https://<SERVER_PUBLIC_HOST>[:<port>]/api/v1`. Az `API_PORT` alapértéke 443; NAT vagy proxy mögött a játékosok által használt port a `PUBLIC_API_PORT`.
    - A játék WebSocket-kapcsolata alapértelmezés szerint ugyanazon a porton van (a `WS_PORT` áthelyezi; a `GET /info` megmondja a kliensnek, hol található).
    - HTTP/1.1 TLS felett. `TLS_MODE=proxy` esetén a szerver előtt álló fordított proxy (reverse proxy) végzi a TLS lezárását. A `TLS_MODE=off` (titkosítatlan HTTP) csak helyi fejlesztésre szolgál.
    - Az e-mailekben lévő linkekről megnyíló HTML-oldalak (`/verify-email`, `/reset-password`, `/confirm-email-change`) a szerver gyökerében találhatók, az `/api/v1` útvonalon kívül. Az állapot-végpontok a gyökérben és az `/api/v1` alatt is válaszolnak. A Google-bejelentkezésnek nincs oldala a szerveren: a Google a böngészőt magához a játékhoz irányítja vissza, a `127.0.0.1` címre, vagy annak a webhelynek egy oldalára, amely a bejelentkezést elindította (`GOOGLE_WEB_REDIRECT_URI`).
    - A szerver figyelmen kívül hagyja a záró perjelet (az `/api/v1/info/` ugyanaz, mint az `/api/v1/info`), az útvonal-paramétereket pedig URL-dekódolja (ha egy paraméter nem érvényes URL-kódolású, a válasz 400 `invalid_request`).

    ## Kérések

    - **JSON-törzsek.** A `POST`, `PUT` és `DELETE` kérések egy JSON-objektumot tartalmaznak, `Content-Type: application/json` fejléccel. A szerver elutasítja az UTF-8-tól eltérő `charset` értéket, és minden más tartalomtípust is (415 `unsupported_media_type`). Az üres törzs `{}` objektumnak számít, és a paraméter nélküli végpontok éppen ezt várják (`POST /auth/logout`, `POST /auth/logout-all`, `DELETE /auth/sessions/{id}`).
    - **Méret és idő.** A törzs legfeljebb `HTTP_BODY_LIMIT` bájt lehet (alapértelmezés szerint 16 384; a `POST /gif` saját korlátja 135 168 bájt): 413 `payload_too_large`. A törzsnek 10 másodpercen belül meg kell érkeznie: 408 `request_timeout`. Bármelyik hiba után a szerver lezárja a kapcsolatot.
    - **Szigorú sémák.** A szerver elutasítja azt a mezőt, amelyet a végpont nem ismer, minden olyan mező kötelező, amelyet a séma nem jelöl nem kötelezőnek, a típusokat és a hosszakat pedig a szerver ellenőrzi. Bármelyik hiba esetén a válasz 400 `invalid_request`, a `field` mezőben a hibás mező nevével (beágyazott mezőnél pontokkal tagolva, például `pow.nonce`). A karakterláncok nem tartalmazhatnak vezérlőkaraktereket. A hosszak UTF-16 kódegységben értendők, így az alapvető többnyelvű síkon (BMP) kívüli karakter (mint egy emoji) kétszer számít. A nem JSON formátumú törzsre a válasz 400 `invalid_json`. A `POST /gif` és a `POST /reports` maga ellenőrzi a törzsét (lásd az egyes végpontokat).
    - **Lekérdezési karakterláncok.** Egy paraméternek az első előfordulása számít, az ismeretlen paramétereket a szerver figyelmen kívül hagyja, a `+` pedig szóközzé dekódolódik: a játékidő-kategóriát fogadó végpontok a `3%2B2` és a `3+2` alakot is elfogadják.
    - **Metódusok.** A `HEAD` a törzs nélküli `GET`. Létező útvonalon az `OPTIONS` válasza 204, `Allow` fejléccel (és egy CORS előzetes kérésre adott válasz fejléceivel, lásd lent). Minden más metódusra, amely az útvonalhoz nem tartozik, a válasz 405 `method_not_allowed`, `Allow` fejléccel.
    - **A kérés célja (request target).** Legfeljebb 4096 karakter lehet (414 `uri_too_long`). Ha egy kérés egyáltalán nem értelmezhető, vagy a fejrésze túl nagy vagy túl lassan érkezik, üres 400-as, 431-es vagy 408-as választ kap, és a szerver lezárja a kapcsolatot.
    - **CORS: engedélyezési lista, alapértelmezés szerint kikapcsolva.** Az API a játékot szolgálja ki, amely nem küld `Origin` fejlécet. Ha a `CORS_ORIGINS` üres (ez az alapértelmezés), a szerver soha nem küld `Access-Control-*` fejlécet, így egyetlen weboldal sem olvashat ki választ. A `CORS_ORIGINS` azokat a webes eredeteket (origin) sorolja fel, amelyek oldalai böngészőből hívhatják az API-t, mindegyiket pontosan úgy, ahogy a böngészők az `Origin` fejlécben küldik: `https://host` vagy `https://host:port`, kisbetűvel, útvonal, záró perjel és alapértelmezett port nélkül; `http://` csak a `localhost`, a `127.0.0.1` és a `[::1]` esetén (helyi fejlesztés); helyettesítő karakter nélkül. A hivatalos szerver a webhelye számára a `CORS_ORIGINS=https://scacelith.com,https://www.scacelith.com` értéket állítja be. Ekkor:
      - Az `/api/v1` alatti útvonalakra adott minden válasz (az állapot-végpontokéi kivételével), a sikeresek és a hibák egyaránt (a címenkénti réteg 429-es válaszát is beleértve), tartalmazza a `Vary: Origin` fejlécet. Ha a kérés `Origin` fejléce bájtról bájtra megegyezik a felsorolt eredetek egyikével, a válasz az `Access-Control-Allow-Origin: <that origin>` fejlécet is tartalmazza, és – az előzetes kérésre adott válasz kivételével – az `Access-Control-Expose-Headers: Retry-After, Content-Disposition` fejlécet is, hogy az oldal kiolvashassa a hibakódokat, a `Retry-After` értékét, valamint a PGN- és GIF-letöltések fájlnevét.
      - Egy felsorolt eredetről, létező útvonalra érkező előzetes kérésre (`OPTIONS` `Access-Control-Request-Method` fejléccel) a válasz 204, törzs nélkül, a következő fejlécekkel: `Allow`, `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods` (az `Allow` metódusai), `Access-Control-Allow-Headers: Authorization, Content-Type`, `Access-Control-Max-Age: 600` és `Vary: Origin`. Nincs szüksége tokenre, és nem számít bele sem a fiókkeretbe, sem egyetlen végpontkorlátba; mint minden kérés, a címenkénti rétegből elhasznál egy tokent. A böngésző 10 percig újrahasznosítja az előzetes kérés eredményét, de csak pontosan ugyanarra az URL-re, a lekérdezési karakterláncot is beleértve. Az `Authorization` fejléc és JSON-törzs nélküli kérésnek, például a `GET /info` vagy a `GET /leaderboard` kérésnek, nincs szüksége előzetes kérésre.
      - Az előzetes kérésre adott bármely más válasz sikertelen előzetes kérésnek számít, még a CORS-fejlécekkel együtt is: a nem létező útvonalra adott 404, vagy a címenkénti réteg 429-es válasza. Ilyenkor a böngésző nem küldi el a kérést, és az oldal hálózati hibát lát, a válasz és annak `Retry-After` fejléce nélkül.
      - A szerver soha nem küldi az `Access-Control-Allow-Credentials` fejlécet, sem az `Access-Control-Allow-Origin: *` értéket: az oldal az `Authorization: Bearer` fejlécben küldi a munkamenet-tokenjét, ahogy a játék is, és az API nem állít be sütit. A HTML-oldalak, az állapot-végpontok és az értelmezhetetlen kérésekre adott üres válaszok nem tartalmaznak CORS-fejlécet. A WebSocket nem használ CORS-t: a `WS_ALLOWED_ORIGINS` sorolja fel azokat az oldalakat, amelyek megnyithatják.
      - Egy fel nem sorolt eredet oldala semmit sem olvashat ki, és a webhelyek közötti írási kéréseit a böngésző soha nem küldi el: a JSON-törzsekhez és az `Authorization` fejléchez előzetes kérés kell, amelynek válaszában nincs erre az eredetre vonatkozó `Access-Control-Allow-Origin`. A `Cross-Origin-Resource-Policy: same-origin` minden válaszon rajta marad: csak a `no-cors` kérésekre vonatkozik (egy másik webhely beágyaz egy választ), soha nem egy felsorolt oldal `cors` kéréseire.

    ## Válaszok és hibák

    - A válaszok UTF-8 kódolású JSON formátumúak, kivéve a PGN-letöltést (`application/x-chess-pgn`), az animált GIF-eket (`image/gif`) és a HTML-oldalakat. Az időpontok 1970-01-01 UTC óta eltelt ezredmásodpercben értendők. Az azonosítók egész számok. A játszmaazonosítók legfeljebb 16 jegyűek, de 2^53 alatt maradnak, így egy JSON-szám (double) pontosan ábrázolja őket.
    - Az API minden válasza tartalmazza a következőket: `Cache-Control: no-store`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`, `X-Frame-Options: DENY`, `Cross-Origin-Resource-Policy: same-origin` és `Content-Security-Policy: default-src 'none'; frame-ancestors 'none'` (a HTML-oldalaknak saját szabályzatuk van). `TLS_MODE=native` esetén a `Strict-Transport-Security: max-age=31536000` fejlécet is tartalmazza. Azok a válaszok, amelyeket a figyelő (listener) maga ír, még az API előtt (az állapot-végpontokra érkező `GET` és `HEAD` kérésekre adott válaszok, a címenkénti réteg 429-es válasza és egy 500 `internal_error`, ha az API teljesen meghibásodik), ugyanezeket a fejléceket tartalmazzák a `Cross-Origin-Resource-Policy` kivételével, és a `Content-Type` értékük `application/json`, karakterkészlet nélkül.
    - A választ attól a pillanattól számított 60 másodpercen belül ki kell olvasni, amikor a szerver elkészítette; ez csak a nagy válaszoknál számít (egy GIF, az adatexport, egy hosszú PGN). Az az idő, amíg a szerver előkészíti a választ, nem számít bele sem ebbe, sem abba a 30 másodpercbe, amely után a szerver lezárja a kapcsolatot, ha addig egyetlen bájt sem érkezett és ment ki rajta.

    A hibák formája mindig ugyanaz (az `Error` séma):

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

    A `message` naplózásra és tartalék szövegként szolgál; a kliens az `error` alapján választja ki, mit jelenítsen meg. A `retryAfter` (másodpercben) csak az idővel lejáró elutasításoknál szerepel, és ilyenkor a válasz ugyanazzal az értékkel `Retry-After` fejlécet is tartalmaz; az egyetlen kivétel a korábbi játszmák, a játszmák, a játékosok és a ranglista lekérdezéseinek 503 `busy` válasza, amelyben csak a mező szerepel. Egyes hibák további mezőket is tartalmaznak: `field` (érvénytelen bemenet), `reason` (`weak_password`, `pow_required`), `pow` (`pow_required`), `until` (`banned`), `line` és `column` (`invalid_pgn`).

    Bármely végpont által adható hibák:

    | Állapotkód | `error` | Mikor |
    |---|---|---|
    | 400 | `invalid_request` | A törzs megsérti a végpont sémáját (a `field` megmondja, hol), a kérés célja vagy a `Content-Length` hibás formátumú, egy útvonal-paraméter nem érvényes URL-kódolású, vagy a törzs csonkán érkezett. |
    | 400 | `invalid_json` | A törzs nem JSON. |
    | 401 | `unauthorized` | Hiányzik az `Authorization` fejléc egy munkamenetet igénylő végponton. |
    | 401 | `invalid_token` | A token hibás formátumú, lejárt, visszavonták, vagy törölt fiókhoz tartozik; azokon a végpontokon is, ahol a munkamenet nem kötelező. |
    | 404 | `not_found` | Nincs ilyen végpont. Egyes végpontok is használják: nincs ilyen játszma, játékos vagy munkamenet. |
    | 405 | `method_not_allowed` | Az útvonal más metódusokhoz létezik (lásd az `Allow` fejlécet). |
    | 408 | `request_timeout` | A törzs nem érkezett meg 10 másodpercen belül. |
    | 413 | `payload_too_large` | A törzs meghaladja a `HTTP_BODY_LIMIT` értéket (a `POST /gif` esetén a 135 168 bájtot). |
    | 414 | `uri_too_long` | A kérés célja meghaladja a 4096 karaktert. |
    | 415 | `unsupported_media_type` | A törzs nem `application/json`, vagy a karakterkészlete nem UTF-8. |
    | 429 | `rate_limited` | Egy kéréskorlát (lásd lent): `retryAfter`, valamint `Retry-After` fejléc. |
    | 500 | `internal_error` | Váratlan hiba. A szerver naplózza. |
    | 503 | `timeout` | A szerver nem válaszolt 30 másodpercen belül (az exportnál 60, a GIF-eknél alapértelmezett beállításokkal 45 másodpercen belül). |
    | 503 | `server_busy` | Egy munkamenet-lekérdezés vagy fiókmódosítás zárolva találta az adatbázist (`retryAfter` 1), vagy a jelszóhashelési sor megtelt (lásd lent). Továbbá 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva: `POST`, `PUT`, `PATCH` vagy `DELETE` esetén túl sok írás vár az adatbázisban; bármely kérésnél túl sok korábbi kérés fut még az időkorlátján túl, vagy a munkamenet-tokent nem sikerült 5 másodpercen belül ellenőrizni. A szerver nem hajtotta végre a kérést, és a kéréskorlátai nem vontak le tokent. |

    Az olvasó végpontok (korábbi játszmák, játszmák, játékosok, ranglista) és az export 503 `busy` választ adnak `retryAfter: 1` értékkel, ha az adatbázis zárolva maradt.

    A jelszót ellenőrző vagy hashelő végpontok az alábbiak egyikével is válaszolhatnak, mindkettő 5 és 15 másodperc közötti véletlenszerű `retryAfter` értékkel:

    - 503 `server_busy`: a szerver jelszóhashelési sora (`PASSWORD_HASH_QUEUE_MAX`) megtelt, vagy lejárt a várakozási idő.
    - 429 `rate_limited`: a sor már legalább félig megtelt, és ennek a kliensnek (egy IPv4-címnek vagy egy IPv6 /48-nak) már `PASSWORD_HASH_WAITERS_PER_SOURCE` hashelése várakozik. Ez a 429-es válasz visszaadja azokat a kéréskorlát-tokeneket, amelyeket a kérés elhasznált.

    Semmi sem változott, és nem számított sikertelen kísérlet (kivéve a `POST /auth/sso/google/link` végpontot, amelynél a jegy próbálkozását és a fiók sikertelen kísérletét a szerver már a hashelés előtt levonta), a visszaállító link pedig érvényes marad.

    A HTML-oldalak a hibáikat ugyanezekkel az állapotkódokkal, HTML-oldalként adják vissza.

    ## Hitelesítés

    A munkamenetet igénylő végpontok bearer tokent várnak az `Authorization` fejlécben (a `bearerAuth` séma):

    ```
    Authorization: Bearer sct_L_8GDd7uzfQ3QQWtqrsWXDTsFWzRwIvJcwIGHhjWPS8
    ```

    - **Token beszerzése.** Tokent (`sct_`, majd 43 base64url karakter) a `POST /auth/login` ad, bekapcsolt kétlépcsős azonosítás esetén azt követően a `POST /auth/login/mfa`, továbbá a `POST /auth/sso/google/finish` és a `POST /auth/sso/google/link` (Google-bejelentkezés), valamint a `POST /auth/sso/complete`. Mindegyikük válasza `{ token, expiresAt, user }`. A szerver csak a token SHA-256 hashét tárolja.
    - **Kötelező munkamenet.** Hiányzó fejlécre a válasz 401 `unauthorized`, `WWW-Authenticate: Bearer realm="scacelith"` fejléccel. Érvénytelen tokenre a válasz 401 `invalid_token`, `WWW-Authenticate: Bearer realm="scacelith", error="invalid_token"` fejléccel. Az `invalid_token` választ kapó tokent a kliens felejtse el, és jelentkezzen be újra.
    - **Nem kötelező munkamenet.** A `GET /games/{id}`, `GET /games/{id}/pgn`, `GET /players/{username}` és `GET /players/{username}/games` végponton a munkamenet nem kötelező. Fejléc nélkül a nyilvános nézetet adják vissza; ha van fejléc, a benne lévő tokennek érvényesnek kell lennie.
    - **Foglalt tároló.** Ha a token ellenőrzése közben az adatbázis zárolva marad, a munkamenetet igénylő vagy elfogadó végpont válasza 503 `server_busy`, `retryAfter: 1` értékkel (és `Retry-After` fejléccel); ha az ellenőrzés 5 másodpercnél tovább tart, a válasz 503 `server_busy`, 2–5 közötti `retryAfter` értékkel. A token egyik esetben sem lett elbírálva: tartsa meg, és küldje el újra a kérést.
    - **Élettartam.** A munkamenet a kettő közül a korábbinál ér véget: a bejelentkezés után `SESSION_MAX_DAYS` (90) nappal (ez az `expiresAt`), vagy `SESSION_IDLE_DAYS` (30) nap használat nélkül. Minden használat kitolja az inaktivitási korlátot; az új értéket a szerver legfeljebb 5 percenként írja ki, a háttérben. Amíg az adatbázisában sok írás várakozik, ezt az írást egy későbbi kérésre halasztja, kivéve az inaktivitási korlát előtti utolsó órában, így a használatban lévő munkamenet nem jár le. Egy fióknak legfeljebb `MAX_SESSIONS_PER_USER` (10) munkamenete lehet: egy új bejelentkezés visszavonja a legrégebbieket e számon felül.
    - **Visszavonás.** A kijelentkezés (`POST /auth/logout`, `POST /auth/logout-all`, `DELETE /auth/sessions/{id}`) visszavonja a munkameneteket; ugyanígy a jelszómódosítás (a többi munkamenetet), a jelszó-visszaállítás és a fiók törlése (az összes munkamenetet), valamint egy adminisztrátor is. Az API-n keresztüli visszavonás azonnal érvénybe lép, és a visszavont munkamenettel megnyitott WebSocket-kapcsolat lezárul. A jelszómódosítás vagy -visszaállítás és a fiók törlése ugyanabban a tranzakcióban von vissza, mint maga a módosítás: ha valamelyikük meghiúsul, egyetlen munkamenet sem lett visszavonva, és semmi más sem változott. A szerver 30 másodpercig gyorsítótárazza a munkamenet-lekérdezéseket, így az adminisztrátori parancs (amely külön folyamat) általi visszavonás 30 másodpercen belül lép érvénybe.
    - **Hatókör.** Egy token egyetlen szerverhez tartozik, és annak WebSocket-kapcsolatát is megnyitja. Soha ne küldje el másik szervernek.

    ## Kéréskorlátok és egyéb korlátozások

    A korlátok két hatókör egyikére vonatkoznak. **Kliens:** egy IPv4-cím vagy egy IPv6 /64 (proxy módban a cím egy `TRUSTED_PROXIES` címről küldött `X-Forwarded-For` fejlécből származik); egyes korlátok minden IPv6 /48-at egészként is számolnak, a benne lévő egyes /64-es hálózatokon felül. **Játékos:** a bejelentkezett fiók, bármilyen címről; az a korlát, amelyet egy olyan végponton, ahol a munkamenet nem kötelező, játékosonként számolnak, token nélküli kérésnél kliensenként számít.

    Minden korlát egy tokenvödör, amelynek kapacitása `limit` kérés, és folyamatosan, `limit / window` ütemben töltődik újra; a `retryAfter` a következő tokenig hátralévő idő. A *közös* jelölésű korlátokat a szerver egy ugyanilyen hosszú csúszóablakban is számolja, hogy egyetlen ablakba se essen jóval több kérés, mint `limit`. Minden korlátot a teljes szerverre számol. Ha egy végpont valamelyik korlátja elutasít egy kérést, a végpont többi korlátja által az adott kérésért levont tokenek visszajárnak. Az elutasítás válasza 429 `rate_limited`, `retryAfter` értékkel és `Retry-After` fejléccel.

    - **Címenkénti réteg.** Minden kérés (bármely útvonal és metódus, beleértve az állapot-végpontokat és a WebSocket-upgrade kéréseket is) először egy tokent von le a kliense keretéből: percenként `HTTP_RATE_PER_IP` (600), fél percnyi löketkapacitással (burst), egy IPv6 /48 esetén pedig `HTTP_RATE_PER_PREFIX` (4 x `HTTP_RATE_PER_IP`). Egy kliensnek legfeljebb `IP_MAX_INFLIGHT` (32 x `WORKERS`) folyamatban lévő kérése lehet (ezen felül `retryAfter` 1). Azt a klienst, amely az elutasítások után is folytatja, a szerver letiltja: egy percen belül `ABUSE_BLOCK_REFUSALS_PER_MIN` (600) elutasítás 1 percre letiltja, majd 6 órán belül minden újabb letiltásnál 4, 16 és 60 percre. Az `auth`, az `auth_*` és a `reauth` korlát egy-egy elutasítása 5-nek számít; a játékosonként számolt korlátok elutasításai sosem számítanak. Az `ABUSE_EXEMPT` listában szereplő címekre ez a réteg nem vonatkozik, a fiókkeret és a végpontkorlátok viszont igen.
    - **Fiókkeret.** Minden érvényes munkamenet-tokent tartalmazó kérés a fiókjába is beszámít: percenként `USER_RATE_PER_MIN` (120) az összes végpontra együtt, bármilyen címről, fél percnyi löketkapacitással.
    - **Végpontkorlátok:**

    | Korlát | Alapérték | Számolás alapja | Végpontok |
    |---|---|---|---|
    | `auth` | `AUTH_RATE_PER_IP` (20) / 10 perc, közös | kliens, és IPv6 /48-anként `AUTH_RATE_PER_PREFIX` (5 x `AUTH_RATE_PER_IP`) | `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) / óra, közös | kliens, IPv6 /48-anként ennek háromszorosa | `POST /auth/register` |
    | `auth_mail` | `AUTH_MAIL_PER_HOUR` (10) / óra, közös | kliens, IPv6 /48-anként ennek háromszorosa | `POST /auth/verify-email/resend` |
    | `auth_forgot` | `AUTH_FORGOT_PER_HOUR` (3) / óra, közös | kliens, IPv6 /48-anként ennek háromszorosa | `POST /auth/password/forgot` |
    | `auth_forgot_day` | `AUTH_FORGOT_PER_DAY` (10) / 24 óra, közös | kliens, IPv6 /48-anként ennek háromszorosa | `POST /auth/password/forgot` |
    | `auth_reset` | `AUTH_RESET_PER_HOUR` (10) / óra, közös | kliens, IPv6 /48-anként ennek háromszorosa | `POST /auth/password/reset`, `POST /reset-password` |
    | `reauth` | az `auth` értékei, saját vödörrel, közös | kliens és 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 perc, közös | játékos | ugyanazok a végpontok, mint a `reauth` esetén |
    | `account` | 60 / perc | játékos | `GET /account/me`, `PUT /account/preferences` |
    | `account_games` | 60 / perc | játékos | `GET /account/games` |
    | `account_export` | 5 / óra, közös | játékos | `POST /account/export` (a `reauth` előtt ellenőrizve; minden kísérlet számít) |
    | `sessions` | 60 / perc | játékos | `POST /auth/logout`, `/auth/logout-all`, `GET /auth/sessions`, `DELETE /auth/sessions/{id}` |
    | `public_read` | 60 / perc | játékos (token nélkül a kliens) | `GET /players/{username}`, `/players/{username}/games`, `/games/{id}`, `/games/{id}/pgn` (egyetlen vödör mind a négyre) |
    | `gif` | 30 / perc | játékos | `GET /games/{id}/gif`, `POST /gif` (egyetlen vödör mindkettőre) |
    | `gif_user_min`, `gif_user_hour` | `GIF_USER_RENDERS_PER_MIN` (4) / perc és `GIF_USER_RENDERS_PER_HOUR` (30) / óra, közös | játékos | ugyanez a kettő, csak ha a GIF-et el kell készíteni (nem a gyorsítótárból jön) |
    | `gif_ip_min`, `gif_ip_hour` | `GIF_IP_RENDERS_PER_MIN` (12) / perc és `GIF_IP_RENDERS_PER_HOUR` (120) / óra, közös | kliens (az összes fiókja együtt), IPv6 /48-anként ennek háromszorosa | ugyanazok, a fentiek szerint |
    | `reports` | 30 / óra | játékos | `POST /reports` |
    | `sso_start` | 30 / 10 perc, közös | kliens, és IPv6 /48-anként 90 | `POST /auth/sso/google/start`, `POST /auth/sso/google/web/start` (együtt) |
    | `sso_finish` | 30 / perc | kliens | `POST /auth/sso/google/finish` |
    | `page` | 60 / perc | kliens | `GET /verify-email`, `/reset-password`, `/confirm-email-change` |

    A `GET /info` és a `GET /leaderboard` végpontnak nincs saját korlátja: csak a címenkénti réteg vonatkozik rájuk. A több korláttal rendelkező végpont a leírásában megadott sorrendben ellenőrzi őket.

    A szerver a kérést ebben a sorrendben dolgozza fel: a címenkénti réteg, majd az állapot-végpontok; a végpont illesztése; annak ellenőrzése, hogy a szerver nincs-e erősen lemaradva (503 `server_busy`): túl sok korábbi kérés fut-e még az időkorlátján túl, illetve `POST`, `PUT`, `PATCH` vagy `DELETE` esetén az adatbázis írási hátraléka; a hitelesítés, 5 másodpercen belül (azon túl 503 `server_busy`); a fiókkeret, ha a kérés munkamenetet tartalmaz; a végpont korlátai; a törzs; maga a végpont (a GIF-végpontok a renderelési korlátaikat csak akkor vonják le, ha GIF-et kell készíteniük). Így a tokenje miatt elutasított kérés a végpont tokenjeiből semmit sem használ el, az érvénytelen törzsű kérés viszont igen.

    Egyéb korlátozások, amelyekre maguk a végpontok válaszolnak:

    - **Sikertelen bejelentkezések egy bejelentkezési névvel** (felhasználónév vagy e-mail-cím): `AUTH_FAILURES_PER_ACCOUNT` (5) sikertelen kísérlettől kezdve minden kísérletnek kétszer annyit kell várnia, mint az előzőnek (2 mp, 4 mp és így tovább, legfeljebb 15 percig): 429 `too_many_attempts`, `retryAfter` értékkel. A számláló egy hiba nélküli óra után törlődik.
    - **Sikertelen második faktorok bejelentkezéskor:** ugyanez a szabály a fiók 5. hibás kódjától kezdve. Egy bejelentkezési lépés legfeljebb 5 hibás kódot fogad el.
    - **Egy fiók második faktoros kódjai:** 15 percenként legfeljebb `AUTH_MFA_PER_ACCOUNT` (10) kód (hitelesítő vagy helyreállító kód, helyes vagy hibás), bármilyen címről, bejelentkezéskor és újrahitelesítéskor; ezen felül 429 `too_many_attempts` még a kód ellenőrzése előtt, így a helyreállító kód nem használódik el.
    - **Egy fiók sikertelen újrahitelesítései** (hibás jelszó vagy kód): ugyanez a szabály (429 `too_many_attempts`), amelyen minden újrahitelesítő végpont osztozik.
    - **E-mailek:** címenként 5 percenként egy megerősítő vagy visszaállító levél (a válasz ugyanaz marad); címenként óránként egy „valaki megpróbálta használni az Ön címét” értesítés.
    - **Jelentések:** játékosonként 24 óra alatt `REPORTS_PER_DAY` (5): 429 `report_limit`.

    ## Munkabizonyíték

    A `POST /auth/register` mindig munkabizonyítékot igényel, ha a `POW_REGISTER_BITS` 0-nál nagyobb (alapértelmezés szerint 18; a `GET /info` `pow.register` néven adja meg). A `POST /auth/login` és a `POST /auth/sso/google/link` (mindkettő ugyanolyan fajta kihívással) csak azután igényel 5 percig munkabizonyítékot, hogy a szerver sikertelen bejelentkezések hullámát észleli (`POW_LOGIN_TRIGGER_PER_MIN`, majd `POW_LOGIN_BITS`); ezt a kliens a válaszból tudja meg.

    1. A bizonyíték nélküli (vagy elutasított bizonyítékkal küldött) kérésre a válasz 428 `pow_required`, `reason` mezővel és egy `pow` kihívással: `{ challenge, bits, expiresAt }`.
    2. Keressen egy nonce-t: legfeljebb 20 jegyű decimális karakterláncot, amelyre a `SHA-256(challenge + ":" + nonce)` `bits` darab nulla bittel kezdődik (az első bájt legmagasabb helyiértékű bitjétől kezdve). 18 bithez átlagosan körülbelül 260 000 hash szükséges.
    3. Küldje el újra ugyanazt a kérést, a törzsben a `"pow": { "challenge": "...", "nonce": "123456" }` mezővel.

    Egy kihívás 2 percig és csak egyszer érvényes, egyetlen végpontra és egyetlen klienshálózatra (egy IPv4-címre vagy IPv6 /64-re). Alá van írva, így a szerver semmit sem tárol róla, amíg vissza nem érkezik. A `reason` megadja, miért utasította el a szerver a bizonyítékot: `required`, `malformed`, `signature`, `endpoint`, `network`, `expired`, `bits`, `work` vagy `replayed`.

    ## Újrahitelesítés

    A fiókmódosítások újra kérik a jelszót, bekapcsolt kétlépcsős azonosítás esetén pedig egy második faktort is:

    - csak a jelszót: `POST /account/password` és `POST /account/mfa/totp/setup`;
    - a jelszót és egy hitelesítő kódot (helyreállító kódot nem fogad el): `POST /account/mfa/recovery-codes`;
    - a jelszót és egy hitelesítő vagy helyreállító kódot: `POST /account/mfa/totp/disable`, `POST /account/email`, `POST /account/export` és `POST /account/delete`.

    A törzsekben a `code` egy 6 jegyű hitelesítő kódot, a `recoveryCode` pedig egy helyreállító kódot tartalmaz (`xxxx-xxxx-xx`; a kis- és nagybetűk, a szóközök és a kötőjelek nem számítanak). Ahol a szerver elfogad helyreállító kódot, ott az a `code` mezőben is elküldhető. Minden kód egyszer használható: a már felhasznált hitelesítő kódot a szerver a következő 30 másodperces lépésig elutasítja, a helyreállító kód pedig felhasználás után megszűnik.

    | Állapotkód | `error` | Mikor |
    |---|---|---|
    | 403 | `invalid_password` | Hibás jelszó, vagy a jelszót a kérés futása közben visszaállították vagy módosították (semmi sem változott). |
    | 403 | `mfa_code_required` | A kétlépcsős azonosítás be van kapcsolva, és sem `code`, sem `recoveryCode` nem érkezett. |
    | 403 | `invalid_code` | Hibás vagy már felhasznált kód. |
    | 400 | `password_not_set` | Egy csak Google-bejelentkezést használó fióknak még nincs jelszava (az „Elfelejtette a jelszavát?” funkcióval állítható be). |
    | 429 | `too_many_attempts` | Túl sok sikertelen kísérlet vagy túl sok kipróbált kód ezen a fiókon. |
    | 503 / 429 | `server_busy` / `rate_limited` | A jelszóhashelési sor foglalt. |

    A hibás jelszavak és kódok beszámítanak a fiók hibaszámlálójába, és biztonsági eseményként rögzülnek.

    ## Metrikaport

    Az API-porton kívül a szerver a metrikaporton is válaszol, titkosítatlan HTTP-n (`METRICS_PORT`, 9464, a `METRICS_BIND` címhez kötve, amely alapértelmezés szerint 127.0.0.1; ne tegye nyilvánosan elérhetővé). Ez nem része ennek az API-nak: a `GET /healthz` válasza `ok`; a `GET /readyz` válasza `ready`, miután az indulás befejeződött (minden shard visszajátszotta a naplóját, és a figyelők kötése megtörtént), egészen a leállítás kezdetéig, egyébként 503 `not ready`; a `GET /metrics` a Prometheus-metrikákat szolgálja ki, és ha a `METRICS_TOKEN` be van állítva, `Authorization: Bearer <token>` fejlécet igényel, pontosan ezzel a tokennel.
  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: A Scacelith-szerver forráskódja és referenciadokumentációja (API.md, PROTOCOL.md, CONFIG.md).
  url: https://github.com/DarkCenobyte/scacelith-chess-server
servers:
  - url: https://caissa.scacelith.com/api/v1
    description: A hivatalos szerver.
  - url: https://{host}:{port}/api/v1
    description: Bármely Scacelith-szerver, például egy közösségi szerver.
    variables:
      host:
        default: caissa.scacelith.com
        description: A szerver nyilvános állomásneve (`SERVER_PUBLIC_HOST`).
      port:
        default: "443"
        description: A nyilvános API-port (`PUBLIC_API_PORT`, ennek hiányában `API_PORT`).
tags:
  - name: server-info
    x-displayName: Szerverinformációk
    description: Amit a kliensnek tudnia kell, mielőtt bejelentkezik vagy csatlakozik.
  - name: health
    x-displayName: Állapotellenőrzés
    description: |-
      A szerver működőképessége (liveness) és készenléte (readiness), monitorozáshoz. Az API-porton válaszolnak, a hitelesítés és minden végpontkorlát előtt, de mint minden kérés, egy tokent levonnak a címenkénti rétegből; a monitorozó gép felvehető az `ABUSE_EXEMPT` listába. Mindegyik végpont mindkét útvonalon működik: a szerver gyökerében és az `/api/v1` alatt.

      A metrikaportnak saját állapot-végpontjai vannak, ezeket a bevezető ismerteti.
  - name: auth
    x-displayName: Regisztráció és bejelentkezés
    description: |-
      Fiók létrehozása, bejelentkezés jelszóval (és második faktorral), megerősítő és jelszó-visszaállító e-mailek.

      A `POST /auth/login`, `POST /auth/login/mfa`, `POST /auth/sso/google/finish`, `POST /auth/sso/google/link` és `POST /auth/sso/complete` válasza egy munkamenet: `{ token, expiresAt, user }` (vagy a felsorolás elején állóknál egy második lépés).
  - name: google-sign-in
    x-displayName: Google-bejelentkezés
    description: |-
      Akkor érhető el, ha a `GET /info` szerint `sso.google: true`; egyébként minden alábbi végpont válasza 404 `sso_disabled`. Egy webhely ugyanígy jelentkezik be a saját kliensével, ha az `sso.googleWeb` értéke true (lásd lent: **Webhelyen**). A játék a rendszer böngészőjén keresztül jelentkezik be, az RFC 8252 szerinti telepített alkalmazásokra vonatkozó folyamattal (egy „Desktop app”, azaz asztali alkalmazás típusú klienssel): a Google a böngészőt a játék egy figyelőjéhez irányítja vissza a `127.0.0.1` címen, soha nem ehhez a szerverhez, és a játék soha nem lát Google-hitelesítő adatot.

      1. A kliens a `127.0.0.1:0` címen figyel (a portot a rendszer választja), és létrehoz egy PKCE-párt: egy 43–128 karakteres, `[A-Za-z0-9._~-]` karakterekből álló `codeVerifier` értéket és a `codeChallenge = BASE64URL(SHA-256(codeVerifier))` értéket, amely 43 karakteres, kitöltés (padding) nélkül.
      2. A kihívással és a porttal küldött `POST /auth/sso/google/start` visszaadja a Google URL-t és a kísérlet `state` értékét. A kliens ellenőrzi az URL-t (lásd lent), és megnyitja a böngészőben.
      3. A Google a böngészőt ide irányítja: `http://127.0.0.1:<port>/oauth2/google/<tag>?code=...&state=...`. A kliens csak az indító válaszban kapott `state` értéket fogadja el, és `error=` átirányítás után semmit sem küld.
      4. `POST /auth/sso/google/finish` a kísérlet azonosítójával, a `codeVerifier`, a `state` és a `code` értékkel (és az `iss` értékkel, ha a Google elküldte).
      5. A válasz egy munkamenet, egy kétlépcsős azonosítási lépés (folytatás: `POST /auth/login/mfa`), új játékos esetén `needsUsername` (folytatás: `POST /auth/sso/complete`), vagy `needsPassword`, ha a címet egy jelszóval rendelkező fiók használja (folytatás: `POST /auth/sso/google/link`).

      **Az eredetcímke.** Az átirányítási URI annak a szervernek a címkéjét tartalmazza, amelyet a játékos hozzáadott, így a játék elutasítja azt az URL-t, amelyet egy másik szerver a saját játékosai számára kapott. Az eredet a kisbetűs állomásnév (IPv6-literál esetén szögletes zárójelben), egy `:` és a decimális API-port, amely mindig ki van írva, a 443 is: a szerver a `SERVER_PUBLIC_HOST` értéket és a nyilvános API-portját veszi, a játék pedig azt a címet, amelyhez csatlakozik. A címke a SHA-256(UTF-8 `"scacelith-sso-origin-v1\n"` + eredet) base64url kódolásának (kitöltés nélkül) első 22 karaktere. Az átirányítási URI `"http://127.0.0.1:" + port + "/oauth2/google/" + tag`: mindig az IPv4-literál és a játék figyelőjének portja (1024–65535). A szerver soha nem vesz át URI-t, állomásnevet vagy útvonalat a klienstől.

      | Eredet | Címke |
      |---|---|
      | `play.scacelith.example:443` | `IhcScoV7eDOzTEcSnqPUPt` |
      | `localhost:8443` | `TFGx7zQ_8QlGZW5zpqznCr` |
      | `[::1]:8443` | `XToJm0DG5PjciEVmZa9Cho` |
      | `127.0.0.1:50443` | `3r653wM5ZjYsHcAJljmCwY` |

      A Google-bejelentkezés tehát csak azoknál a játékosoknál működik, akik a szervert pontosan a `SERVER_PUBLIC_HOST` néven és a nyilvános API-portjával adták hozzá.

      **Amit a játék a böngésző megnyitása előtt ellenőriz.** Az `authUrl` pontosan a `https://accounts.google.com/o/oauth2/v2/auth?` előtaggal kezdődik, 4096 karakternél rövidebb, nyomtatható ASCII-szöveg, a lekérdezési része pedig pontosan egy `response_type=code` paramétert, pontosan egy olyan `redirect_uri` paramétert, amely megegyezik a játék által a portjából és az eredetcímkéjéből kiszámított URI-val, pontosan egy, a válasz `state` értékével megegyező `state` paramétert, továbbá `code_challenge_method=S256` paramétert és egy 43 karakteres `code_challenge` paramétert tartalmaz. Ellenkező esetben a játék leállítja a figyelőjét, és semmit sem nyit meg. A `finish` és a `link` kérést csak annak a szervernek küldi el, amely a `start` kérésre válaszolt.

      **Webhelyen.** Egy webhely (a hivatalos: <https://scacelith.com/>) ugyanannak a Google-projektnek egy „Web application” (webalkalmazás) típusú kliensével jelentkezik be (`GOOGLE_WEB_CLIENT_ID`). A Google a böngészőt a webhelynek az üzemeltető által beállított egyik oldalára irányítja vissza (`GOOGLE_WEB_REDIRECT_URI`, amely szerepel a kliens „Authorized redirect URIs” listájában), és ez az oldal küldi el a kódot a `finish` végpontnak; a szerver itt sem vesz át átirányítási URI-t a klienstől.

      1. Az oldal a fentiek szerint létrehoz egy PKCE-párt, az ellenőrzőt (verifier) a lapon tárolja (`sessionStorage`), és elküldi a `POST /auth/sso/google/web/start` kérést a kihívással.
      2. A játékhoz hasonlóan ellenőrzi az `authUrl` értéket, azzal a különbséggel, hogy a `redirect_uri` értékének a saját eredetén lévő oldalnak kell lennie, majd odanavigál. Google-szkript nem töltődik be.
      3. A Google a böngészőt ide irányítja: `GOOGLE_WEB_REDIRECT_URI?code=...&state=...`. Az oldal csak az indító válaszban kapott `state` értéket fogadja el, és `error=` átirányítás után semmit sem küld.
      4. A `finish`, a `link` és a `complete` ugyanúgy következik, mint a játéknál. A kísérlet megjegyzi, hogy egy webhely indította: a kód cseréje a webhely kliensével és oldalával történik, és az ID-tokennek ezt a klienst kell megneveznie (a játék kliensének kódja vagy ID-tokenje 502 `sso_failed` hibával meghiúsul, és fordítva is).

      Egy Google-fiók a játékból és a webhelyről is ugyanahhoz a Scacelith-fiókhoz jut el: a kapcsolat a Google subject azonosítóját (`sub`) tárolja, amely a klienstől függetlenül azonosítja a Google-fiókot. A webhely eredetének szerepelnie kell a `CORS_ORIGINS` listában (ellenkező esetben a `check-config` figyelmeztet).

      **Melyik fiókba jut a Google-bejelentkezés:** abba a fiókba, amely már ehhez a Google-fiókhoz van kapcsolva; ha ilyen nincs, abba az aktív fiókba, amely a Google által megerősített címet használja (ha van jelszava, a `finish` válasza `needsPassword`, és a kapcsolat csak akkor tárolódik, ha a jelszava, majd bekapcsolt második faktor esetén az is sikeresen átmegy az ellenőrzésen; jelszó nélkül 409 `sso_account_exists`); egyébként egy új fiókba (`needsUsername`). Egy Google-fiók soha nem kapcsolódik egy meglévő fiókhoz pusztán a címe alapján. A fiók címére levél érkezik, amikor a Google-bejelentkezés fiókot hoz létre, és amikor egy meglévő fiókhoz hozzáadják.
  - name: sessions
    x-displayName: Munkamenetek
    description: A fiók bejelentkezett eszközei és a kijelentkezés.
  - name: account
    x-displayName: Fiók
    description: A fiók nézete, beállításai és jelszava.
  - name: two-step-verification
    x-displayName: Kétlépcsős azonosítás
    description: 'Hitelesítő alkalmazások (TOTP, RFC 6238): SHA-1, 6 számjegy, 30 másodperces lépések és mindkét irányban egylépésnyi tűrés, valamint egyszer használható helyreállító kódok.'
  - name: email-change
    x-displayName: E-mail-cím módosítása
    description: A fiók e-mail-címének módosítása, az új címre küldött linkkel megerősítve.
  - name: data-export
    x-displayName: Adatexport
    description: Minden, amit a szerver a fiókról tárol, egyetlen JSON-fájlban.
  - name: account-deletion
    x-displayName: Fiók törlése
    description: A fiók végleges törlése.
  - name: game-history
    x-displayName: Korábbi játszmák
    description: A bejelentkezett játékos saját játszmái, szűrve és lapozva.
  - name: games
    x-displayName: Játszmák és PGN
    description: |-
      Játszmarekordok a lépéseikkel és az óraállásokkal, valamint a PGN-fájljaik.

      **Játszmakódok.** `status`: 1 `WhiteWins`, 2 `BlackWins`, 3 `Draw`, 4 `Aborted` (csak a befejezett játszmák tárolódnak). `result`: `1-0`, `0-1`, `1/2-1/2` vagy `*` (megszakított). `reason`, a nevével (`termination`) és azokkal a szavakkal, amelyek a PGN-fájl lépésszövegét zárják (a fájlban angolul, alább zárójelben a magyar megfelelőjükkel); a 7-es, a 14-es és a 21-es kód döntetlen (az a játékos, akinek lejárt az ideje, aki feladta a játszmát vagy elhagyta, olyan ellenféllel állt szemben, aki nem tudott mattot adni: FIDE 6.9 és 5.1.2; a szabályt a `DESIGN.md` 6.3 pontja adja meg), a 4-es és a 13-as kóddal pedig a szerver soha nem fejez be játszmát (ezek az okok közös listájához tartoznak):

      | Kód | `termination` | Szavak a PGN-ben |
      |---|---|---|
      | 1 | `Checkmate` | Checkmate (matt) |
      | 2 | `Resignation` | Resignation (feladás) |
      | 3 | `Timeout` | Loss on time (időtúllépés) |
      | 4 | `IllegalMoves` | Second illegal move (forfeit) (második szabálytalan lépés, játszmavesztés) |
      | 5 | `Stalemate` | Stalemate (patt) |
      | 6 | `InsufficientMaterial` | Dead position (insufficient material) (holt állás, elégtelen anyag) |
      | 7 | `TimeoutVsInsufficient` | Flag fall, but the opponent cannot checkmate (lejárt az idő, de az ellenfél nem tud mattot adni) |
      | 8 | `FivefoldRepetition` | Fivefold repetition (ötszöri lépésismétlés) |
      | 9 | `SeventyFiveMoves` | 75-move rule (75 lépéses szabály) |
      | 10 | `ThreefoldClaim` | Threefold repetition (claimed) (háromszori lépésismétlés, igénylésre) |
      | 11 | `FiftyMoveClaim` | 50-move rule (claimed) (50 lépéses szabály, igénylésre) |
      | 12 | `Agreement` | Draw by agreement (döntetlen megegyezéssel) |
      | 13 | `IllegalMovesVsInsufficient` | Second illegal move, but the opponent cannot checkmate (második szabálytalan lépés, de az ellenfél nem tud mattot adni) |
      | 14 | `ResignationVsInsufficient` | Resignation, but the opponent cannot checkmate (feladás, de az ellenfél nem tud mattot adni) |
      | 20 | `Abandonment` | Abandoned (disconnected for too long) (elhagyta a játszmát, túl sokáig nem volt kapcsolat) |
      | 21 | `AbandonmentVsInsufficient` | Abandoned, but the opponent cannot checkmate (elhagyta a játszmát, de az ellenfél nem tud mattot adni) |
      | 22 | `Aborted` | Game aborted (megszakított játszma) |
      | 23 | `NoShow` | Aborted: first move not played in time (megszakítva: az első lépés nem történt meg időben) |
      | 24 | `Forfeit` | Forfeit (fair play violation) (játszmavesztés, a fair play megsértése) |
      | 25 | `ServerAborted` | Aborted by the server (a szerver megszakította) |
      | 26 | `BothDisconnected` | Aborted: both players disconnected (megszakítva: mindkét játékos kapcsolata megszakadt) |

      A 14-es kód a protokoll 1-es alverziójában (minor) új: az ennél korábban írt kliens az ismeretlen kódot egyszerű játszmavégként jelenítse meg, a `result` értékével.
  - name: gifs
    x-displayName: Animált GIF-ek
    description: |-
      Egy játszma animált GIF-ként, megőrzésre vagy megosztásra: a tábla felülnézetből, állásonként egy képkocka a kezdőállástól a végállásig, a játékosok neve és értékszáma a tábla fölött, az utolsó lépés alatta, az utolsó képkockán pedig az eredmény és a játszma befejeződésének módja.

      **Költség, gyorsítótár és kvóták.** A GIF saját renderelő szálon készül, soha nem a játszmákat futtató szálakon, a legalacsonyabb CPU-prioritással: `GIF_THREADS` szálon (alapértelmezés szerint `WORKERS`), amelyek az első GIF-fel indulnak, és egy GIF nélküli perc után leállnak. Legfeljebb `GIF_QUEUE_MAX` (4 x `WORKERS`) GIF várakozhat szálra, mindegyik legfeljebb `GIF_QUEUE_TIMEOUT_MS` (10 mp) ideig; egy renderelés `GIF_RENDER_TIMEOUT_MS` (30 mp) ideig tarthat. A szerver az elkészített GIF-eket egy `GIF_CACHE_MB` MB méretű (32 x `WORKERS`) gyorsítótárban tartja, ahonnan elsőként a legrégebben használtak kerülnek ki; a gyorsítótárból származó, illetve egy másik kérés számára éppen készülő GIF nem jár rendereléssel. A gyorsítótár kulcsa mindaz, ami a képet megváltoztatja, a neveket is beleértve.

      Minden kérés beszámít a `gif` korlátba (játékosonként percenként 30, a két végpontra együtt). Az elkészítendő GIF a renderelési korlátokba is beszámít: játékosonként percenként `GIF_USER_RENDERS_PER_MIN` (4) és óránként `GIF_USER_RENDERS_PER_HOUR` (30); kliensenként, az összes fiókját együtt számolva, percenként `GIF_IP_RENDERS_PER_MIN` (12) és óránként `GIF_IP_RENDERS_PER_HOUR` (120), IPv6 /48-anként pedig ennek háromszorosa. A kliens tartsa meg a letöltött fájlt ahelyett, hogy újra lekérné, és 429-es vagy 503-as válasz után várjon `retryAfter` ideig.

      Méretek: `small` (32 px-es mezők: 284 x 350 képpont), `medium` (48 px: 424 x 515), `large` (72 px: 628 x 762); koordináták nélkül 268 x 342, 400 x 503 és 600 x 748. A kezdőállás legalább 1 mp-ig látható (ha a késleltetés hosszabb, addig), a végállás 3 mp-ig, és a GIF ismétlődik; az első képkocka után csak a kép változó része tárolódik. Egy 40 lépéses játszma kb. 135, 205 és 325 KiB (kicsi, közepes, nagy), egy 150 lépéses 0,5; 0,8 és 1,2 MiB.
  - name: players
    x-displayName: Játékosok
    description: Nyilvános profilok és legutóbbi játszmák. Csak nyilvános adatok, soha nem e-mail-cím, munkamenet, szankció vagy integritási szint. A törölt fióknak nincs profilja.
  - name: leaderboard
    x-displayName: Ranglista
    description: Az egyes hivatalos kategóriák legjobb játékosai.
  - name: reports
    x-displayName: Jelentések
    description: Egy legutóbbi játszma ellenfelének jelentése a moderátoroknak.
  - name: pages
    x-displayName: HTML-oldalak
    description: |-
      Böngészőnek szóló oldalak, amelyek az e-mailekben lévő linkekről nyílnak meg. A linkjeik a `https://<SERVER_PUBLIC_HOST>` címre mutatnak (`:<PUBLIC_API_PORT>` kiegészítéssel, ha az nem 443), az `/api/v1` útvonalon kívül.

      - Az oldalak nem futtatnak JavaScriptet, és nem töltenek be külső erőforrást. A szerver a következő fejléccel szolgálja ki őket: `Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'`.
      - A `GET` csak egy gombot vagy űrlapot jelenít meg, hogy a linket megnyitó levélszkenner ne használja el. A módosítás `POST` kérésre történik: egy `application/x-www-form-urlencoded` formában küldött űrlap (JSON-törzs is elfogadott), a link tokenjével egy rejtett mezőben. A kétszer megadott mezőt a szerver elutasítja.
      - A hibák (kéréskorlátok, érvénytelen mezők) szintén HTML-oldalak, „Request refused” (kérés elutasítva), 500-tól felfelé pedig „Server error” (szerverhiba) címmel. Csak az oldal illesztése előtt észlelt hibák (túl hosszú vagy hibás formátumú kéréscél, a címenkénti réteg) kapnak JSON-választ.
x-tagGroups:
  - name: server
    x-displayName: Szerver
    tags:
      - server-info
      - health
  - name: accounts
    x-displayName: Fiókok
    tags:
      - auth
      - google-sign-in
      - sessions
      - account
      - two-step-verification
      - email-change
      - data-export
      - account-deletion
  - name: games
    x-displayName: Játszmák
    tags:
      - game-history
      - games
      - gifs
  - name: community
    x-displayName: Játékosok és jelentések
    tags:
      - players
      - leaderboard
      - reports
  - name: browser-pages
    x-displayName: Böngészőoldalak
    tags:
      - pages
paths:
  /info:
    get:
      operationId: getServerInfo
      tags:
        - server-info
      summary: A szerver nevének, verzióinak, portjainak és regisztrációs szabályainak lekérése
      description: |-
        Amire a kliensnek szüksége van, mielőtt bejelentkezik vagy csatlakozik: a szerver neve és azonosítója, a WebSocket-protokoll verziói és a WebSocket helye, a regisztrációs szabályok, a hivatalos játékidők, valamint azok a korlátok, amelyeket a kliens egy űrlap elküldése előtt ellenőrizhet.

        **Korlátok:** csak a címenkénti réteg.
      security: []
      responses:
        '200':
          description: A szerver leírása.
          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`: a címenkénti réteg.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /auth/register:
    post:
      operationId: registerAccount
      tags:
        - auth
      summary: Fiók létrehozása
      description: |-
        Fiókot hoz létre: amint az e-mail-címét a címre küldött linkkel megerősítették, vagy e-mail-megerősítés nélkül azonnal.

        **E-mail-megerősítéssel** (`REQUIRE_EMAIL_VERIFICATION`, alapértelmezés): 202 `verification_sent`. Fiók még nem létezik: a regisztráció 24 órán át függőben van, és addig lefoglalja a felhasználónevet. A címre egy erre a 24 órára érvényes link megy ki, címenként legfeljebb 5 percenként egy (a `POST /auth/verify-email/resend` újat küld); a fiók a link használatakor (a `/verify-email` oldal gombjával) jön létre, megerősített címmel, és a játékos ezután bejelentkezhet. Addig az ezzel a felhasználónévvel történő bejelentkezés válasza `invalid_credentials`, mint egy ismeretlen fióknál, és a nyilvános profil nem létezik. Egy ugyanazzal a címmel történő új regisztráció felváltja a függőben lévőt. A válasz akkor is ugyanaz, ha a címet már egy másik fiók használja: ilyenkor nem megy ki link, helyette a cím tulajdonosa kap értesítést (legfeljebb óránként egyet), és a felhasználónév ugyanígy lefoglalódik, hogy semmi ne árulja el, tartozik-e fiók a címhez. Az a regisztráció, amelynek linkjét nem használták fel, 24 óra után törlődik, és a felhasználónév ismét szabad lesz.

        **E-mail-megerősítés nélkül** (`REQUIRE_EMAIL_VERIFICATION=false`): 201 `ready`, a fiók azonnal létrejön, és be lehet vele jelentkezni.

        **Szabályok.** `username`: `USERNAME_MIN`–`USERNAME_MAX` karakter (3–20), betűk, számjegyek, `_` és `-`, betűvel vagy számjeggyel kezdődően (a `GET /info` ezeket `limits` néven adja meg); a fenntartott neveket (`admin`, `moderator`, `deleted`...) és egyes előtagokat a szerver elutasítja; a kis- és nagybetűktől függetlenül egyedi. `email`: a széleiről levágott szóközökkel és kisbetűsen tárolva, egyszerű ASCII, pontot tartalmazó domainnel. `password`: legalább `PASSWORD_MIN_LENGTH` (10) karakter és legfeljebb 256 bájt UTF-8; nem tartalmazhatja a felhasználónevet vagy az e-mail-cím helyi részét, és nem lehet gyakori jelszó.

        **Hibák**, ebben a sorrendben ellenőrizve: 403 `registration_closed`; 400 `invalid_username`; 400 `invalid_email`; 400 `weak_password`; 409 `username_taken`; 428 `pow_required`; a jelszóhashelési sor hibái; 409 `email_taken` (csak e-mail-megerősítés nélkül).

        **Korlátok:** `auth`, majd `auth_register` (kliensenként óránként 10 regisztráció). **Munkabizonyíték:** mindig, ha a `POW_REGISTER_BITS` 0-nál nagyobb.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
            examples:
              first:
                summary: Első kísérlet, munkabizonyíték nélkül
                value:
                  username: alice
                  email: alice@example.org
                  password: correct horse battery
              withPow:
                summary: Újraküldés munkabizonyítékkal
                value:
                  username: alice
                  email: alice@example.org
                  password: correct horse battery
                  pow:
                    challenge: eyJ2IjoxLCJlIjoicmVnaXN0ZXIiLCJiIjoxOCwieCI6MTc5MDg4Mjk5MTIwMH0.6Ku_D7kw3S69OYxj-9KxsXszRCwz6ECnOQ-B6-v2erM
                    nonce: '123456'
      responses:
        '201':
          description: A fiók létrejött, és be lehet vele jelentkezni (ezen a szerveren nincs e-mail-megerősítés).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    const: ready
              example:
                status: ready
        '202':
          description: A regisztráció a megerősítő linkre vár (vagy a címhez már tartozik fiók; a válasz ezt nem árulja el).
          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` mezővel), `invalid_json`, `invalid_username`, `invalid_email` vagy `weak_password` `reason` mezővel: `too_short`, `too_long`, `contains_username`, `contains_email` vagy `too_common`.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`registration_closed`: ezen a szerveren a regisztráció le van zárva (a `GET /info` szerint `registration: closed`).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`username_taken`: a felhasználónév már egy fiókhoz tartozik, vagy egy másik cím függőben lévő regisztrációja foglalja. `email_taken`: a címet egy másik fiók használja (csak e-mail-megerősítés nélkül).'
        '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`: az `auth` vagy az `auth_register` korlát, vagy a jelszóhashelési sor.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (a jelszóhashelési sor, vagy az adatbázis zárolva maradt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /auth/login:
    post:
      operationId: logIn
      tags:
        - auth
      summary: Bejelentkezés jelszóval
      description: |-
        Bejelentkezés felhasználónévvel vagy e-mail-címmel és jelszóval. Két lehetséges válasz van, mindkettő 200-as állapotkóddal: egy munkamenet, vagy bekapcsolt kétlépcsős azonosítás esetén egy második lépés, amelyet 5 percen belül kell befejezni a `POST /auth/login/mfa` kéréssel.

        Az ismeretlen fiók, a hibás jelszó és a jelszó nélküli fiók ugyanazt a választ kapja, ugyanannyi idő után: 401 `invalid_credentials`. A fiókellenőrzések (`banned`, `email_unverified`) csak helyes jelszó után következnek. Egy bejelentkezési névvel elkövetett `AUTH_FAILURES_PER_ACCOUNT` (5) sikertelen kísérlettől kezdve minden kísérletnek várnia kell (429 `too_many_attempts`).

        **Korlát:** `auth`. **Munkabizonyíték:** csak sikertelen bejelentkezések hulláma idején (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: Egy munkamenet, vagy egy kétlépcsős azonosítású bejelentkezés második lépése.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginAnswer'
              examples:
                session:
                  summary: Bejelentkezve
                  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: A kétlépcsős azonosítás be van kapcsolva
                  value:
                    mfaRequired: true
                    mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                    expiresIn: 300
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_credentials`: ismeretlen fiók, hibás jelszó, vagy jelszó nélküli fiók.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: 'Csak helyes jelszó után: `banned`, `until` mezővel (Unix-idő, ms; végleges kitiltásnál `null`), vagy `email_unverified` (egy régebbi fiók, amelynek a címe még nincs megerősítve).'
        '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` (ennek a bejelentkezési névnek a hibakésleltetése), vagy `rate_limited` (az `auth` korlát, a jelszóhashelési sor).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (a jelszóhashelési sor, vagy az adatbázis zárolva maradt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /auth/login/mfa:
    post:
      operationId: logInWithSecondFactor
      tags:
        - auth
      summary: Bejelentkezés befejezése második faktorral
      description: |-
        Egy kétlépcsős azonosítású bejelentkezés második lépése, miután a `POST /auth/login` vagy egy Google-bejelentkezés (`POST /auth/sso/google/finish` vagy `POST /auth/sso/google/link`) `mfaRequired` választ adott. Küldje el a `code` (6 jegyű hitelesítő kód vagy helyreállító kód) vagy a `recoveryCode` mezőt. Az itt felhasznált helyreállító kód megszűnik.

        Egy lépés legfeljebb 5 hibás kódot fogad el; a lépés akkor is véget ér, ha a jelszót visszaállítják vagy módosítják. Egy Google-összekapcsolás jelszavas lépése után a kapcsolat csak akkor tárolódik, ha itt a kód sikeres.

        **Korlátok:** `auth`, valamint fiókonként 15 percenként legfeljebb `AUTH_MFA_PER_ACCOUNT` (10) kód, bármilyen címről; ezen felül a szerver nem ellenőrzi a kódot, így a helyreállító kód nem használódik el.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MfaLoginRequest'
            examples:
              authenticator:
                summary: Hitelesítő kód
                value:
                  mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                  code: '123456'
              recovery:
                summary: Helyreállító kód
                value:
                  mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                  recoveryCode: j7v5-3ezx-zn
      responses:
        '200':
          description: Bejelentkezve.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionAnswer'
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_request`: sem `code`, sem `recoveryCode` nem érkezett, vagy a törzs megsérti a sémát; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_mfa_token`: a lépés lejárt, már felhasználták, vagy 5 hibás kód után véget ért, vagy a jelszót az első lépés óta visszaállították vagy módosították (jelentkezzen be újra). `invalid_code`: hibás vagy már felhasznált kód.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`banned` (`until` mezővel), `email_unverified`.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_already_linked`: csak egy Google-összekapcsolás jelszavas lépése után fordul elő: a Google-fiókot időközben egy másik fiókhoz kapcsolták.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: csak egy Google-összekapcsolás jelszavas lépése után fordul elő: a fiók időközben megváltozott; kezdje újra a bejelentkezést.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts`: a fiók hibakésleltetése, vagy elfogyott a fiók utolsó 15 percre jutó `AUTH_MFA_PER_ACCOUNT` kódja. `rate_limited`: az `auth` korlát.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` `retryAfter: 1` értékkel: az adatbázis zárolva maradt (Google-összekapcsolási lépés után semmi sem lett összekapcsolva: kezdje újra a bejelentkezést). `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /auth/logout:
    post:
      operationId: logOut
      tags:
        - sessions
      summary: Kijelentkezés ebből a munkamenetből
      description: |-
        Visszavonja a használt token munkamenetét. A vele megnyitott WebSocket-kapcsolat lezárul. Nincs törzs (vagy `{}`).

        **Korlát:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Kijelentkezve.
          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`: a `{}` objektumtól eltérő törzs; `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`: a `sessions` korlát vagy a fiókkeret.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (az adatbázis zárolva maradt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /auth/logout-all:
    post:
      operationId: logOutEverywhere
      tags:
        - sessions
      summary: Kijelentkezés minden munkamenetből
      description: |-
        Visszavonja a fiók összes munkamenetét, ezt is beleértve. A velük megnyitott WebSocket-kapcsolatok lezárulnak. Nincs törzs (vagy `{}`).

        **Korlát:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Minden munkamenet ki lett jelentkeztetve.
          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`: a `{}` objektumtól eltérő törzs; `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`: a `sessions` korlát vagy a fiókkeret.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (az adatbázis zárolva maradt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /auth/verify-email/resend:
    post:
      operationId: resendVerificationEmail
      tags:
        - auth
      summary: Megerősítő link újraküldése
      description: |-
        Újraküldi az e-mail-megerősítő linket. A válasz a címtől függetlenül 202 `accepted`, hogy soha ne árulja el, használja-e a címet fiók vagy regisztráció.

        Egy kérés címenként legfeljebb 5 percenként egyszer fejt ki hatást. Az ezzel a címmel függőben lévő regisztráció ismét 24 órát kap, akár használja a címet egy másik fiók, akár nem, hogy a felhasználónév mindkét esetben ugyanannyi ideig foglalt maradjon. Link csak ennek a regisztrációnak megy ki, ha a címhez nem tartozik fiók (az új, 24 óráig érvényes link felváltja az előzőt), vagy egy ezzel a címmel rendelkező aktív, meg nem erősített fióknak.

        **Korlátok:** `auth`, majd `auth_mail` (kliensenként óránként 10).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmailRequest'
            example:
              email: alice@example.org
      responses:
        '202':
          description: Elfogadva (a címtől függetlenül).
          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`: az `auth` vagy az `auth_mail` korlát (a címtől függetlenül).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` `retryAfter: 1` értékkel: az adatbázis zárolva maradt a fiók keresése vagy egy meg nem erősített fiók linkjének tárolása közben (a függőben lévő regisztráció megújítása csak a lehetőségekhez mérten történik, és soha nem okozza a kérés meghiúsulását); a cím 5 perce ettől függetlenül elindul, így az ezen belül érkező új kérés nem csinál semmit. `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /auth/password/forgot:
    post:
      operationId: requestPasswordReset
      tags:
        - auth
      summary: Jelszó-visszaállító link küldése
      description: |-
        Egy óráig érvényes jelszó-visszaállító linket küld, amely a `/reset-password` oldalt nyitja meg. A válasz a címtől függetlenül 202 `accepted`. Link csak aktív fióknak megy ki, címenként legfeljebb 5 percenként egyszer. Egy csak Google-bejelentkezést használó fiók így állítja be az első jelszavát.

        A jelszó-helyreállításnak vannak a legszigorúbb korlátai az API-ban, mindegyik a teljes szerverre számolva: kliensenként (egy IPv4-cím vagy egy IPv6 /64) óránként 3 kérés (`AUTH_FORGOT_PER_HOUR`) és 24 óránként 10 (`AUTH_FORGOT_PER_DAY`), IPv6 /48-anként ezek háromszorosa, az `auth` korláton (10 percenként 20) és a címenként 5 percenként egy levélen felül. Sem a 202-es, sem a 429-es válasz nem árulja el, hogy használja-e fiók a címet. Az új jelszó beállításának saját korlátja van (`auth_reset`).

        **Korlátok:** `auth`, `auth_forgot`, majd `auth_forgot_day`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmailRequest'
            example:
              email: alice@example.org
      responses:
        '202':
          description: Elfogadva (a címtől függetlenül).
          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`: az `auth`, az `auth_forgot` vagy az `auth_forgot_day` korlát (a címtől függetlenül).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` `retryAfter: 1` értékkel: az adatbázis zárolva maradt a fiók keresése vagy a link tárolása közben; a cím 5 perce ettől függetlenül elindul, így az ezen belül érkező új kérés nem küld linket. `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /auth/password/reset:
    post:
      operationId: resetPassword
      tags:
        - auth
      summary: Új jelszó beállítása egy visszaállító link tokenjével
      description: |-
        Új jelszót állít be egy visszaállító link tokenjével (a `/reset-password` oldal ugyanezt teszi). Az új jelszóra a regisztráció szabályai vonatkoznak.

        A visszaállításnak a következő hatásai vannak, egyetlen tranzakcióban (vagy mind, vagy egyik sem): visszavonja az összes munkamenetet, és törli a függőben lévő e-mail-cím-módosítást; a fiók többi visszaállító linkje érvényét veszti; a cím megerősítettnek számít (a link ezt bizonyította); a tulajdonos levelet kap. A kétlépcsős azonosítás változatlan marad. Jelszóhashelési sor hibája, 503-as vagy 500-as válasz után semmi sem változott: a régi jelszó és a munkamenetek megmaradnak, a link pedig érvényes marad.

        **Korlátok:** `auth`, majd `auth_reset` (kliensenként óránként 10, IPv6 /48-anként 30, az oldallal közösen: minden kísérlet egy jelszó hashelésével jár).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasswordResetRequest'
            example:
              token: A0ATfmXCDKFC8UkSxLJCD85K7au5nMIrZ2P3K-1VWQg
              newPassword: a much better passphrase
      responses:
        '200':
          description: A jelszó megváltozott, és minden munkamenet ki lett jelentkeztetve.
          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`: a link érvénytelen, már felhasználták vagy lejárt, vagy olyan címre küldték, amely már nem a fiókhoz tartozik. `weak_password` (`reason` mezővel). `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`: az `auth` vagy az `auth_reset` korlát, vagy a jelszóhashelési sor.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: a jelszóhashelési sor, vagy az adatbázis zárolva maradt (`retryAfter: 1`, semmi sem változott). `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /auth/sso/google/start:
    post:
      operationId: startGoogleSignIn
      tags:
        - google-sign-in
      summary: Google-bejelentkezés indítása
      description: |-
        Google-bejelentkezési kísérletet indít a PKCE-kihíváshoz és a játék `127.0.0.1` címen futó figyelőjének portjához. A válasz megadja a rendszer böngészőjében megnyitandó Google URL-t és a kísérlet `state` értékét; a kísérlet 10 percig érvényes.

        Az `authUrl` a következőket tartalmazza: `client_id`, `redirect_uri` (a `redirectPort` értékből és a szerver eredetcímkéjéből összeállítva), `response_type=code`, `scope=openid email profile`, `state`, `nonce`, `code_challenge` (a szerver saját, a Google felé használt PKCE-ellenőrzőjének S256 értéke) `code_challenge_method=S256` mellett, valamint `prompt=select_account`. A játék megnyitás előtt ellenőrzi (lásd a szakasz leírását).

        **Korlát:** `sso_start`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SsoStartRequest'
            example:
              codeChallenge: 6e7diXEYxG7OTYw7STfNOltEeLxAilPthC_txzaE0xA
              redirectPort: 51234
      responses:
        '200':
          description: A kísérlet.
          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`: ezen a szerveren nem érhető el a Google-bejelentkezés.'
        '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`: az `sso_start` korlát.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (az adatbázis zárolva maradt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /auth/sso/google/web/start:
    post:
      operationId: startGoogleWebSignIn
      tags:
        - google-sign-in
      summary: Google-bejelentkezés indítása webhelyen
      description: |-
        Egy webhely Google-bejelentkezési kísérletét indítja el az oldala PKCE-kihívásához. A válasz ugyanaz, mint a játék esetén: a Google URL, amelyre navigálni kell, és a kísérlet `state` értéke; a kísérlet 10 percig érvényes. Akkor érhető el, ha a `GET /info` szerint `sso.googleWeb: true`.

        Az `authUrl` a webhely `client_id` értékét és a `redirect_uri=GOOGLE_WEB_REDIRECT_URI` paramétert tartalmazza, a többi paraméter ugyanaz, mint a játéknál. Az oldal ellenőrzi, mielőtt odanavigál (lásd a szakasz leírásában: **Webhelyen**).

        **Korlát:** `sso_start`, a játék indításával közösen.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SsoWebStartRequest'
            example:
              codeChallenge: 6e7diXEYxG7OTYw7STfNOltEeLxAilPthC_txzaE0xA
      responses:
        '200':
          description: A kísérlet.
          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`: hiányzó vagy hibás formátumú `codeChallenge`, vagy bármilyen más mező (a `redirectPort` is).'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: ezen a szerveren nem érhető el a Google-bejelentkezés, vagy webhelyek számára nem érhető el.'
        '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`: az `sso_start` korlát (a játék indításával közös).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (az adatbázis zárolva maradt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /auth/sso/google/finish:
    post:
      operationId: finishGoogleSignIn
      tags:
        - google-sign-in
      summary: A Google válaszának átadása a szervernek
      description: |-
        Átadja a szervernek azt a `code` és `state` értéket, amelyet a Google a játék figyelőjének vagy a webhely oldalának küldött, a kísérlet azonosítójával és a PKCE-ellenőrzővel (verifier) együtt. A kísérlet azonosítója az ellenőrző nélkül használhatatlan, a kód pedig a szerver saját PKCE-ellenőrzője és klienstitka (client secret) nélkül használhatatlan.

        A szerver ellenőrzi a kísérletet (ismeretlen, felhasznált vagy lejárt: 410), majd az ellenőrzőt (hibás ellenőrző esetén a kísérlet használható marad), ezután egyszer felhasználja a kísérletet, ellenőrzi a `state` és az `iss` értéket, a Google-nél beváltja a kódot, és ellenőrzi az ID-tokent. A válasz (200) a következők egyike:

        - `{ token, expiresAt, user }`: bejelentkezés a kapcsolt fiókba;
        - `{ mfaRequired, mfaToken, expiresIn }`: folytatás a `POST /auth/login/mfa` kéréssel;
        - `{ needsUsername, ssoTicket, suggestedUsername }`: új fiók; folytatás 10 percen belül a `POST /auth/sso/complete` kéréssel. A `suggestedUsername` a Google-névből vagy a címből származik, és `""`, ha semmi sem megfelelő;
        - `{ needsPassword, linkTicket, username, expiresIn }`: az ezzel a címmel rendelkező fióknak van jelszava; folytatás a `POST /auth/sso/google/link` kéréssel. Még semmi sincs összekapcsolva.

        **Korlát:** `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: Egy munkamenet, egy második lépés, vagy egy első Google-bejelentkezés következő lépése.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SsoFinishAnswer'
              examples:
                session:
                  summary: Bejelentkezve a kapcsolt fiókba
                  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: A kétlépcsős azonosítás be van kapcsolva
                  value:
                    mfaRequired: true
                    mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                    expiresIn: 300
                needsUsername:
                  summary: Új játékos
                  value:
                    needsUsername: true
                    ssoTicket: sso_gYIKKhwnz1AqWfFxOC3R-ZvCACw06q74fEMtGEOJQWU
                    suggestedUsername: alice
                needsPassword:
                  summary: A címet egy jelszóval rendelkező fiók használja
                  value:
                    needsPassword: true
                    linkTicket: sso_ZfEI6iZjePTHbnbevupaD9O7GjBCzcL-GJu2XGOdFGc
                    username: alice
                    expiresIn: 600
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_verifier`: az ellenőrző (verifier) nem egyezik a kísérlet kihívásával (a kísérlet használható marad). `sso_email_unverified`: a Google nem erősítette meg a címet. `registration_closed`. `account_disabled`. Csak kapcsolt fióknál: `banned` (`until` mezővel), `email_unverified`.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: ezen a szerveren nem érhető el a Google-bejelentkezés.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_account_exists`: a címet egy jelszó nélküli aktív fiók használja (a válasz nem nevezi meg).'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: ismeretlen, felhasznált vagy lejárt kísérlet.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: az `sso_finish` korlát.'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/BadGateway'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (az adatbázis zárolva maradt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /auth/sso/google/link:
    post:
      operationId: linkGoogleAccount
      tags:
        - google-sign-in
      summary: Google-fiók összekapcsolása egy meglévő fiókkal annak jelszavával
      description: |-
        Miután a `finish` `needsPassword` választ adott: a Google-fiókot a meglévő fiókhoz kapcsolja annak a fióknak a játékban begépelt jelszavával. A válasz (200) egy munkamenet (a kapcsolat tárolódik, és a játékos bejelentkezik), vagy bekapcsolt kétlépcsős azonosítás esetén `{ mfaRequired, mfaToken, expiresIn }`: folytatás a `POST /auth/login/mfa` kéréssel, és a kapcsolat csak akkor tárolódik, ha ott egy kód sikeres.

        Hibás jelszó esetén a jegy használható marad, összesen 5 próbálkozásra. A szerver közvetlenül a jelszó ellenőrzése előtt von le egy próbálkozást: a 429 `too_many_attempts` és a 428 `pow_required` nem von le egyet sem, a jelszóhashelési sor elutasítása (503 `server_busy`, 429 `rate_limited`) viszont már levont egyet, és a fiók sikertelen kísérletének számít. A hibakésleltetés ugyanaz a számláló, mint a `POST /auth/login` esetén.

        **Korlát:** `auth` (az IPv6 /48-anként számolt értékével együtt). **Munkabizonyíték:** ugyanúgy, mint a `POST /auth/login` esetén.
      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: Munkamenet, vagy a bejelentkezés második lépése.
          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`: hibás jelszó; a jegy megmarad, összesen 5 próbálkozásig.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`banned` (`until` mezővel), csak helyes jelszó után.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: ez a szerver nem kínál Google-bejelentkezést.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_already_linked`: a Google-fiókot időközben egy másik fiókhoz kapcsolták.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: ismeretlen, már felhasznált vagy lejárt jegy, az 5. hibás jelszó, olyan fiók, amelynek az állapota vagy a címe megváltozott a `finish` óta, vagy olyan, amelynek az állapota, a címe, a jelszava vagy a kétlépcsős azonosítása megváltozott, miközben a Google-kapcsolat tárolása zajlott. Kezdje újra a bejelentkezést.'
        '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` (a fiók hibakésleltetése), vagy `rate_limited` (az `auth` korlát, a jelszóhashelési sor).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: a jelszóhashelési sor, vagy az adatbázis zárolva maradt (`retryAfter: 1`, semmi sem lett összekapcsolva). `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /auth/sso/complete:
    post:
      operationId: completeGoogleSignUp
      tags:
        - google-sign-in
      summary: Fiók létrehozása az első Google-bejelentkezéskor
      description: |-
        Miután a `finish` `needsUsername` választ adott: létrehozza a fiókot a választott felhasználónévvel (a regisztráció szabályai érvényesek), és bejelentkezteti. A fióknak nincs jelszava: a `hasPassword` értéke `false`, és az „Elfelejtette a jelszavát?” funkcióval (`POST /auth/password/forgot`) kaphat jelszót.

        **Korlát:** `auth` (az IPv6 /48-onkénti számlálásával együtt).
      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: A fiók létrejött, és be van jelentkezve.
          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`: ez a szerver nem kínál Google-bejelentkezést.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`username_taken` (egy fiók már használja a felhasználónevet, vagy egy másik cím függőben lévő regisztrációja foglalja), `sso_already_linked`, `email_taken`.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: ismeretlen, már felhasznált vagy lejárt jegy.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: az `auth` korlát.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (az adatbázis zárolva maradt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /auth/sessions:
    get:
      operationId: listSessions
      tags:
        - sessions
      summary: A bejelentkezett eszközök listázása
      description: |-
        A fiók aktív munkamenetei (bejelentkezett eszközök), a legutóbb használttal kezdve. A `current` jelöli a kérést küldő munkamenetet. A `lastSeenAt` legfeljebb 5 percenként frissül (később, amíg a szerver erősen le van maradva); az `expiresAt` az abszolút lejárat, és az inaktivitási korlát a munkamenetet korábban is megszüntetheti.

        **Korlát:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Az aktív munkamenetek.
          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`: a `sessions` korlát vagy a fiókkeret.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (az adatbázis zárolva maradt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /auth/sessions/{id}:
    delete:
      operationId: revokeSession
      tags:
        - sessions
      summary: Egy eszköz kijelentkeztetése
      description: |-
        Kijelentkezteti a fiók egy munkamenetét; az aktuális munkamenet is kijelentkeztethető. A vele megnyitott WebSocket bezárul. Nincs törzs (vagy `{}`).

        **Korlát:** `sessions`.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/SessionId'
      responses:
        '200':
          description: A munkamenet kijelentkeztetve.
          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`: a törzs nem `{}`, vagy az `id` URL-kódolása érvénytelen; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: ennek a fióknak nincs ilyen azonosítójú aktív munkamenete.'
        '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`: a `sessions` korlát vagy a fiókkeret.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (az adatbázis zárolva maradt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /account/me:
    get:
      operationId: getAccount
      tags:
        - account
      summary: A fiók, az értékszámok és az aktív szankciók lekérése
      description: |-
        A fiók úgy, ahogy a játékosa látja: a fióknézet (minden bejelentkezési válasz `user` mezője is ez), egy értékszám-rekord minden olyan kategóriához, amelyben a játékos értékelt játszmákat játszott, az aktív szankciók és a kitiltás. A csalásellenes rendszer integritási szintje soha nem látható.

        **Korlát:** `account`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: A fiók.
          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`, vagy `invalid_token` (akkor is, ha a fiókot törölték).'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: az `account` korlát vagy a fiókkeret.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (az adatbázis zárolva maradt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /account/preferences:
    put:
      operationId: updatePreferences
      tags:
        - account
      summary: Közvetlen kihívások fogadása vagy elutasítása
      description: |-
        Beállítja, hogy más játékosok név szerint kihívhatják-e ezt a játékost. `none` értékkel a szerver elutasítja a közvetlen kihívásokat: a kihívó azt a tájékoztatást kapja, hogy a játékos nem érhető el. Nincs újrahitelesítés.

        **Korlát:** `account`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Preferences'
            example:
              acceptChallenges: none
      responses:
        '200':
          description: A most érvényes beállítások.
          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`: az `account` korlát vagy a fiókkeret.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (az adatbázis zárolva maradt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /account/password:
    post:
      operationId: changePassword
      tags:
        - account
      summary: Jelszó módosítása
      description: |-
        Módosítja a jelszót. A jelenlegi jelszó szükséges, második faktor viszont nem, bekapcsolt kétlépcsős azonosítás esetén sem. Az új jelszóra a regisztráció szabályai vonatkoznak (ellenőrzésük a jelenlegi jelszó után történik).

        A módosításnak a következő hatásai vannak, egyetlen tranzakcióban (vagy mind, vagy egyik sem): visszavonja az összes többi munkamenetet (ez a munkamenet bejelentkezve marad), törli a függőben lévő e-mail-cím-módosítást, érvényteleníti a fiók jelszó-visszaállító linkjeit, és e-mailt küld a tulajdonosnak.

        **Újrahitelesítés:** csak a jelszó. **Korlátok:** `reauth`, majd `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: A jelszó megváltozott.
          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` (csak Google-bejelentkezést használó fiók), `weak_password` (`reason` mezővel), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password`: hibás jelenlegi jelszó, vagy olyan jelszó-visszaállítás vagy -módosítás, amely a kérés ellenőrzése közben történt.'
        '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` (a fiók sikertelen újrahitelesítései), vagy `rate_limited` (a `reauth` vagy a `reauth_user` korlát, a fiókkeret, a jelszóhashelési sor).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: a jelszóhashelési sor, vagy az adatbázis zárolva maradt (`retryAfter: 1`, semmi sem változott). `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /account/mfa/totp/setup:
    post:
      operationId: startTotpSetup
      tags:
        - two-step-verification
      summary: A kétlépcsős azonosítás bekapcsolásának megkezdése
      description: |-
        Eltárol egy új, függőben lévő hitelesítő titkos kulcsot, amely felváltja a korábbi függőben lévőt, és visszaadja a hitelesítő alkalmazás számára (szövegként, valamint QR-kódként megjelenítendő `otpauth://` URI-ként). A kétlépcsős azonosítás még nincs bekapcsolva: a `POST /account/mfa/totp/enable` kapcsolja be ennek a titkos kulcsnak egy kódjával.

        **Újrahitelesítés:** csak a jelszó. **Korlátok:** `reauth`, majd `reauth_user`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasswordOnlyRequest'
            example:
              password: correct horse battery
      responses:
        '200':
          description: A függőben lévő titkos kulcs.
          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` (csak Google-bejelentkezést használó fiók), `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` (a jelszó előtt ellenőrizve).'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (a fiók sikertelen újrahitelesítései), vagy `rate_limited` (a `reauth` vagy a `reauth_user` korlát, a fiókkeret, a jelszóhashelési sor).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (a jelszóhashelési sor, vagy az adatbázis zárolva maradt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /account/mfa/totp/enable:
    post:
      operationId: enableTotp
      tags:
        - two-step-verification
      summary: A kétlépcsős azonosítás bekapcsolásának befejezése és a helyreállító kódok lekérése
      description: |-
        Bekapcsolja a kétlépcsős azonosítást a függőben lévő titkos kulcs egy kódjával (pontosan 6 számjegy). Itt nem kell jelszó; azt a beállításkor kellett megadni. A válasz 10 helyreállító kódot tartalmaz, amelyek csak most jelennek meg.

        **Korlátok:** `reauth`, majd `reauth_user`; a hibás kód beszámít a fiók sikertelen újrahitelesítései közé.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TotpEnableRequest'
            example:
              code: '123456'
      responses:
        '200':
          description: A kétlépcsős azonosítás be van kapcsolva.
          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`: a `code` nem pontosan 6 számjegy, vagy a törzs nem felel meg a sémának; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_code`: hibás kód (ellenőrizze az eszköz óráját).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`mfa_already_enabled`; `mfa_setup_required`: nincs függőben lévő titkos kulcs (előbb hívja meg a `POST /account/mfa/totp/setup` végpontot).'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (a fiók sikertelen újrahitelesítései), vagy `rate_limited` (a `reauth` vagy a `reauth_user` korlát, a fiókkeret).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (az adatbázis zárolva maradt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /account/mfa/totp/disable:
    post:
      operationId: disableTotp
      tags:
        - two-step-verification
      summary: A kétlépcsős azonosítás kikapcsolása
      description: |-
        Kikapcsolja a kétlépcsős azonosítást. A titkos kulcs és a helyreállító kódok törlődnek, a tulajdonos pedig e-mailt kap.

        **Újrahitelesítés:** a jelszó és egy hitelesítő kód vagy egy helyreállító kód (a `code` és a `recoveryCode` közül az egyik kötelező). **Korlátok:** `reauth`, majd `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: A kétlépcsős azonosítás ki van kapcsolva.
          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` (csak Google-bejelentkezést használó fiók), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`mfa_code_required` (sem `code`, sem `recoveryCode` nincs megadva; a jelszó előtt ellenőrizve), `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` (sikertelen újrahitelesítések, vagy túl sok kipróbált kód ezen a fiókon), vagy `rate_limited` (a `reauth` vagy a `reauth_user` korlát, a fiókkeret, a jelszóhashelési sor).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (a jelszóhashelési sor, vagy az adatbázis zárolva maradt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /account/mfa/recovery-codes:
    post:
      operationId: regenerateRecoveryCodes
      tags:
        - two-step-verification
      summary: A helyreállító kódok cseréje
      description: |-
        10 új kódra cseréli a helyreállító kódokat; a régiek érvényüket vesztik.

        **Újrahitelesítés:** a jelszó és egy hitelesítő kód a `code` mezőben (helyreállító kódra a válasz 403 `invalid_code`). **Korlátok:** `reauth`, majd `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: Az új helyreállító kódok, amelyek csak most jelennek meg.
          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` (csak Google-bejelentkezést használó fiók), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password`, `mfa_code_required`, `invalid_code` (helyreállító kódra is).'
        '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` (sikertelen újrahitelesítések, vagy túl sok kipróbált kód ezen a fiókon), vagy `rate_limited` (a `reauth` vagy a `reauth_user` korlát, a fiókkeret, a jelszóhashelési sor).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (a jelszóhashelési sor, vagy az adatbázis zárolva maradt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /account/email:
    post:
      operationId: changeEmail
      tags:
        - email-change
      summary: Az e-mail-cím módosítása
      description: |-
        Módosítja a fiók e-mail-címét. A szerver a `newEmail` értékéről levágja a kezdő és záró szóközöket, kisbetűssé alakítja, majd úgy ellenőrzi, mint egy regisztrációkor megadott címet.

        **E-mailes megerősítéssel** (alapértelmezés): 202 `verification_sent`. Az új címre 24 óráig érvényes link megy; ez a `/confirm-email-change` oldalt nyitja meg, és a cím csak akkor változik meg, amikor a játékos megnyomja az oldal gombját. Addig a `GET /account/me` az új címet `pendingEmail` mezőként mutatja. Egy új kérés felváltja a függőben lévőt; jelszómódosítás vagy jelszó-visszaállítás törli. Egy adott új címre 5 percenként legfeljebb egy link megy, bárki kéri is (a már függőben lévő módosításra vonatkozó kérés megtartja a korábban elküldött linket, amely érvényes marad); a válasz ugyanaz. A jelenlegi cím értesítést kap arról, hogy egy maszkolt címre (`a***@example.org`) való módosítást kértek. A válasz és a `pendingEmail` akkor is ugyanaz, ha egy másik fiók már használja az új címet: ilyenkor nem megy ki link, így ez a módosítás soha nem fejeződik be, ehelyett pedig annak a címnek a tulajdonosa kap értesítést (óránként legfeljebb egyet).

        A link megerősítésekor a cím megváltozik és megerősítettnek számít, az eszközök bejelentkezve maradnak, a korábban elküldött linkek (megerősítés, jelszó-visszaállítás, egyéb módosítások) érvényüket vesztik, a korábbi cím pedig értesítést kap, amelyben az új cím maszkolva szerepel.

        **E-mailes megerősítés nélkül** (`REQUIRE_EMAIL_VERIFICATION=false`): a cím azonnal megváltozik (200 `email_changed`), és a korábbi cím értesítést kap. Ha egy másik fiók használja a címet, a válasz 409 `email_taken`, és annak a címnek a tulajdonosa kapja meg az értesítést.

        **Újrahitelesítés:** a jelszó és – kétlépcsős azonosítás esetén – egy hitelesítő kód vagy egy helyreállító kód. Az `invalid_email` és a `same_email` ellenőrzése a jelszó előtt történik, így ezek nem számítanak sikertelen próbálkozásnak. **Korlátok:** `reauth`, majd `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: A cím azonnal megváltozott (ezen a szerveren nincs e-mailes megerősítés).
          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: Az új cím, ahogy tárolva van.
              example:
                status: email_changed
                email: alice.new@example.org
        '202':
          description: Megerősítő link ment az új címre (vagy a cím egy másik fiókhoz tartozik; a válaszból ez nem derül ki).
          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` (a fiók jelenlegi címe), `password_not_set` (csak Google-bejelentkezést használó fiók), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password` (akkor is, ha egy jelszómódosítás vagy jelszó-visszaállítás megelőzte a kérést: ilyenkor nem megy ki link), `mfa_code_required`, `invalid_code`.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`email_taken`: csak e-mailes megerősítés nélkül.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (sikertelen újrahitelesítések, vagy túl sok kipróbált kód ezen a fiókon), vagy `rate_limited` (a `reauth` vagy a `reauth_user` korlát, a fiókkeret, a jelszóhashelési sor).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: a jelszóhashelési sor, vagy az adatbázis zárolva maradt (`retryAfter: 1`: semmi sem változott, és ugyanaz a kérés újra elküldhető). `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /account/export:
    post:
      operationId: exportAccountData
      tags:
        - data-export
      summary: A fiók adatainak letöltése
      description: |-
        Minden, amit a szerver a fiókról tárol, egyetlen menthető JSON-fájlként (`format` `scacelith-account-export`, `version` 1). Az exportálás biztonsági eseményt rögzít (`account_exported`). A dokumentum `notes` tömbje egyszerű angol nyelven közli a játékossal, mi maradt ki.

        **Soha nem kerül az exportba:** a jelszóhash, a kétlépcsős azonosítás titkos kulcsa és a helyreállító kódok; semmilyen munkamenet- vagy linktoken, sem ezek hashe; a csalásellenes rendszer adatai (integritási szint és pontszám, anomáliák, a játszmák elemzése, egy jelentés súlya); a más játékosok által a játékosról tett jelentések; a moderátorok személye; más játékosok magánadatai (az ellenfelek a nyilvános nevükkel és értékszámukkal szerepelnek, semmi sem árulja el, hogy egy másik játékost szankcionáltak-e, és nem szerepel olyan IP-cím, amely esetleg egy másik személyé).

        **Újrahitelesítés:** a jelszó és – kétlépcsős azonosítás esetén – egy hitelesítő kód vagy egy helyreállító kód. **Korlátok:** `account_export` (játékosonként óránként 5, az egész szerverre; minden kísérlet számít, a sikertelenek is; elsőként ellenőrizve), majd `reauth` és `reauth_user`. **Időkorlát:** 60 másodperc.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReauthCredentials'
            example:
              password: correct horse battery
              code: '123456'
      responses:
        '200':
          description: Az exportdokumentum, mellékletként.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            Content-Disposition:
              description: '`attachment; filename="scacelith-account-<username>.json"`. A felhasználónévben a betűkön, a számjegyeken és a `_`, `.` és `-` jeleken kívüli karakterek helyére `_` kerül.'
              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` (csak Google-bejelentkezést használó fiók), `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` (sikertelen újrahitelesítések, vagy túl sok kipróbált kód ezen a fiókon), vagy `rate_limited` (az `account_export`, a `reauth` vagy a `reauth_user` korlát, a fiókkeret, a jelszóhashelési sor).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (az adatbázis zárolva maradt, `Retry-After: 1` fejléccel), `server_busy` (a jelszóhashelési sor), `timeout` (60 másodperc). Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /account/delete:
    post:
      operationId: deleteAccount
      tags:
        - account-deletion
      summary: A fiók törlése
      description: |-
        Törli a fiókot; ez nem vonható vissza.

        - A szerver azonnal visszavon minden munkamenetet: a token ettől kezdve 401 `invalid_token` választ kap.
        - A felhasználónév `deleted#<id>` lesz, a fiókban és minden játszmarekordban.
        - Törlődnek: az e-mail-cím, a jelszóhash, a kétlépcsős azonosítás titkos kulcsa és a helyreállító kódok, a munkamenetek és a linktokenek, a Google-kapcsolat, a csalásellenes rendszer integritási rekordja, valamint a biztonsági eseményekkel együtt tárolt IP-címek.
        - Az értékszámok és a játszmák megmaradnak. A játszmák az anonim név alatt továbbra is olvashatók, a `GET /players/{username}` pedig 404-es választ ad a korábbi névre.

        **Újrahitelesítés:** a jelszó és – kétlépcsős azonosítás esetén – egy hitelesítő kód vagy egy helyreállító kód. **Korlátok:** `reauth`, majd `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: A fiók törölve.
          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` (csak Google-bejelentkezést használó fiók), `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` (sikertelen újrahitelesítések, vagy túl sok kipróbált kód ezen a fiókon), vagy `rate_limited` (a `reauth` vagy a `reauth_user` korlát, a fiókkeret, a jelszóhashelési sor).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (a jelszóhashelési sor, vagy az adatbázis zárolva maradt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /account/games:
    get:
      operationId: listAccountGames
      tags:
        - game-history
      summary: A játékos játszmáinak listázása, szűrve és lapozva
      description: |-
        A bejelentkezett játékos játszmái, a legújabbal kezdve, szűrve és lapozva, a szűrőnek megfelelő játszmák számával. Minden lekérdezési paraméter elhagyható, és az üres érték hiányzónak számít.

        Lapozás: adja meg az előző oldal `next` értékét `before` paraméterként; az utolsó oldalon a `next` értéke `null`. A `total` a szűrőnek megfelelő játszmákat számolja, az összes oldalon együtt.

        **Korlát:** `account_games`.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/BeforeQuery'
        - $ref: '#/components/parameters/LimitQuery'
        - name: category
          in: query
          required: false
          description: Hivatalos kategória azonosítója (`3+2` vagy `3%2B2`), vagy `custom` minden más játékidejű játszmához.
          schema:
            type: string
            pattern: '^\s*([0-9]+[+ ][0-9]+|custom)\s*$'
          example: '3+2'
        - name: rated
          in: query
          required: false
          description: Csak az értékelt (`true`) vagy a barátságos (`false`) játszmák.
          schema:
            type: string
            enum:
              - 'true'
              - 'false'
        - name: result
          in: query
          required: false
          description: Csak a megnyert, az elvesztett vagy a döntetlenre végződött játszmák, a játékos szemszögéből. A megszakított játszmák csak e szűrő nélkül jelennek meg.
          schema:
            type: string
            enum:
              - win
              - loss
              - draw
      responses:
        '200':
          description: A korábbi játszmák egy oldala.
          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` (a `before` nem játszmaazonosító), `invalid_limit` vagy `invalid_filter` (`category`, `rated` vagy `result`), mindegyik a paramétert megnevező `field` mezővel.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: az `account_games` korlát vagy a fiókkeret.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (az adatbázis zárolva maradt; `retryAfter: 1` a törzsben, `Retry-After` fejléc nélkül), `server_busy` (a munkamenet keresésekor az adatbázis zárolva volt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /games/{id}:
    get:
      operationId: getGame
      tags:
        - games
      summary: Játszmarekord lekérése a lépésekkel és az óraállásokkal
      description: |-
        Egy játszmarekord a lépéseivel és az óraállásaival. Token nélkül, vagy olyan játékos tokenjével, aki nem játszott a játszmában, a nyilvános válasz érkezik. Ha a token játékosa játszott a játszmában, a válasz a `you` és a `reportable` mezőt is tartalmazza.

        **Korlát:** `public_read` (tokennel játékosonként, token nélkül kliensenként).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
      responses:
        '200':
          description: A játszmarekord.
          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`: nem legfeljebb 16 jegyű pozitív egész szám (2^53 alatt); `invalid_request`: érvénytelen URL-kódolás.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: a kérés tokent tartalmazott, de az érvénytelen.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: nincs ilyen játszma.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: a `public_read` korlát vagy a fiókkeret.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (az adatbázis zárolva maradt; `retryAfter: 1` a törzsben, `Retry-After` fejléc nélkül), `server_busy` (a munkamenet keresésekor az adatbázis zárolva volt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /games/{id}/pgn:
    get:
      operationId: getGamePgn
      tags:
        - games
      summary: Játszma letöltése PGN-fájlként
      description: |-
        Ugyanaz a játszma PGN-fájlként: egyetlen játszma `\n` sorvégekkel, a lépések szövege 80 oszlopnál rövidebb sorokban. A válasz tokennel és token nélkül is ugyanaz.

        PGN-címkék, ebben a sorrendben: `Event` (`<SERVER_NAME> rated <category>` vagy `<SERVER_NAME> casual <category>`), `Site` (`SERVER_PUBLIC_HOST`), `Date` (a kezdés dátuma UTC szerint), `Round`, `White`, `Black`, `Result` (megszakított játszmánál `*`), `UTCDate` és `UTCTime` (a kezdés), `WhiteElo` és `BlackElo` (az értékszámok a kezdéskor, vagy `-`), `WhiteRatingDiff` és `BlackRatingDiff` (a változások, például `+10` és `-10`, minden értékelt játszmában, `+0`, ha az értékszám-szabályok változatlanul hagyják az értékszámot; barátságos, egyéni játékidejű vagy megszakított játszmában egyik sincs), `TimeControl` (másodpercben), `Termination` (PGN-szabvány szerinti érték: `normal`; `time forfeit`, a zászló leesése, akkor is, ha döntetlennel végződik; `abandoned`; `rules infraction`, második szabálytalan lépés vagy a fair play megsértése miatti játszmavesztés; `unterminated`, megszakított játszma), `PlyCount`, `ScacelithGameId` (a decimális azonosító).

        Minden lépés mellett ott áll a `{[%clk h:mm:ss.f] [%emt h:mm:ss.f]}`: a lépő játékos órája a lépés után és a lépésre elszámolt idő, tizedmásodpercben (csonkolva). Egy érték kimarad, ha a rekordban nincs meg. Az utolsó lépés után a játszma végének oka következik szavakkal, majd az eredmény.

        **Korlát:** `public_read` (tokennel játékosonként, token nélkül kliensenként).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
      responses:
        '200':
          description: A PGN-fájl.
          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: 'A PGN-szöveg, UTF-8 kódolással (`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`: nem legfeljebb 16 jegyű pozitív egész szám (2^53 alatt); `invalid_request`: érvénytelen URL-kódolás.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: a kérés tokent tartalmazott, de az érvénytelen.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: nincs ilyen játszma.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: a `public_read` korlát vagy a fiókkeret.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`internal_error`: többek között akkor, ha a tárolt lépések újrajátszása nem vezet el a tárolt befejezéshez (a szerver naplózza).'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (az adatbázis zárolva maradt; `retryAfter: 1` a törzsben, `Retry-After` fejléc nélkül), `server_busy` (a munkamenet keresésekor az adatbázis zárolva volt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /games/{id}/gif:
    get:
      operationId: getGameGif
      tags:
        - gifs
      summary: A szerver egy játszmájának letöltése animált GIF-ként
      description: |-
        A játszma animált GIF-ként. A nevek és az értékszámok a játszmarekordból származnak (az értékszámok a kezdéskor, törölt fiók `deleted#<id>` néven), és ugyanígy az eredmény és a befejezés módja is (`Resignation`, `Loss on time`…).

        Az ellenőrzések sorrendje: `gif_disabled`, a képbeállítások (`size`, `orientation`, `delay`, `coords`), a játszma azonosítója, a játszma, a hossza.

        **Munkamenet szükséges:** a kvóták fiókonként számítanak. **Korlátok:** `gif` minden kérésnél; `gif_user_min`, `gif_user_hour`, `gif_ip_min` és `gif_ip_hour` csak akkor, ha a GIF-et el kell készíteni (lásd a szakasz leírását). **Időkorlát:** `GIF_QUEUE_TIMEOUT_MS` + `GIF_RENDER_TIMEOUT_MS` + 5 másodperc (alapértelmezés szerint 45 másodperc).
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
        - name: size
          in: query
          required: false
          description: 'A kép mérete: `small` (32 px-es mezők), `medium` (48 px) vagy `large` (72 px).'
          schema:
            type: string
            enum:
              - small
              - medium
              - large
            default: medium
        - name: orientation
          in: query
          required: false
          description: A tábla alján lévő fél.
          schema:
            type: string
            enum:
              - white
              - black
            default: white
        - name: delay
          in: query
          required: false
          description: Ezredmásodperc lépésenként (1–6 decimális számjegy).
          schema:
            type: integer
            minimum: 100
            maximum: 3000
            default: 500
        - name: coords
          in: query
          required: false
          description: A vonalak betűi és a sorok számai megjelennek-e a tábla körül (`1`) vagy sem (`0`).
          schema:
            type: string
            enum:
              - '1'
              - '0'
            default: '1'
      responses:
        '200':
          $ref: '#/components/responses/GifFile'
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_option` `field` mezővel (`size`, `orientation`, `delay` vagy `coords`): a megengedetteken kívüli érték. `invalid_game_id`. `invalid_request`: érvénytelen URL-kódolás.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: nincs ilyen játszma. `gif_disabled`: a szerver kikapcsolta a GIF-eket (`GIF_ENABLED=false`; a `gif` token visszajár).'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '422':
          $ref: '#/components/responses/UnprocessableContent'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: a `gif` korlát, egy renderelési korlát vagy a fiókkeret.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`render_failed`: a GIF-et nem sikerült elkészíteni (a szerver naplózza az okát). `internal_error`.'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` `retryAfter` értékkel (3–10 másodperc) és `Retry-After` fejléccel: a renderelési sor megtelt, vagy a GIF `GIF_QUEUE_TIMEOUT_MS` (10 másodperc) ideig várt egy szabad szálra; a kérés által elvett minden korláttoken visszajár. `busy`: az adatbázis zárolva maradt (`retryAfter: 1` csak a törzsben). `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /gif:
    post:
      operationId: renderPgnGif
      tags:
        - gifs
      summary: Animált GIF készítése bármely PGN-ként elküldött játszmáról
      description: |-
        Ugyanaz a kép, mint a `GET /games/{id}/gif` esetén, bármely PGN-szövegként elküldött játszmáról: a játék által mentett játszmáról, egy másik oldal exportjáról, kézzel begépelt játszmáról. A szerver csak a szöveg első játszmáját használja.

        - A PGN-beolvasó azt fogadja el, amit a játék saját beolvasója: minden PGN-t, amelyet ez a szerver ír, és más oldalak szokásos exportjait (a megjegyzéseket, a változatokat, a NAG-okat és az óra-annotációkat kihagyja; a lépésszámokat és a SAN-t engedékenyen olvassa; a `FEN` címke adja a kezdőállást, hacsak a `SetUp` értéke nem `"0"`).
        - A nevek és az értékszámok a `White`, `Black`, `WhiteElo` és `BlackElo` címkékből származnak. Az ékezetes betűk elveszítik az ékezetüket, a nyomtatható ASCII-n kívüli egyéb karakterekből `?` lesz, a hosszú neveket pedig a szerver levágja (48 karakter). Az eredmény a `Result` címkéből származik, ennek hiányában a lépésszöveg végéről. A `Termination` címke megjelenik, kivéve ha `normal`: ilyenkor a végállás magáért beszél (matt, patt).
        - Ez a végpont maga ellenőrzi a törzsét: ismeretlen mezőre, illetve hiányzó vagy nem karakterlánc típusú `pgn` mezőre a válasz 400 `invalid_request` `field` mezővel; hibás beállításra a válasz 400 `invalid_option` (a `delayMs` csak JSON-szám, a `coords` csak logikai érték lehet; a `null` nem elfogadott).

        **Munkamenet szükséges:** a kvóták fiókonként számítanak. **Korlátok:** mint a `GET /games/{id}/gif` esetén. **Törzsméret-korlát:** 135 168 bájt, a `HTTP_BODY_LIMIT` értékétől függetlenül (a PGN JSON-karakterláncként, az escape-szekvenciákkal együtt). **Időkorlát:** alapértelmezés szerint 45 másodperc.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GifRequest'
            examples:
              short:
                summary: Rövid, begépelt játszma, koordináták nélkül
                value:
                  pgn: 1. f3 e5 2. g4 Qh4# 0-1
                  coords: false
              options:
                summary: PGN-fájl minden beállítással
                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` mezővel (ismeretlen mező, vagy a `pgn` hiányzik, illetve nem karakterlánc; `field` nélkül, ha a törzs nem objektum); `invalid_json`; `invalid_option` `field` mezővel (`size`, `orientation`, `delayMs` vagy `coords`); `invalid_pgn` `line` és `column` mezővel (1-től számozva, az oszlopok karakterben) és a beolvasó `message` üzenetével: szabálytalan vagy többértelmű lépés, hibás címke, ismeretlen sakkvariáns, 65 536 bájtnál több.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`gif_disabled`: a szerver kikapcsolta a GIF-eket (`GIF_ENABLED=false`; a `gif` token visszajár).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
          description: '`payload_too_large`: 135 168 bájtnál nagyobb törzs.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '422':
          $ref: '#/components/responses/UnprocessableContent'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: a `gif` korlát, egy renderelési korlát vagy a fiókkeret.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`render_failed`: a GIF-et nem sikerült elkészíteni (a szerver naplózza az okát). `internal_error`.'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` `retryAfter` értékkel (3–10 másodperc) és `Retry-After` fejléccel: a renderelési sor megtelt, vagy a GIF túl sokáig várt egy szabad szálra; a kérés által elvett minden korláttoken visszajár. `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /players/{username}:
    get:
      operationId: getPlayer
      tags:
        - players
      summary: Egy játékos nyilvános profiljának lekérése
      description: |-
        Egy játékos nyilvános profilja: értékszámok a hivatalos kategóriákban (a szerver sorrendjében) és a játszmák száma. A `games.total` minden tárolt játszmát számol, a barátságos és a megszakított játszmákat is; a `games.rated`, `wins`, `draws` és `losses` az értékszám-rekordokból összegzett értékek, tehát csak az értékelt játszmákat számolják. A válasz tokennel és token nélkül is ugyanaz.

        **Korlát:** `public_read` (tokennel játékosonként, token nélkül kliensenként).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Username'
      responses:
        '200':
          description: A profil.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlayerProfile'
              example:
                username: alice
                createdAt: 1790882839743
                ratings:
                  - category: '3+2'
                    rating: 1510
                    provisional: true
                    games: 2
                    wins: 1
                    draws: 1
                    losses: 0
                    peak: 1510
                games:
                  total: 3
                  rated: 2
                  wins: 1
                  draws: 1
                  losses: 0
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_username`: nem 2–24 karakterből áll a következő készletből: `[A-Za-z0-9_.-]`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: a kérés tokent tartalmazott, de az érvénytelen.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: nincs ilyen játékos, vagy a fiókot törölték.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: a `public_read` korlát vagy a fiókkeret.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (az adatbázis zárolva maradt; `retryAfter: 1` a törzsben, `Retry-After` fejléc nélkül), `server_busy` (a munkamenet keresésekor az adatbázis zárolva volt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /players/{username}/games:
    get:
      operationId: listPlayerGames
      tags:
        - players
      summary: Egy játékos legutóbbi játszmáinak listázása
      description: |-
        A játékos legutóbbi játszmái, a legújabbal kezdve, a korábbi játszmák listájához hasonlóan lapozva, de szűrők és összesített darabszám nélkül. A `color` ennek a játékosnak a színe. A `next` mindig az utolsó játszma azonosítója, ha az oldal megtelt, így a következő oldal üres is lehet. A szerver először a játékost keresi meg: ismeretlen játékosra a válasz 404, bármit tartalmaz is a lekérdezés.

        **Korlát:** `public_read` (tokennel játékosonként, token nélkül kliensenként).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Username'
        - $ref: '#/components/parameters/BeforeQuery'
        - $ref: '#/components/parameters/LimitQuery'
      responses:
        '200':
          description: A játékos játszmáinak egy oldala.
          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` (a `before` nem játszmaazonosító), `invalid_limit` (az utóbbi kettő `field` nélkül).'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: a kérés tokent tartalmazott, de az érvénytelen.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: nincs ilyen játékos, vagy a fiókot törölték.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: a `public_read` korlát vagy a fiókkeret.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (az adatbázis zárolva maradt; `retryAfter: 1` a törzsben, `Retry-After` fejléc nélkül), `server_busy` (a munkamenet keresésekor az adatbázis zárolva volt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /leaderboard:
    get:
      operationId: getLeaderboard
      tags:
        - leaderboard
      summary: Egy kategória legjobb játékosainak lekérése
      description: |-
        Egy hivatalos kategória 100 legjobb értékszám-rekordja, legalább `minGames` (`PROVISIONAL_GAMES`) beszámított játszmával, a törölt fiókok és a bizonyított csalók nélkül. A szerver minden ranglistát legfeljebb 10 másodpercenként számol újra; az `updatedAt` mutatja, mikor. Egy kategóriánál egyszerre csak egy számítás fut: közben a többi kérés az előző listát kapja, vagy megvárja a számítást, ha még nincs lista.

        **Korlátok:** csak a címenkénti réteg.
      security: []
      parameters:
        - name: category
          in: query
          required: true
          description: Hivatalos kategória azonosítója (`3+2` vagy `3%2B2`).
          schema:
            type: string
            pattern: '^\s*[0-9]+[+ ][0-9]+\s*$'
          example: '3+2'
        - name: limit
          in: query
          required: false
          description: Hány játékos legyen a listán, 1–100. A legfeljebb 3 jegyű nagyobb szám 100-nak számít; az üres érték hiányzónak számít.
          schema:
            type: integer
            minimum: 1
            maximum: 999
            default: 100
      responses:
        '200':
          description: A ranglista.
          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` (hiányzik, vagy nem hivatalos kategória), `invalid_limit`.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: a címenkénti réteg.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (az adatbázis zárolva maradt; `retryAfter: 1` a törzsben, `Retry-After` fejléc nélkül), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /reports:
    post:
      operationId: reportPlayer
      tags:
        - reports
      summary: Egy nemrégi játszma ellenfelének jelentése
      description: |-
        Jelenti a játékos egy saját, az elmúlt 7 napban befejeződött játszmájának ellenfelét. Egy jelentés önmagában soha nem változtat meg értékszámot, szankciót vagy integritási szintet. Megemeli a moderátorok által látott felülvizsgálati prioritást, és – az `abuse` kivételével – kéri a játszma gépi elemzését.

        Ugyanannak az ellenfélnek ugyanarra a játszmára vonatkozó jelentése ugyanazt a választ kapja, és nem változtat semmin; a válasz soha nem árul el semmit a jelentett fiókról. A `GET /games/{id}` előre megmondja a játszma játékosainak, hogy a szerver elfogadna-e egy jelentést (`reportable`).

        Ez a végpont maga ellenőrzi a törzsét, a következő sorrendben: `gameId`, `reported`, `category`, `comment`; hiba esetén a válasz 400 `invalid_request` `field` nélkül. Az ismeretlen mezőket figyelmen kívül hagyja.

        **Korlátok:** `reports` (játékosonként), majd játékosonként 24 óra alatt `REPORTS_PER_DAY` (5) jelentés (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: A jelentés beérkezett (vagy már korábban beérkezett).
          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` nélkül), `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`report_not_allowed`: a jelentett játékos nem a jelentő ellenfele egy olyan játszmában, amely az elmúlt 7 napban fejeződött be.'
        '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` fejléccel): `REPORTS_PER_DAY` jelentés az elmúlt 24 órában. `rate_limited`: a `reports` korlát vagy a fiókkeret.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (a munkamenet keresésekor az adatbázis zárolva volt), `timeout`. Továbbá `server_busy` 2–5 közötti `retryAfter` értékkel, ha a szerver erősen le van maradva (lásd a bevezetőt): a szerver nem hajtotta végre a kérést.'
  /verify-email:
    servers:
      - url: https://caissa.scacelith.com
        description: A hivatalos szerver (az oldalak a gyökérben vannak, nem `/api/v1` alatt).
      - url: https://{host}:{port}
        description: Bármely Scacelith-szerver (az oldalak a gyökérben vannak, nem `/api/v1` alatt).
        variables:
          host:
            default: caissa.scacelith.com
            description: A szerver nyilvános állomásneve (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: A nyilvános API-port (`PUBLIC_API_PORT`, ennek hiányában `API_PORT`).
    get:
      operationId: showVerifyEmailPage
      tags:
        - pages
      summary: Az e-mail-megerősítő oldal megjelenítése
      description: |-
        Egy e-mail-megerősítő link oldala (regisztráció, vagy újraküldött megerősítő e-mail). Csak egy „Confirm my e-mail address” (Az e-mail-címem megerősítése) gombot jelenít meg, hogy egy, a linket megnyitó levélszkenner ne használja el a linket; a gomb a `POST /verify-email` címre küldi el az űrlapot.

        **Korlát:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Az oldal a megerősítő gombjával.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: A link érvénytelen vagy lejárt (HTML-oldal).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: A `page` korlát (HTML-oldal), vagy a címenkénti réteg (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitVerifyEmailPage
      tags:
        - pages
      summary: Az e-mail-cím megerősítése
      description: |-
        A megerősítő oldal űrlapja. Megerősíti a címet; új regisztrációnál ekkor jön létre a fiók, és a játékos bejelentkezhet.

        **Korlát:** `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: A cím megerősítve (új regisztrációnál a fiók létrehozva).
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: A link érvénytelen, már felhasznált vagy lejárt, vagy az űrlap érvénytelen (HTML-oldal).
        '408':
          $ref: '#/components/responses/HtmlRequestTimeout'
        '409':
          $ref: '#/components/responses/HtmlConflict'
          description: Olyan új regisztráció linkje, amelynek a felhasználónevét vagy a címét időközben egy másik fiók foglalta el; nem jön létre fiók (HTML-oldal).
        '413':
          $ref: '#/components/responses/HtmlPayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/HtmlUnsupportedMediaType'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Az `auth` korlát (HTML-oldal), vagy a címenkénti réteg (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Az adatbázis zárolva maradt (`Retry-After: 1`), vagy a szerver erősen le van maradva (`Retry-After` 2–5, lásd a bevezetőt): semmi sem változott, és a link továbbra is működik. Továbbá a kezelő időtúllépése.'
  /reset-password:
    servers:
      - url: https://caissa.scacelith.com
        description: A hivatalos szerver (az oldalak a gyökérben vannak, nem `/api/v1` alatt).
      - url: https://{host}:{port}
        description: Bármely Scacelith-szerver (az oldalak a gyökérben vannak, nem `/api/v1` alatt).
        variables:
          host:
            default: caissa.scacelith.com
            description: A szerver nyilvános állomásneve (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: A nyilvános API-port (`PUBLIC_API_PORT`, ennek hiányában `API_PORT`).
    get:
      operationId: showResetPasswordPage
      tags:
        - pages
      summary: A jelszó-visszaállító űrlap megjelenítése
      description: |-
        Egy jelszó-visszaállító link oldala: az új jelszó űrlapja (a jelszó kétszer). Az űrlap a `POST /reset-password` címre küld.

        **Korlát:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Az új jelszó űrlapja.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: A link érvénytelen (akkor is, ha olyan címre küldték, amely már nem a fióké).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: A `page` korlát (HTML-oldal), vagy a címenkénti réteg (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitResetPasswordPage
      tags:
        - pages
      summary: Új jelszó beállítása a visszaállító űrlapról
      description: |-
        A visszaállító oldal űrlapja; ugyanazt teszi, mint a `POST /auth/password/reset`, egyetlen tranzakcióban: minden eszköz kijelentkezik, a függőben lévő e-mail-cím-módosítás törlődik, a többi visszaállító link érvényét veszti, a cím megerősítettnek számít, és a tulajdonos e-mailt kap.

        A szerver először a linket ellenőrzi, majd azt, hogy a két jelszó megegyezik-e, végül a jelszószabályokat. Ha a szerver foglalt, az űrlap `Retry-After` fejléccel jön vissza, és a link érvényes marad.

        **Korlátok:** `auth`, majd `auth_reset` (közös a `POST /auth/password/reset` végponttal).
      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: A jelszó megváltozott, és minden eszköz kijelentkezett.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Ismét az űrlap a hibával (a jelszavak eltérnek, gyenge jelszó), a link érvénytelen, vagy az űrlap érvénytelen (HTML-oldal).
        '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: Az `auth` vagy az `auth_reset` korlát (HTML-oldal), ismét az űrlap `Retry-After` fejléccel, ha ennek a kliensnek túl sok jelszóhashelése várakozik (a link érvényes marad), vagy a címenkénti réteg (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Ismét az űrlap `Retry-After` fejléccel, ha a szerver foglalt (a jelszóhashelési sor, vagy az adatbázis zárolva maradt); a link érvényes marad. Ha a szerver erősen le van maradva (lásd a bevezetőt), ehelyett egy „Server error” (Szerverhiba) oldal `Retry-After` (2–5) fejléccel: semmi sem futott le, és a link érvényes marad. Továbbá a kezelő időtúllépése.'
  /confirm-email-change:
    servers:
      - url: https://caissa.scacelith.com
        description: A hivatalos szerver (az oldalak a gyökérben vannak, nem `/api/v1` alatt).
      - url: https://{host}:{port}
        description: Bármely Scacelith-szerver (az oldalak a gyökérben vannak, nem `/api/v1` alatt).
        variables:
          host:
            default: caissa.scacelith.com
            description: A szerver nyilvános állomásneve (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: A nyilvános API-port (`PUBLIC_API_PORT`, ennek hiányában `API_PORT`).
    get:
      operationId: showConfirmEmailChangePage
      tags:
        - pages
      summary: Az e-mail-cím-módosítás megerősítő oldalának megjelenítése
      description: |-
        Egy e-mail-cím-módosító link oldala: megjeleníti az új címet és a fiók nevét, egy „Use this e-mail address” (Ennek az e-mail-címnek a használata) gombbal, amely a `POST /confirm-email-change` címre küldi az űrlapot.

        **Korlát:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Az oldal az új címmel és a megerősítő gombjával.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: A link érvénytelen vagy lejárt (akkor is, ha a fiók címe a kérés óta megváltozott).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: A `page` korlát (HTML-oldal), vagy a címenkénti réteg (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitConfirmEmailChangePage
      tags:
        - pages
      summary: Az új e-mail-cím megerősítése
      description: |-
        Az e-mail-cím-módosító oldal űrlapja. A cím megváltozik és megerősítettnek számít; az eszközök bejelentkezve maradnak; a korábban elküldött linkek (megerősítés, jelszó-visszaállítás, egyéb módosítások) érvényüket vesztik; a korábbi cím értesítést kap, amelyben az új cím maszkolva szerepel.

        **Korlát:** `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: A cím megváltozott.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: A link érvénytelen, már felhasznált vagy lejárt, vagy az űrlap érvénytelen (HTML-oldal).
        '408':
          $ref: '#/components/responses/HtmlRequestTimeout'
        '409':
          $ref: '#/components/responses/HtmlConflict'
          description: Időközben egy másik fiók foglalta el a címet (HTML-oldal).
        '413':
          $ref: '#/components/responses/HtmlPayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/HtmlUnsupportedMediaType'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Az `auth` korlát (HTML-oldal), vagy a címenkénti réteg (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Az adatbázis zárolva maradt (`Retry-After: 1`), vagy a szerver erősen le van maradva (`Retry-After` 2–5, lásd a bevezetőt): semmi sem változott, és a link továbbra is működik. Továbbá a kezelő időtúllépése.'
  /healthz:
    servers:
      - url: https://caissa.scacelith.com
        description: A hivatalos szerver, a gyökérben.
      - url: https://caissa.scacelith.com/api/v1
        description: A hivatalos szerver, `/api/v1` alatt.
      - url: https://{host}:{port}
        description: Bármely Scacelith-szerver, a gyökérben.
        variables:
          host:
            default: caissa.scacelith.com
            description: A szerver nyilvános állomásneve (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: A nyilvános API-port (`PUBLIC_API_PORT`, ennek hiányában `API_PORT`).
      - url: https://{host}:{port}/api/v1
        description: Bármely Scacelith-szerver, `/api/v1` alatt.
        variables:
          host:
            default: caissa.scacelith.com
            description: A szerver nyilvános állomásneve (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: A nyilvános API-port (`PUBLIC_API_PORT`, ennek hiányában `API_PORT`).
    get:
      operationId: getLiveness
      tags:
        - health
      summary: Annak ellenőrzése, hogy a folyamat fut-e
      description: |-
        200 `ok` választ ad, amíg a folyamat fut. A `HEAD` is működik. Minden más metódusra a válasz 405 `method_not_allowed` `Allow: GET, HEAD` fejléccel (az `OPTIONS` metódusra is).

        **Korlátok:** csak a címenkénti réteg.
      security: []
      responses:
        '200':
          description: A folyamat fut.
          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`: a címenkénti réteg.'
  /readyz:
    servers:
      - url: https://caissa.scacelith.com
        description: A hivatalos szerver, a gyökérben.
      - url: https://caissa.scacelith.com/api/v1
        description: A hivatalos szerver, `/api/v1` alatt.
      - url: https://{host}:{port}
        description: Bármely Scacelith-szerver, a gyökérben.
        variables:
          host:
            default: caissa.scacelith.com
            description: A szerver nyilvános állomásneve (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: A nyilvános API-port (`PUBLIC_API_PORT`, ennek hiányában `API_PORT`).
      - url: https://{host}:{port}/api/v1
        description: Bármely Scacelith-szerver, `/api/v1` alatt.
        variables:
          host:
            default: caissa.scacelith.com
            description: A szerver nyilvános állomásneve (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: A nyilvános API-port (`PUBLIC_API_PORT`, ennek hiányában `API_PORT`).
    get:
      operationId: getReadiness
      tags:
        - health
      summary: Annak ellenőrzése, hogy a szerver fogad-e játékosokat
      description: |-
        200 `ready` választ ad, ha a szerver fogad játékosokat, és 503 `not_ready` választ, amíg indul vagy leáll. A `HEAD` is működik. Minden más metódusra a válasz 405 `method_not_allowed` `Allow: GET, HEAD` fejléccel (az `OPTIONS` metódusra is).

        **Korlátok:** csak a címenkénti réteg.
      security: []
      responses:
        '200':
          description: A szerver fogad játékosokat.
          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`: a címenkénti réteg.'
        '503':
          description: A szerver indul vagy leáll.
          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: |-
        Egy bejelentkezési válaszból kapott munkamenet-token (`sct_`, majd 43 base64url karakter) az `Authorization` fejlécben: `Authorization: Bearer <token>`. Az előtag pontosan `Bearer` (kis- és nagybetűérzékeny) és egy szóköz; az ilyen előtag nélküli értékre, illetve az 1–512 nyomtatható ASCII-karakterből nem álló tokenre a válasz 401 `invalid_token`, mint bármely más érvénytelen tokenre. Az üres fejléc hiányzó fejlécnek számít.
  parameters:
    GameId:
      name: id
      in: path
      required: true
      description: 'A játszma azonosítója: legfeljebb 16 jegyű, kezdő nullák nélküli pozitív decimális egész szám, 2^53 alatt.'
      schema:
        type: integer
        format: int64
        minimum: 1
        maximum: 9007199254740991
      example: 4100000000001
    SessionId:
      name: id
      in: path
      required: true
      description: A munkamenet azonosítója, ahogy a `GET /auth/sessions` megadja, kezdő nullák nélkül írva (a `03` nem a 3-as munkamenet).
      schema:
        type: integer
        format: int64
        minimum: 1
      example: 3
    Username:
      name: username
      in: path
      required: true
      description: 'A játékos felhasználóneve, kis- és nagybetűtől függetlenül. A szerver bármely olyan nevet elfogad, amely 2–24 karakterből áll a következő készletből: `[A-Za-z0-9_.-]` (ez a valaha megengedett legtágabb szabály).'
      schema:
        type: string
        pattern: '^[A-Za-z0-9_.-]{2,24}$'
      example: alice
    BeforeQuery:
      name: before
      in: query
      required: false
      description: Játszmaazonosító; csak a régebbi játszmák jelennek meg a listában. Adja meg az előző oldal `next` értékét. Az üres érték hiányzónak számít.
      schema:
        type: integer
        format: int64
        minimum: 1
        maximum: 9007199254740991
      example: 4100000000002
    LimitQuery:
      name: limit
      in: query
      required: false
      description: Az oldalméret, 1–50. A legfeljebb 3 jegyű nagyobb szám 50-nek számít; az üres érték hiányzónak számít.
      schema:
        type: integer
        minimum: 1
        maximum: 999
        default: 20
    LinkTokenQuery:
      name: token
      in: query
      required: false
      description: Az e-mailben küldött link tokenje (43 base64url karakter). Hiányzó vagy hibás token esetén a „link invalid or expired” (a link érvénytelen vagy lejárt) oldal jelenik meg.
      schema:
        type: string
        pattern: '^[A-Za-z0-9_-]{43}$'
      example: asfioO4P0r_Zn5DxWzcQuF6Drgxu7wXGjCyS-UFbTaw
  headers:
    CacheControl:
      description: A szerver minden válasza tiltja a gyorsítótárazást.
      schema:
        type: string
        const: no-store
    RetryAfter:
      description: Az újrapróbálkozás előtt kivárandó másodpercek; ugyanaz az érték, mint a törzs `retryAfter` mezője.
      schema:
        type: integer
        minimum: 1
      example: 30
    WwwAuthenticate:
      description: A Bearer-kihívás, érvénytelen tokennél `error="invalid_token"` paraméterrel.
      schema:
        type: string
        enum:
          - Bearer realm="scacelith"
          - Bearer realm="scacelith", error="invalid_token"
    ConnectionClose:
      description: A szerver e válasz után lezárja a kapcsolatot.
      schema:
        type: string
        const: close
  responses:
    BadRequest:
      description: '`invalid_request` (`field` mezővel, ha a törzs egy mezője hibás) vagy `invalid_json`.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalidRequest:
              summary: Egy mező nem felel meg a sémának
              value:
                error: invalid_request
                message: '"password" is required'
                field: password
            invalidJson:
              summary: A törzs nem JSON
              value:
                error: invalid_json
                message: The body is not valid JSON.
            weakPassword:
              summary: Gyenge jelszó
              value:
                error: weak_password
                message: The password must have at least 10 characters.
                reason: too_short
            invalidPgn:
              summary: Beolvashatatlan PGN
              value:
                error: invalid_pgn
                message: illegal move 'Ke3'
                line: 1
                column: 13
    Unauthorized:
      description: '`unauthorized` (nincs `Authorization` fejléc) vagy `invalid_token` (a token hibás formátumú, lejárt, visszavont, vagy törölt fiókhoz tartozik).'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        WWW-Authenticate:
          $ref: '#/components/headers/WwwAuthenticate'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            unauthorized:
              summary: Nincs munkamenet
              value:
                error: unauthorized
                message: Log in first.
            invalidToken:
              summary: Érvénytelen token
              value:
                error: invalid_token
                message: The session is invalid or has expired; log in again.
    Forbidden:
      description: A szerver elutasítja a kérést.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalidPassword:
              summary: Hibás jelszó újrahitelesítéskor
              value:
                error: invalid_password
                message: Wrong password.
            banned:
              summary: Kitiltott fiók
              value:
                error: banned
                message: This account is banned.
                until: 1791487639708
    NotFound:
      description: '`not_found`, vagy olyan funkció, amelyet ez a szerver kikapcsolt.'
      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`: a törzs nem érkezett meg 10 másodpercen belül. A szerver lezárja a kapcsolatot.'
      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: A kérés ütközik a szerver állapotával.
      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`: a Google-bejelentkezési kísérlet vagy jegy ismeretlen, már felhasznált vagy lejárt; kezdje újra a bejelentkezést.'
      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`: a törzs meghaladja a `HTTP_BODY_LIMIT` értékét (alapértelmezés szerint 16 384 bájt). A szerver lezárja a kapcsolatot.'
      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`: a kérés célja meghaladja a 4096 karaktert.'
      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`: a törzs nem `application/json`, vagy a karakterkészlete nem 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`: a játszma több mint `GIF_MAX_PLIES` (600) féllépésből áll.'
      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`: oldja meg a `pow` kihívást, és küldje el újra ugyanazt a kérést `pow: { challenge, nonce }` mezővel. A `reason` megadja az okát: `required`, `malformed`, `signature`, `endpoint`, `network`, `expired`, `bits`, `work` vagy `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` (egy kéréskorlát, a fiókkeret vagy a jelszóhashelési sor) vagy `too_many_attempts` (a fiók korlátozása), `retryAfter` mezővel és `Retry-After` fejléccel.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            rateLimited:
              summary: Kéréskorlát
              value:
                error: rate_limited
                message: Too many requests; try again later.
                retryAfter: 30
            tooManyAttempts:
              summary: Túl sok sikertelen próbálkozás ezen a fiókon
              value:
                error: too_many_attempts
                message: Too many attempts; wait before trying again.
                retryAfter: 4
    InternalError:
      description: '`internal_error`: váratlan hiba. A szerver naplózza.'
      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`: hibás `state` vagy `iss`, vagy a Google elutasította a kódot, vagy olyan ID-tokent küldött, amelynek az ellenőrzése sikertelen (ide tartozik a másik kliens – a játék vagy a webhely – kódja vagy ID-tokenje is). Soha nem tartalmazza a Google szövegét.'
      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` vagy `timeout`: próbálja újra a `retryAfter` által megadott idő elteltével, ha meg van adva.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            serverBusy:
              summary: A szerver foglalt
              value:
                error: server_busy
                message: The server is busy; try again in a few seconds.
                retryAfter: 9
            busy:
              summary: Az adatbázis zárolva maradt (olvasás)
              value:
                error: busy
                message: Try again shortly.
                retryAfter: 1
            timeout:
              summary: A szerver túl sokáig dolgozott
              value:
                error: timeout
                message: The server took too long to answer; try again.
    GifFile:
      description: Az animált GIF, mellékletként.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Content-Disposition:
          description: '`attachment; filename="scacelith-<id>.gif"` a szerver egy játszmájánál, `attachment; filename="scacelith-game.gif"` a `POST /gif` végponttal küldött PGN-nél.'
          schema:
            type: string
            pattern: '^attachment; filename="scacelith-([1-9][0-9]{0,15}|game)\.gif"$'
          example: attachment; filename="scacelith-4100000000001.gif"
        Content-Length:
          description: A fájl mérete bájtban.
          schema:
            type: integer
            minimum: 1
      content:
        image/gif:
          schema:
            type: string
            format: binary
    HtmlPage:
      description: HTML-oldal.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlBadRequest:
      description: Az elutasítást magyarázó HTML-oldal.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlRequestTimeout:
      description: Az űrlap nem érkezett meg 10 másodpercen belül (HTML-oldal). A szerver lezárja a kapcsolatot.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Connection:
          $ref: '#/components/headers/ConnectionClose'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlConflict:
      description: Az ütközést magyarázó HTML-oldal.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlPayloadTooLarge:
      description: Az űrlap meghaladja a `HTTP_BODY_LIMIT` értékét (HTML-oldal). A szerver lezárja a kapcsolatot.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Connection:
          $ref: '#/components/headers/ConnectionClose'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlUnsupportedMediaType:
      description: A törzs sem űrlap (`application/x-www-form-urlencoded`), sem JSON, vagy a karakterkészlete nem UTF-8 (HTML-oldal).
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlTooManyRequests:
      description: Kéréskorlát (HTML-oldal), `Retry-After` fejléccel.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlInternalError:
      description: Váratlan hiba („Server error” – Szerverhiba – című HTML-oldal). A szerver naplózza.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlServiceUnavailable:
      description: A szerver foglalt (az adatbázis zárolva maradt, vagy a szerver erősen le van maradva; `Retry-After` fejléccel), vagy túl sokáig dolgozott („Server error” – Szerverhiba – című HTML-oldal).
      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: A JSON API minden hibaválaszának egységes formája.
      required:
        - error
        - message
      properties:
        error:
          type: string
          pattern: '^[a-z][a-z0-9_]*$'
          description: A `snake_case` formátumú hibakód. A kliens ez alapján választja ki, mit jelenítsen meg.
          examples:
            - rate_limited
        message:
          type: string
          description: Egy angol mondat, naplókhoz és tartalékként.
        retryAfter:
          type: integer
          minimum: 1
          description: Az újrapróbálkozás előtt kivárandó másodpercek; csak az idővel megszűnő elutasításoknál van jelen. A válasz ilyenkor ugyanezzel az értékkel `Retry-After` fejlécet is tartalmaz, kivéve a 503 `busy` választ a korábbi játszmák, a játszmák, a játékosok és a ranglista lekérdezésekor.
        field:
          type: string
          description: A hibás mező vagy lekérdezési paraméter (`invalid_request`, `invalid_option`, valamint a `GET /account/games` `invalid_cursor`, `invalid_limit` és `invalid_filter` hibája). A beágyazott mező neve pontokkal tagolt (`pow.nonce`).
        reason:
          type: string
          description: 'Az ok: a szabály, amelyet egy `weak_password` megsért, vagy hogy miért kért vagy utasított el a szerver munkabizonyítékot (`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: 'Csak `banned` esetén: a kitiltás vége (Unix-idő, ms), végleges kitiltásnál `null`.'
        line:
          type: integer
          minimum: 1
          description: 'Csak `invalid_pgn` esetén: a hiba sora, 1-től számozva.'
        column:
          type: integer
          minimum: 1
          description: 'Csak `invalid_pgn` esetén: a hiba oszlopa, 1-től számozva, karakterben.'
    PowChallenge:
      type: object
      description: Munkabizonyíték-kihívás (egy 428 `pow_required` válasz `pow` mezője).
      required:
        - challenge
        - bits
        - expiresAt
      properties:
        challenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{16,400}\.[A-Za-z0-9_-]{43}$'
          description: Az aláírt kihívás, változatlanul visszaküldendő.
        bits:
          type: integer
          minimum: 1
          maximum: 26
          description: A kezdő nullabitek száma, amellyel a `SHA-256(challenge + ":" + nonce)` értéknek rendelkeznie kell.
        expiresAt:
          type: integer
          format: int64
          description: A kihívás lejárta (Unix-idő, ms), a kiadása után 2 perccel.
    PowAnswer:
      type: object
      description: Válasz egy munkabizonyíték-kihívásra.
      additionalProperties: false
      required:
        - challenge
        - nonce
      properties:
        challenge:
          type: string
          minLength: 16
          maxLength: 512
          description: A 428-as válasz `challenge` mezője, változatlanul.
        nonce:
          type: string
          pattern: '^[0-9]{1,20}$'
          description: Legfeljebb 20 jegyű decimális karakterlánc, amelyre a `SHA-256(challenge + ":" + nonce)` `bits` darab nullabittel kezdődik.
    ServerInfo:
      type: object
      description: Amire egy kliensnek szüksége van, mielőtt bejelentkezik vagy csatlakozik.
      required:
        - name
        - serverId
        - motd
        - protocol
        - wsPort
        - wsPath
        - registration
        - emailVerification
        - sso
        - mfa
        - pow
        - categories
        - limits
      properties:
        name:
          type: string
          description: A szerver neve (`SERVER_NAME`).
        serverId:
          type:
            - string
            - 'null'
          format: uuid
          description: UUID, amelyet az adatbázis az első indításakor kap. Újraindítások után is ugyanaz marad. `null`, ha az adatbázis nem tudja megadni.
        motd:
          type: string
          description: A nap üzenete (`SERVER_MOTD`), lehet üres is.
        protocol:
          type: object
          description: A WebSocket-protokoll (`PROTOCOL.md`).
          required:
            - min
            - max
            - schema
            - subprotocol
          properties:
            min:
              type: integer
              minimum: 1
              description: A szerver által ismert legrégebbi protokollverzió.
            max:
              type: integer
              minimum: 1
              description: A szerver által ismert legújabb protokollverzió.
            schema:
              type: integer
              format: int64
              minimum: 0
              maximum: 4294967295
              description: 'A protokollséma ujjlenyomata (a kanonikus alakjából számított SHA-256 hash első 4 bájtja, előjel nélküli egész számként: `0xc649140f`, azaz 3326678031 a protokoll 2-es alverziójánál); tájékoztató jellegű, összehasonlításra soha nem használják.'
            subprotocol:
              type: string
              description: A WebSocket-alprotokoll tokenje (`Sec-WebSocket-Protocol`).
        wsPort:
          type: integer
          minimum: 0
          maximum: 65535
          description: A játékosok által használt WebSocket-port (`PUBLIC_WS_PORT`, ennek hiányában `WS_PORT`, annak hiányában `API_PORT`).
        wsPath:
          type: string
          const: /ws
          description: A WebSocket útvonala.
        registration:
          type: string
          enum:
            - open
            - closed
          description: Létrehozhatók-e új fiókok.
        emailVerification:
          type: boolean
          description: Meg kell-e erősíteniük az új fiókoknak a címüket (`REQUIRE_EMAIL_VERIFICATION`).
        sso:
          type: object
          required:
            - google
            - googleWeb
          properties:
            google:
              type: boolean
              description: Elérhető-e Google-bejelentkezés a játék számára.
            googleWeb:
              type: boolean
              description: Elérhető-e Google-bejelentkezés egy webhely számára is (`POST /auth/sso/google/web/start`).
        mfa:
          type: boolean
          const: true
          description: A kétlépcsős azonosítás mindig elérhető.
        pow:
          type: object
          required:
            - register
          properties:
            register:
              type: integer
              minimum: 0
              maximum: 26
              description: A regisztrációhoz szükséges munkabizonyíték-bitek száma (0, ha nincs).
        categories:
          type: array
          description: A hivatalos (értékelt) játékidők (`RATED_CATEGORIES`), a szerver sorrendjében. Minden más játékidő `custom`.
          items:
            $ref: '#/components/schemas/Category'
        limits:
          type: object
          description: Szabályok, amelyeket a kliens egy űrlap elküldése előtt ellenőrizhet.
          required:
            - usernameMin
            - usernameMax
            - usernamePattern
            - passwordMinLength
            - passwordMaxBytes
            - customTimeControls
            - reportsPerDay
            - wsMaxMessageBytes
          properties:
            usernameMin:
              type: integer
              description: A legrövidebb felhasználónév hossza (`USERNAME_MIN`).
            usernameMax:
              type: integer
              description: A leghosszabb felhasználónév hossza (`USERNAME_MAX`).
            usernamePattern:
              type: string
              description: A reguláris kifejezés, amelynek egy új felhasználónévnek meg kell felelnie.
            passwordMinLength:
              type: integer
              description: A legrövidebb jelszó hossza karakterben (`PASSWORD_MIN_LENGTH`).
            passwordMaxBytes:
              type: integer
              description: A leghosszabb jelszó hossza UTF-8 bájtban.
            customTimeControls:
              type: boolean
              description: Használhatnak-e a kihívások és a privát játszmák egyéni játékidőt.
            reportsPerDay:
              type: integer
              description: Hány jelentést tehet egy játékos 24 óránként (`REPORTS_PER_DAY`).
            wsMaxMessageBytes:
              type: integer
              description: A kliens által küldhető legnagyobb WebSocket-üzenet mérete bájtban.
      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: Hivatalos játékidő.
      required:
        - id
        - baseSec
        - incSec
      properties:
        id:
          type: string
          pattern: '^[0-9]+\+[0-9]+$'
          description: 'A kategória azonosítója: percek és többletidő-másodpercek (`3+2`).'
        baseSec:
          type: number
          minimum: 0
          description: Az alapidő másodpercben.
        incSec:
          type: number
          minimum: 0
          description: A lépésenkénti többletidő másodpercben.
    AccountView:
      type: object
      description: A fiók úgy, ahogy a játékosa látja (a bejelentkezési válaszok és a `GET /account/me` `user` mezője).
      required:
        - id
        - username
        - email
        - emailVerified
        - mfaEnabled
        - googleLinked
        - hasPassword
        - acceptChallenges
        - createdAt
        - lastLoginAt
        - pendingEmail
      properties:
        id:
          type: integer
          format: int64
          description: A fiók azonosítója.
        username:
          type: string
          description: A felhasználónév.
        email:
          type: string
          format: email
          description: Az e-mail-cím.
        emailVerified:
          type: boolean
          description: Meg van-e erősítve a cím.
        mfaEnabled:
          type: boolean
          description: Be van-e kapcsolva a kétlépcsős azonosítás.
        googleLinked:
          type: boolean
          description: Van-e kapcsolt Google-fiók.
        hasPassword:
          type: boolean
          description: '`false` a Google-on keresztül létrehozott olyan fióknál, amely még nem állított be jelszót.'
        acceptChallenges:
          type: string
          enum:
            - all
            - none
          description: Fogadja-e a fiók a név szerinti közvetlen kihívásokat (lásd `PUT /account/preferences`).
        createdAt:
          type: integer
          format: int64
          description: A fiók létrehozásának ideje (Unix-idő, ms).
        lastLoginAt:
          type:
            - integer
            - 'null'
          format: int64
          description: Az utolsó bejelentkezés (Unix-idő, ms), vagy `null`.
        pendingEmail:
          type:
            - string
            - 'null'
          format: email
          description: A linkjére váró e-mail-cím-módosítás új címe, vagy `null`.
    SessionAnswer:
      type: object
      description: Új munkamenet.
      required:
        - token
        - expiresAt
        - user
      properties:
        token:
          type: string
          pattern: '^sct_[A-Za-z0-9_-]{43}$'
          description: A munkamenet-token, az `Authorization` fejléchez és a WebSockethez.
        expiresAt:
          type: integer
          format: int64
          description: A munkamenet abszolút lejárata (Unix-idő, ms); az inaktivitási korlát korábban is megszüntetheti.
        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: 'Kétlépcsős azonosítással történő bejelentkezés második lépése; a folytatás: `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: A lépés tokenje, a `POST /auth/login/mfa` számára.
        expiresIn:
          type: integer
          const: 300
          description: A lépés befejezéséig hátralévő másodpercek.
    LoginAnswer:
      description: Munkamenet, vagy kétlépcsős azonosítással történő bejelentkezés második lépése.
      oneOf:
        - $ref: '#/components/schemas/SessionAnswer'
        - $ref: '#/components/schemas/MfaChallenge'
    SsoNeedsUsername:
      type: object
      description: 'Első Google-bejelentkezés; a folytatás: `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: A jegy a `POST /auth/sso/complete` számára, 10 percig érvényes.
        suggestedUsername:
          type: string
          description: A Google-névből vagy a címből képzett felhasználónév, vagy `""`, ha semmi sem megfelelő.
    SsoNeedsPassword:
      type: object
      description: 'Egy jelszóval rendelkező fiók használja a címet; a folytatás: `POST /auth/sso/google/link`. Még semmi sincs összekapcsolva.'
      required:
        - needsPassword
        - linkTicket
        - username
        - expiresIn
      properties:
        needsPassword:
          type: boolean
          const: true
        linkTicket:
          type: string
          pattern: '^sso_[A-Za-z0-9_-]{43}$'
          description: A jegy a `POST /auth/sso/google/link` számára.
        username:
          type: string
          description: Az adott fiók felhasználóneve (csak az látja, aki a Google előtt igazolta a címet).
        expiresIn:
          type: integer
          const: 600
          description: Ennyi másodpercig érvényes a jegy.
    SsoFinishAnswer:
      description: A `POST /auth/sso/google/finish` válasza.
      oneOf:
        - $ref: '#/components/schemas/SessionAnswer'
        - $ref: '#/components/schemas/MfaChallenge'
        - $ref: '#/components/schemas/SsoNeedsUsername'
        - $ref: '#/components/schemas/SsoNeedsPassword'
    SsoStartAnswer:
      type: object
      description: Google-bejelentkezési kísérlet.
      required:
        - attemptId
        - authUrl
        - state
        - expiresIn
      properties:
        attemptId:
          type: string
          pattern: '^sso_[A-Za-z0-9_-]{43}$'
          description: A kísérlet, a `POST /auth/sso/google/finish` számára.
        authUrl:
          type: string
          format: uri
          description: A rendszerböngészőben megnyitandó Google-URL, ellenőrzés után.
        state:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: A `state`, amelyet a Google-nak vissza kell küldenie.
        expiresIn:
          type: integer
          const: 600
          description: Ennyi másodpercig érvényes a kísérlet.
    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: A felhasználónév; ezt követően a szerver szabályai érvényesek (lásd a leírást).
        email:
          type: string
          minLength: 1
          maxLength: 254
          description: Az e-mail-cím. A szerver levágja a kezdő és záró szóközöket, és kisbetűsen tárolja.
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: A jelszó; ezt követően a jelszószabályok érvényesek.
        pow:
          $ref: '#/components/schemas/PowAnswer'
    LoginRequest:
      type: object
      additionalProperties: false
      required:
        - login
        - password
      properties:
        login:
          type: string
          minLength: 1
          maxLength: 254
          description: A felhasználónév vagy az e-mail-cím (bármely szöveg, amelyben van `@`).
        password:
          type: string
          minLength: 1
          maxLength: 1024
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
        pow:
          $ref: '#/components/schemas/PowAnswer'
    ClientLabel:
      type: string
      maxLength: 64
      description: Nem kötelező. A bejelentkezett eszközök listájában jelenik meg (például `Scacelith 1.4 (Windows)`).
    MfaLoginRequest:
      type: object
      additionalProperties: false
      required:
        - mfaToken
      properties:
        mfaToken:
          type: string
          minLength: 1
          maxLength: 64
          description: A bejelentkezési válasz (vagy egy Google-bejelentkezés válaszának) `mfaToken` mezője.
        code:
          type: string
          maxLength: 32
          description: Nem kötelező. 6 jegyű hitelesítő kód vagy helyreállító kód.
        recoveryCode:
          type: string
          maxLength: 32
          description: Nem kötelező. Helyreállító kód. A `code` és a `recoveryCode` közül az egyik szükséges.
    EmailRequest:
      type: object
      additionalProperties: false
      required:
        - email
      properties:
        email:
          type: string
          minLength: 1
          maxLength: 254
          description: Az e-mail-cím.
    PasswordResetRequest:
      type: object
      additionalProperties: false
      required:
        - token
        - newPassword
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: A visszaállító link `token` paramétere.
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Az új jelszó; a regisztráció jelszószabályai érvényesek.
    SsoStartRequest:
      type: object
      additionalProperties: false
      required:
        - codeChallenge
        - redirectPort
      properties:
        codeChallenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: A kliens `codeVerifier` értékének S256 PKCE-kihívása (`BASE64URL(SHA-256(codeVerifier))`).
        redirectPort:
          type: integer
          minimum: 1024
          maximum: 65535
          description: A kliens figyelőjének portja a `127.0.0.1` címen.
    SsoWebStartRequest:
      type: object
      additionalProperties: false
      required:
        - codeChallenge
      properties:
        codeChallenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: Az oldal `codeVerifier` értékének S256 PKCE-kihívása (`BASE64URL(SHA-256(codeVerifier))`).
    SsoFinishRequest:
      type: object
      additionalProperties: false
      required:
        - attemptId
        - codeVerifier
        - state
        - code
      properties:
        attemptId:
          type: string
          minLength: 1
          maxLength: 64
          description: A `POST /auth/sso/google/start` által adott `attemptId`.
        codeVerifier:
          type: string
          pattern: '^[A-Za-z0-9._~-]{43,128}$'
          description: A PKCE-ellenőrző (verifier); SHA-256 hashének egyeznie kell a `start` végpontnak megadott kihívással.
        state:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: A `state`, ahogy a Google visszaküldte.
        code:
          type: string
          minLength: 1
          maxLength: 2048
          pattern: '^[\x21-\x7E]+$'
          description: A `code`, ahogy a Google visszaküldte (nyomtatható ASCII, szóközök nélkül).
        iss:
          type: string
          minLength: 1
          maxLength: 256
          description: Nem kötelező. Az `iss`, ahogy a Google visszaküldte, ha visszaküldte.
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
    SsoLinkRequest:
      type: object
      additionalProperties: false
      required:
        - linkTicket
        - password
      properties:
        linkTicket:
          type: string
          minLength: 1
          maxLength: 64
          description: A `finish` által adott `linkTicket`, 10 percig érvényes.
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: A fiók jelszava.
        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: A `finish` által adott `ssoTicket`, 10 percig érvényes.
        username:
          type: string
          minLength: 1
          maxLength: 64
          description: A felhasználónév; a regisztráció szabályai érvényesek.
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
    SessionList:
      type: object
      required:
        - sessions
      properties:
        sessions:
          type: array
          description: Az aktív munkamenetek, a legutóbb használttal kezdve.
          items:
            $ref: '#/components/schemas/SessionEntry'
    SessionEntry:
      type: object
      description: Aktív munkamenet (bejelentkezett eszköz).
      required:
        - id
        - createdAt
        - lastSeenAt
        - expiresAt
        - clientLabel
        - current
      properties:
        id:
          type: integer
          format: int64
          description: A munkamenet azonosítója, a `DELETE /auth/sessions/{id}` számára.
        createdAt:
          type: integer
          format: int64
          description: A bejelentkezés ideje (Unix-idő, ms).
        lastSeenAt:
          type: integer
          format: int64
          description: Az utolsó használat ideje (Unix-idő, ms), legfeljebb 5 percenként frissül (később, amíg a szerver erősen le van maradva).
        expiresAt:
          type: integer
          format: int64
          description: Az abszolút lejárat (Unix-idő, ms); az inaktivitási korlát a munkamenetet korábban is megszüntetheti.
        clientLabel:
          type:
            - string
            - 'null'
          description: A bejelentkezéskor küldött `clientLabel`, vagy `null`, ha nem küldött ilyet.
        current:
          type: boolean
          description: Ez-e a kérést küldő munkamenet.
    AccountMe:
      type: object
      required:
        - user
        - ratings
        - sanctions
        - ban
      properties:
        user:
          $ref: '#/components/schemas/AccountView'
        ratings:
          type: array
          description: Egy rekord minden olyan kategóriához, amelyben a játékos értékelt játszmákat játszott.
          items:
            $ref: '#/components/schemas/RatingSummary'
        sanctions:
          type: array
          description: Az aktív szankciók.
          items:
            $ref: '#/components/schemas/ActiveSanction'
        ban:
          description: 'Kitiltás alatt `{ until }`, egyébként `null`.'
          oneOf:
            - $ref: '#/components/schemas/Ban'
            - type: 'null'
    RatingSummary:
      type: object
      description: Egy kategória értékszám-rekordja.
      required:
        - category
        - rating
        - games
        - wins
        - draws
        - losses
        - peak
        - provisional
      properties:
        category:
          type: string
          description: A kategória azonosítója.
        rating:
          type: integer
          description: Az értékszám.
        games:
          type: integer
          minimum: 0
          description: A kategóriában játszott játszmák (akár beszámítottak, akár nem).
        wins:
          type: integer
          minimum: 0
        draws:
          type: integer
          minimum: 0
        losses:
          type: integer
          minimum: 0
        peak:
          type: integer
          description: Az elért legmagasabb értékszám.
        provisional:
          type: boolean
          description: '`true`, amíg a rekord még az értékszám nélküli szakaszban van, vagy kevesebb mint `PROVISIONAL_GAMES` beszámított játszmán alapul (a játék `1510?` formában jeleníti meg).'
    ActiveSanction:
      type: object
      required:
        - kind
        - reason
        - startsAt
        - endsAt
      properties:
        kind:
          type: string
          enum:
            - ban
            - mm_block
            - warning
          description: A szankció típusa.
        reason:
          type:
            - string
            - 'null'
          description: A megadott indok, ha van.
        startsAt:
          type: integer
          format: int64
          description: A kezdet (Unix-idő, ms).
        endsAt:
          type:
            - integer
            - 'null'
          format: int64
          description: A vége (Unix-idő, ms), végleges szankciónál `null`.
    Ban:
      type: object
      required:
        - until
      properties:
        until:
          type:
            - integer
            - 'null'
          format: int64
          description: A kitiltás vége (Unix-idő, ms), véglegesnél `null`.
    Preferences:
      type: object
      additionalProperties: false
      required:
        - acceptChallenges
      properties:
        acceptChallenges:
          type: string
          enum:
            - all
            - none
          description: '`all` a név szerinti közvetlen kihívások fogadásához, `none` az elutasításukhoz.'
    PasswordChangeRequest:
      type: object
      additionalProperties: false
      required:
        - currentPassword
        - newPassword
      properties:
        currentPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: A jelenlegi jelszó.
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Az új jelszó; a regisztráció jelszószabályai érvényesek.
    PasswordOnlyRequest:
      type: object
      additionalProperties: false
      required:
        - password
      properties:
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: A fiók jelszava.
    TotpEnableRequest:
      type: object
      additionalProperties: false
      required:
        - code
      properties:
        code:
          type: string
          pattern: '^[0-9]{6}$'
          description: A függőben lévő titkos kulcs egy kódja, pontosan 6 számjegy.
    ReauthCredentials:
      type: object
      additionalProperties: false
      required:
        - password
      properties:
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: A fiók jelszava.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: Nem kötelező; kétlépcsős azonosítás esetén szükséges (vagy a `recoveryCode`). Hitelesítő kód vagy helyreállító kód.
        recoveryCode:
          type: string
          minLength: 1
          maxLength: 32
          description: Nem kötelező. Helyreállító kód (`xxxx-xxxx-xx`; a kis- és nagybetűk, a szóközök és a kötőjelek nem számítanak).
    RecoveryCodesRequest:
      type: object
      additionalProperties: false
      required:
        - password
        - code
      properties:
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: A fiók jelszava.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: Hitelesítő kód (helyreállító kódot a szerver elutasít).
    EmailChangeRequest:
      type: object
      additionalProperties: false
      required:
        - newEmail
        - password
      properties:
        newEmail:
          type: string
          minLength: 1
          maxLength: 254
          description: Az új cím; a kezdő és záró szóközök levágása és kisbetűssé alakítás után úgy ellenőrződik, mint egy regisztrációs cím.
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: A fiók jelszava.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: Nem kötelező; kétlépcsős azonosítás esetén szükséges (vagy a `recoveryCode`). Hitelesítő kód vagy helyreállító kód.
        recoveryCode:
          type: string
          minLength: 1
          maxLength: 32
          description: Nem kötelező. Helyreállító kód.
    TotpSetup:
      type: object
      description: A függőben lévő titkos kulcs a hitelesítő alkalmazáshoz.
      required:
        - secret
        - uri
        - algorithm
        - digits
        - period
      properties:
        secret:
          type: string
          pattern: '^[A-Z2-7]+$'
          description: A titkos kulcs base32 kódolással, az alkalmazásba való begépeléshez.
        uri:
          type: string
          format: uri
          description: Az `otpauth://totp/...` URI, QR-kódként való megjelenítéshez.
        algorithm:
          type: string
          const: SHA1
        digits:
          type: integer
          const: 6
        period:
          type: integer
          const: 30
          description: Egy időlépés hossza másodpercben.
    RecoveryCodeList:
      type: array
      description: A 10 helyreállító kód, amelyek csak most jelennek meg. Mindegyik egyszer használható.
      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: Az egyik fél a játszmában.
      required:
        - name
        - rating
        - ratingAfter
        - ratingDiff
      properties:
        name:
          type: string
          description: A név a játszmarekordban (törölt fióknál `deleted#<id>`).
        rating:
          type:
            - integer
            - 'null'
          description: Az értékszám a kezdéskor, ismeretlen esetén `null`.
        ratingAfter:
          type:
            - integer
            - 'null'
          description: Az értékszám a játszma után. Értékelt játszmánál mindig megvan; `null` csak olyan játszmánál, amely nem számít bele az értékszámokba (barátságos, egyéni, megszakított).
        ratingDiff:
          type:
            - integer
            - 'null'
          description: Az értékszám változása; `0`, ha az értékszám-szabályok változatlanul hagyják az értékszámot (a FIDE nulla pontos szabálya, értékszám nélküli ellenfél, egy értékszám nélküli játékos első játszmái); `null` ugyanott, ahol a `ratingAfter`.
    GameSummary:
      type: object
      description: Egy tárolt játszma összefoglalója.
      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: A játszma azonosítója.
        category:
          type: string
          description: A hivatalos kategória azonosítója (`3+2`), vagy `custom`.
        rated:
          type: boolean
          description: Beleszámít-e a játszma az értékszámokba.
        timeControl:
          type: string
          pattern: '^[0-9]+\+[0-9]+$'
          description: A játékidő másodpercben (`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` (lásd a játszmakódokat).'
        reason:
          type: integer
          minimum: 0
          maximum: 255
          description: A játszma végének okát jelző kód (lásd a játszmakódokat).
        result:
          type: string
          enum:
            - 1-0
            - 0-1
            - 1/2-1/2
            - '*'
          description: Az eredmény; megszakított játszmánál `*`.
        termination:
          $ref: '#/components/schemas/Termination'
        plies:
          type: integer
          minimum: 0
          description: A féllépések száma.
        startedAt:
          type: integer
          format: int64
          description: A kezdés ideje (Unix-idő, ms).
        endedAt:
          type: integer
          format: int64
          description: A befejezés ideje (Unix-idő, ms).
    Termination:
      type: string
      description: A játszma végét okozó ok neve (lásd a játszmakódokat); a protokoll által nem ismert kódnál `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: Játszma-összefoglaló az egyik játékos szemszögéből.
      allOf:
        - $ref: '#/components/schemas/GameSummary'
        - type: object
          required:
            - color
          properties:
            color:
              type: string
              enum:
                - white
                - black
              description: A játékos színe.
    HistoryGameSummary:
      description: A bejelentkezett játékos egy korábbi játszmája.
      allOf:
        - $ref: '#/components/schemas/PlayerGameSummary'
        - type: object
          required:
            - baseMs
            - incMs
            - outcome
          properties:
            baseMs:
              type: integer
              format: int64
              minimum: 0
              description: Az alapidő ezredmásodpercben.
            incMs:
              type: integer
              format: int64
              minimum: 0
              description: A többletidő ezredmásodpercben.
            outcome:
              type: string
              enum:
                - win
                - loss
                - draw
                - aborted
              description: A kimenetel a játékos szemszögéből.
    HistoryPage:
      type: object
      required:
        - games
        - next
        - total
      properties:
        games:
          type: array
          description: Az oldal játszmái, a legújabbal kezdve.
          items:
            $ref: '#/components/schemas/HistoryGameSummary'
        next:
          type:
            - integer
            - 'null'
          format: int64
          description: A következő oldalhoz `before` paraméterként megadandó azonosító, vagy az utolsó oldalon `null`.
        total:
          type: integer
          minimum: 0
          description: A szűrőnek megfelelő játszmák száma, az összes oldalon együtt.
    MoveRecord:
      type: object
      description: A játszma egy féllépése.
      required:
        - uci
        - spentMs
        - clockMs
      properties:
        uci:
          type: string
          pattern: '^[a-h][1-8][a-h][1-8][nbrq]?$'
          description: A lépés UCI-jelöléssel; átváltozásnál a végére kerül egy `n`, `b`, `r` vagy `q`.
        spentMs:
          type:
            - integer
            - 'null'
          minimum: 0
          description: A lépésre elszámolt idő (ms), `null`, ha a rekordban nincs meg.
        clockMs:
          type:
            - integer
            - 'null'
          minimum: 0
          description: A lépő játékos órája a lépés után (ms), `null`, ha a rekordban nincs meg.
    PgnTagsView:
      type: object
      description: A fő PGN-címkék, megjelenítéshez. A `Termination` itt a játszma végét okozó ok neve; a PGN-fájl a PGN-szabvány szerinti értékeket használja.
      required:
        - Event
        - Site
        - Date
        - Round
        - White
        - Black
        - Result
        - WhiteElo
        - BlackElo
        - TimeControl
        - Termination
        - PlyCount
      properties:
        Event:
          type: string
          description: '`<SERVER_NAME> rated <category>` vagy `<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: A kezdés dátuma UTC szerint.
        Round:
          type: string
          const: '-'
        White:
          type: string
        Black:
          type: string
        Result:
          type: string
          enum:
            - 1-0
            - 0-1
            - 1/2-1/2
            - '*'
        WhiteElo:
          description: Az értékszám a kezdéskor, vagy `"-"`.
          oneOf:
            - type: integer
            - type: string
              const: '-'
        BlackElo:
          description: Az értékszám a kezdéskor, vagy `"-"`.
          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: Játszmarekord a lépéseivel és az óraállásaival. A játszma-összefoglaló mezőit tartalmazza, a `color` és az `outcome` kivételével.
      allOf:
        - $ref: '#/components/schemas/GameSummary'
        - type: object
          required:
            - baseMs
            - incMs
            - statusName
            - rematchOf
            - moves
            - pgn
          properties:
            baseMs:
              type: integer
              format: int64
              minimum: 0
              description: Az alapidő ezredmásodpercben.
            incMs:
              type: integer
              format: int64
              minimum: 0
              description: A többletidő ezredmásodpercben.
            statusName:
              type: string
              enum:
                - WhiteWins
                - BlackWins
                - Draw
                - Aborted
              description: A `status` neve.
            rematchOf:
              type:
                - integer
                - 'null'
              format: int64
              description: Annak a játszmának az azonosítója, amelynek ez a visszavágója, vagy `null`.
            moves:
              type: array
              description: Féllépésenként egy bejegyzés.
              items:
                $ref: '#/components/schemas/MoveRecord'
            pgn:
              $ref: '#/components/schemas/PgnTagsView'
            you:
              type: string
              enum:
                - white
                - black
              description: Csak ha a token játékosa játszott ebben a játszmában; az ő színe.
            reportable:
              type: boolean
              description: Csak ha a token játékosa játszott ebben a játszmában; `true`, ha a `POST /reports` most elfogadná az ellenfél jelentését erre a játszmára (a játszma 7 napon belül fejeződött be, a napi kvóta nem fogyott el, és az ellenfelet még nem jelentették erre a játszmára).
    PlayerProfile:
      type: object
      required:
        - username
        - createdAt
        - ratings
        - games
      properties:
        username:
          type: string
          description: A felhasználónév, ahogy a játékos írta.
        createdAt:
          type: integer
          format: int64
          description: A fiók létrehozásának ideje (Unix-idő, ms).
        ratings:
          type: array
          description: Csak a hivatalos kategóriák, a szerver sorrendjében.
          items:
            $ref: '#/components/schemas/RatingSummary'
        games:
          type: object
          required:
            - total
            - rated
            - wins
            - draws
            - losses
          properties:
            total:
              type: integer
              minimum: 0
              description: Minden tárolt játszma, a barátságosakat és a megszakítottakat is beleértve.
            rated:
              type: integer
              minimum: 0
              description: Értékelt játszmák, az értékszám-rekordokból összegezve.
            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: A felhasználónév, ahogy a játékos írta.
        games:
          type: array
          description: Az oldal játszmái, a legújabbal kezdve.
          items:
            $ref: '#/components/schemas/PlayerGameSummary'
        next:
          type:
            - integer
            - 'null'
          format: int64
          description: Az utolsó játszma azonosítója, ha az oldal megtelt (a következő oldal ekkor üres is lehet), egyébként `null`.
    Leaderboard:
      type: object
      required:
        - category
        - minGames
        - updatedAt
        - players
      properties:
        category:
          type: string
          description: A kategória azonosítója.
        minGames:
          type: integer
          minimum: 0
          description: A listázáshoz egy rekordnak szükséges beszámított játszmák száma (`PROVISIONAL_GAMES`).
        updatedAt:
          type: integer
          format: int64
          description: A ranglista kiszámításának ideje (Unix-idő, ms).
        players:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/LeaderboardEntry'
    LeaderboardEntry:
      type: object
      required:
        - rank
        - username
        - rating
        - games
        - wins
        - draws
        - losses
        - peak
      properties:
        rank:
          type: integer
          minimum: 1
        username:
          type: string
        rating:
          type: integer
        games:
          type: integer
          minimum: 0
        wins:
          type: integer
          minimum: 0
        draws:
          type: integer
          minimum: 0
        losses:
          type: integer
          minimum: 0
        peak:
          type: integer
    GifRequest:
      type: object
      additionalProperties: false
      required:
        - pgn
      properties:
        pgn:
          type: string
          description: A PGN-szöveg, legfeljebb 65 536 bájt UTF-8 kódolásban. A szerver csak az első játszmát használja.
        size:
          type: string
          enum:
            - small
            - medium
            - large
          default: medium
          description: Nem kötelező. A kép mérete.
        orientation:
          type: string
          enum:
            - white
            - black
          default: white
          description: Nem kötelező. A tábla alján lévő fél.
        delayMs:
          type: integer
          minimum: 100
          maximum: 3000
          default: 500
          description: Nem kötelező. Ezredmásodperc lépésenként, egész értékű JSON-szám.
        coords:
          type: boolean
          default: true
          description: Nem kötelező. Megjelennek-e a vonalak betűi és a sorok számai a tábla körül.
    ReportRequest:
      type: object
      required:
        - gameId
        - reported
        - category
      properties:
        gameId:
          description: A játszma, egész számként vagy 1–16 számjegyből álló karakterláncként.
          oneOf:
            - type: integer
              format: int64
              minimum: 1
              maximum: 9007199254740991
            - type: string
              pattern: '^[0-9]{1,16}$'
        reported:
          type: string
          minLength: 1
          maxLength: 24
          description: Az ellenfél felhasználóneve, a játszmarekordban szereplő vagy a jelenlegi alakjában, kis- és nagybetűtől függetlenül. Nem lehet üres.
        category:
          type: string
          enum:
            - cheating
            - abuse
            - other
          description: Mire vonatkozik a jelentés.
        comment:
          type:
            - string
            - 'null'
          description: Nem kötelező. Legfeljebb 500 karakter a vezérlőkarakterek (a tabulátor és a soremelés kivételével) eltávolítása, valamint a kezdő és záró szóközök levágása után.
    AccountExport:
      type: object
      description: Minden, amit a szerver a fiókról tárol (az időpontok Unix-időben, ms).
      required:
        - format
        - version
        - exportedAt
        - server
        - notes
        - account
        - ratings
        - ratingRefunds
        - sessions
        - securityEvents
        - sanctions
        - conduct
        - reportsFiled
        - games
      properties:
        format:
          type: string
          const: scacelith-account-export
        version:
          type: integer
          const: 1
        exportedAt:
          type: integer
          format: int64
          description: Az exportálás ideje.
        server:
          type: object
          required:
            - name
            - host
          properties:
            name:
              type: string
              description: '`SERVER_NAME`.'
            host:
              type: string
              description: '`SERVER_PUBLIC_HOST`.'
        notes:
          type: array
          description: Mit tartalmaz és mit hagy ki a fájl, egyszerű angol nyelven a játékos számára.
          items:
            type: string
        account:
          description: A `GET /account/me` fióknézete, `googleEmail` mezővel kiegészítve.
          allOf:
            - $ref: '#/components/schemas/AccountView'
            - type: object
              required:
                - googleEmail
              properties:
                googleEmail:
                  type:
                    - string
                    - 'null'
                  description: A kapcsolt Google-fiók címe, vagy `null`.
        ratings:
          type: array
          description: A teljes értékszám-rekordok.
          items:
            $ref: '#/components/schemas/ExportRating'
        ratingRefunds:
          type: array
          description: Az ellenfél csalásának megállapítása után visszakapott értékszámpontok, UTC-naponként és kategóriánként összesítve, a legújabbal kezdve. Sem a játszmák, sem a csalók nincsenek megnevezve.
          items:
            $ref: '#/components/schemas/RatingRefund'
        sessions:
          type: array
          description: Minden tárolt munkamenet, a legújabbal kezdve, tokenek nélkül. Az adatmegőrzési törlés eltávolítja a lejárt munkamenetet, a kijelentkeztetettet pedig a kijelentkeztetés után egy nappal.
          items:
            $ref: '#/components/schemas/ExportSession'
        securityEvents:
          type: array
          description: A biztonsági események, a legújabbal kezdve, `RETENTION_SECURITY_DAYS` napig megőrizve.
          items:
            $ref: '#/components/schemas/SecurityEvent'
        sanctions:
          type: array
          description: Minden szankció, a feloldottakat is beleértve. A moderátor neve soha nem szerepel benne.
          items:
            $ref: '#/components/schemas/ExportSanction'
        conduct:
          type: array
          description: A játékos elhagyott, megszakított és első lépés nélkül maradt játszmáihoz rögzített magatartási események, 30 napig megőrizve.
          items:
            $ref: '#/components/schemas/ConductEvent'
        reportsFiled:
          type: array
          description: A játékos által tett jelentések.
          items:
            $ref: '#/components/schemas/FiledReport'
        games:
          type: object
          required:
            - total
            - list
          properties:
            total:
              type: integer
              minimum: 0
              description: A játszmák száma.
            list:
              type: array
              description: Minden játszma, a legújabbal kezdve, a `GET /account/games` összefoglalóiként. A lépések a `GET /games/{id}` végponton érhetők el, a PGN a `GET /games/{id}/pgn` végponton.
              items:
                $ref: '#/components/schemas/HistoryGameSummary'
    ExportRating:
      description: Teljes értékszám-rekord.
      allOf:
        - $ref: '#/components/schemas/RatingSummary'
        - type: object
          required:
            - rated
            - countedGames
            - updatedAt
          properties:
            rated:
              type: boolean
              description: Kilépett-e már a játékos az értékszám nélküli szakaszból.
            countedGames:
              type: integer
              minimum: 0
              description: Az értékszámba beszámított játszmák.
            updatedAt:
              type: integer
              format: int64
              description: A rekord utolsó módosításának ideje.
    RatingRefund:
      type: object
      required:
        - day
        - category
        - points
      properties:
        day:
          type: integer
          format: int64
          description: A nap 00:00 UTC időpontja (Unix-idő, ms).
        category:
          type: string
        points:
          type: integer
          description: Az aznap, abban a kategóriában visszaadott pontok.
    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: A munkamenet kijelentkeztetésének ideje, vagy `null`.
        clientLabel:
          type:
            - string
            - 'null'
        ip:
          type:
            - string
            - 'null'
          description: A bejelentkezés IP-címe, `RETENTION_IP_DAYS` nap után törlődik.
    SecurityEvent:
      type: object
      description: |-
        Biztonsági esemény. Az `ip` csak a bejelentkezve, a fiók jelszavával (és második faktorral) vagy a fiók címére küldött linkkel végzett műveleteknél szerepel: `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` és `account_exported`; minden más típusnál `ip: null`, és az `ip` `RETENTION_IP_DAYS` nap után törlődik.

        A `detail` csak ezeket a mezőket őrzi meg: `login`: `method`; `sso_login`, `sso_account_created`: `provider`; `sso_linked`: `provider` és `method` (`password` vagy `password+totp`); `login_failed`: `failures`; `login_lockout`: `retryAfterMs`; `mfa_failed`: `attempts`; `recovery_code_used`: `remaining`; `reauth_failed`: `factor`; `session_revoked` és `sessions_revoked_all`: `reason`; `email_change_refused`: `reason`; `sanction_auto`: `kind`, `gameId`, `until`. Egy `moderator_action` esemény csak a `{ action }` értéket őrzi meg, a `ban`, `unban`, `reset_mfa`, `verify_email` és `revoke_sessions` műveleteknél (a többi moderátori művelet kimarad). Minden más típusnál `detail: null`. A `rating_refund` események kimaradnak: a pontjaik a `ratingRefunds` mezőben vannak.
      required:
        - kind
        - at
        - ip
        - detail
      properties:
        kind:
          type: string
          description: Az esemény típusa (`login`, `password_changed`…).
        at:
          type: integer
          format: int64
          description: Mikor történt.
        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: A jelentett játékos jelenlegi nyilvános neve.
        category:
          type: string
          enum:
            - cheating
            - abuse
            - other
        comment:
          type:
            - string
            - 'null'
        createdAt:
          type: integer
          format: int64
        status:
          type: string
          enum:
            - open
            - closed
          description: Nyitott-e még a jelentés (az nem derül ki, hogy a jelentett játékost szankcionálták-e).
    LinkTokenForm:
      type: object
      additionalProperties: false
      required:
        - token
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: Az e-mailben küldött link tokenje (az oldal űrlapjának rejtett mezője).
    ResetPasswordForm:
      type: object
      additionalProperties: false
      required:
        - token
        - newPassword
        - confirmPassword
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: A visszaállító link tokenje (az űrlap rejtett mezője).
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Az új jelszó; a regisztráció jelszószabályai érvényesek.
        confirmPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Az új jelszó még egyszer; meg kell egyeznie.
    HtmlDocument:
      type: string
      contentMediaType: text/html
      description: UTF-8 kódolású HTML-oldal (`text/html; charset=utf-8`), JavaScript és külső erőforrások nélkül.
