openapi: 3.1.1
info:
  title: API HTTPS strežnika Scacelith
  version: "1.1.0"
  summary: API HTTPS vseh strežnikov Scacelith, uradnega in skupnostnih.
  description: |-
    Vsak strežnik Scacelith, tako uradni `caissa.scacelith.com` kot vsak skupnostni strežnik, podpira ta API HTTPS. Igra ga uporablja za vse, kar ni sama partija: registracijo in prijavo (vključno z dvofaktorskim preverjanjem pristnosti in prijavo z Googlom), stran računa, zgodovino partij, prenose PGN in animirane GIF-e partij, prijavljene naprave, prenos podatkov, izbris računa in prijave kršitev.

    Igra v živo (iskanje nasprotnika, izzivi, poteze, ure) poteka prek WebSocketa istega strežnika (`wss://<host>/ws`), ki se odpre z žetonom seje tega API-ja; opisana je v `PROTOCOL.md`, ne tukaj.

    Spodaj navedene privzete vrednosti so vrednosti strežnika z nespremenjeno konfiguracijo; skupnostni strežnik jih lahko spremeni (`CONFIG.md` navaja vse nastavitve).

    ## Osnovni URL, vrata in prenos

    - Uradni strežnik: `https://caissa.scacelith.com/api/v1`, vrata TCP 443.
    - Skupnostni strežnik: `https://<SERVER_PUBLIC_HOST>[:<port>]/api/v1`. `API_PORT` je privzeto 443; za NAT-om ali posredniškim strežnikom (proxy) so vrata, ki jih uporabljajo igralci, `PUBLIC_API_PORT`.
    - WebSocket igre je privzeto na istih vratih (`WS_PORT` ga premakne; `GET /info` odjemalcu pove, kje je).
    - HTTP/1.1 prek TLS. Pri `TLS_MODE=proxy` TLS zaključuje obratni posredniški strežnik pred strežnikom. `TLS_MODE=off` (navaden HTTP) je samo za lokalni razvoj.
    - Strani HTML, ki se odprejo s povezavami v e-poštnih sporočilih (`/verify-email`, `/reset-password`, `/confirm-email-change`), so v korenu strežnika, zunaj `/api/v1`. Končne točke za preverjanje stanja odgovarjajo tako v korenu kot pod `/api/v1`. Prijava z Googlom na strežniku nima nobene strani: Google brskalnik preusmeri nazaj v samo igro, na `127.0.0.1`, ali na stran spletnega mesta, ki je prijavo začelo (`GOOGLE_WEB_REDIRECT_URI`).
    - Končna poševnica se ne upošteva (`/api/v1/info/` je `/api/v1/info`), parametri poti pa se dekodirajo iz kodiranja URL (na parameter, ki ni veljavno kodiran za URL, je odgovor 400 `invalid_request`).

    ## Zahteve

    - **Telesa JSON.** Zahteve `POST`, `PUT` in `DELETE` vsebujejo objekt JSON z `Content-Type: application/json`. `charset`, ki ni UTF-8, je zavrnjen, prav tako vsaka druga vrsta vsebine (415 `unsupported_media_type`). Prazno telo šteje kot `{}`, kar sprejemajo končne točke brez parametrov (`POST /auth/logout`, `POST /auth/logout-all`, `DELETE /auth/sessions/{id}`).
    - **Velikost in čas.** Telo je omejeno na `HTTP_BODY_LIMIT` bajtov (privzeto 16.384; `POST /gif` ima lastno omejitev 135.168 bajtov): 413 `payload_too_large`. Prispeti mora v 10 sekundah: 408 `request_timeout`. Po kateri koli od teh dveh napak strežnik zapre povezavo.
    - **Stroge sheme.** Polje, ki ga končna točka ne pozna, je zavrnjeno, polje, ki ni označeno kot neobvezno, je obvezno, preverjajo pa se tudi vrste in dolžine. Odgovor na vsako od teh napak je 400 `invalid_request`, pri čemer `field` navede polje (za gnezdeno polje s pikami, na primer `pow.nonce`). Nizi ne smejo vsebovati kontrolnih znakov. Dolžine se štejejo v kodnih enotah UTF-16, zato znak zunaj osnovne večjezične ravnine (emoji) šteje dvakrat. Odgovor na telo, ki ni JSON, je 400 `invalid_json`. `POST /gif` in `POST /reports` svoja telesa preverjata sama (glejte posamezno končno točko).
    - **Poizvedbeni nizi.** Šteje prva pojavitev parametra, neznani parametri se ne upoštevajo, `+` pa se dekodira v presledek: končne točke, ki sprejemajo kategorijo tempa igre, sprejmejo tako `3%2B2` kot `3+2`.
    - **Metode.** `HEAD` je `GET` brez telesa. `OPTIONS` na obstoječi poti odgovori 204 z glavo `Allow` (in glavami zahteve preflight CORS, glejte spodaj). Na vsako drugo metodo, ki je pot nima, je odgovor 405 `method_not_allowed` z `Allow`.
    - **Cilj zahteve.** Ne sme presegati 4096 znakov (414 `uri_too_long`). Zahteva, ki je sploh ni mogoče razčleniti ali katere glava je prevelika ali prispe prepočasi, dobi prazen odgovor 400, 431 ali 408, povezava pa se zapre.
    - **CORS: seznam dovoljenih, privzeto izklopljen.** API streže igri, ki ne pošilja `Origin`. Ko je `CORS_ORIGINS` prazen (privzeto), se nikoli ne pošlje nobena glava `Access-Control-*`, zato nobena spletna stran ne more prebrati odgovora. `CORS_ORIGINS` navaja spletne izvore, katerih strani lahko kličejo API iz brskalnika, vsakega natanko tako, kot ga brskalniki pošljejo v `Origin`: `https://host` ali `https://host:port`, z malimi črkami, brez poti, končne poševnice ali privzetih vrat; `http://` samo za `localhost`, `127.0.0.1` in `[::1]` (lokalni razvoj); brez nadomestnih znakov. Uradni strežnik za svoje spletno mesto nastavi `CORS_ORIGINS=https://scacelith.com,https://www.scacelith.com`. Takrat:
      - Vsak odgovor za pot pod `/api/v1`, razen končnih točk za preverjanje stanja, tako uspešen kot z napako (vključno z 429 sloja po naslovu), vsebuje `Vary: Origin`. Kadar je `Origin` zahteve bajt za bajtom enak enemu od navedenih izvorov, vsebuje tudi `Access-Control-Allow-Origin: <that origin>` in, razen v odgovoru na zahtevo preflight, `Access-Control-Expose-Headers: Retry-After, Content-Disposition`, tako da stran lahko prebere kode napak, `Retry-After` in imena datotek prenosov PGN in GIF.
      - Na zahtevo preflight (`OPTIONS` z `Access-Control-Request-Method`) z navedenega izvora na obstoječi poti je odgovor 204, brez telesa, z `Allow`, `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods` (metode iz `Allow`), `Access-Control-Allow-Headers: Authorization, Content-Type`, `Access-Control-Max-Age: 600` in `Vary: Origin`. Ne potrebuje žetona seje in se ne šteje v nobeno kvoto računa niti v nobeno omejitev končne točke; kot vsaka zahteva porabi žeton sloja po naslovu. Brskalnik zahtevo preflight znova uporablja 10 minut, vendar samo za točno ta URL, vključno s poizvedbenim nizom. Zahteva brez `Authorization` in brez telesa JSON, na primer `GET /info` ali `GET /leaderboard`, ne potrebuje zahteve preflight.
      - Vsak drug odgovor na zahtevo preflight pomeni neuspelo zahtevo preflight, tudi z glavami CORS: 404 za pot, ki ne obstaja, ali 429 sloja po naslovu. Brskalnik takrat zahteve ne pošlje, stran pa vidi omrežno napako, brez odgovora in njegovega `Retry-After`.
      - `Access-Control-Allow-Credentials` se nikoli ne pošlje, prav tako ne `Access-Control-Allow-Origin: *`: stran pošlje svoj žeton seje v `Authorization: Bearer`, tako kot igra, API pa ne nastavlja piškotkov. Strani HTML, končne točke za preverjanje stanja in prazni odgovori na neberljive zahteve nimajo nobene glave CORS. WebSocket ne uporablja CORS: `WS_ALLOWED_ORIGINS` navaja strani, ki ga smejo odpreti.
      - Stran z izvora, ki ni naveden, ne prebere ničesar, njene zahteve za pisanje z drugega spletnega mesta pa se nikoli ne pošljejo: telesa JSON in `Authorization` potrebujejo zahtevo preflight, katere odgovor za ta izvor nima `Access-Control-Allow-Origin`. `Cross-Origin-Resource-Policy: same-origin` ostane na vsakem odgovoru: velja samo za zahteve `no-cors` (drugo spletno mesto vdela odgovor), nikoli za zahteve `cors` navedene strani.

    ## Odgovori in napake

    - Odgovori so JSON v UTF-8, razen prenosa PGN (`application/x-chess-pgn`), animiranih GIF-ov (`image/gif`) in strani HTML. Časi so v milisekundah od 1970-01-01 UTC. ID-ji so cela števila. ID-ji partij imajo do 16 števk, vendar ostanejo pod 2^53, zato jih število JSON (double) zapiše natančno.
    - Vsak odgovor API-ja vsebuje `Cache-Control: no-store`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`, `X-Frame-Options: DENY`, `Cross-Origin-Resource-Policy: same-origin` in `Content-Security-Policy: default-src 'none'; frame-ancestors 'none'` (strani HTML imajo lasten pravilnik). Pri `TLS_MODE=native` vsebuje tudi `Strict-Transport-Security: max-age=31536000`. Odgovori, ki jih poslušalec zapiše sam, pred API-jem (`GET` in `HEAD` končnih točk za preverjanje stanja, 429 sloja po naslovu in 500 `internal_error`, kadar API v celoti odpove), vsebujejo enake glave razen `Cross-Origin-Resource-Policy`, njihov `Content-Type` pa je `application/json` brez nabora znakov.
    - Odgovor mora biti prebran v 60 sekundah od trenutka, ko ga ima strežnik pripravljenega, kar je pomembno samo pri velikih odgovorih (GIF, izvoz podatkov, dolg PGN). Čas, ki ga strežnik porabi za pripravo odgovora, se ne šteje ne v ta rok ne v 30 sekund brez prejetega ali poslanega bajta, po katerih se povezava zapre.

    Napake imajo enotno obliko (shema `Error`):

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

    `message` je namenjen dnevnikom in kot nadomestna možnost; odjemalec izbere, kaj prikaže, glede na `error`. `retryAfter` (v sekundah) je prisoten samo pri zavrnitvah, ki s časom prenehajo, odgovor pa takrat vsebuje tudi glavo `Retry-After` z isto vrednostjo; edina izjema je 503 `busy` pri branju zgodovine, partij, igralcev in lestvice, ki ima samo polje. Nekatere napake dodajo polja: `field` (neveljaven vnos), `reason` (`weak_password`, `pow_required`), `pow` (`pow_required`), `until` (`banned`), `line` in `column` (`invalid_pgn`).

    Napake, ki jih lahko vrne katera koli končna točka:

    | Status | `error` | Kdaj |
    |---|---|---|
    | 400 | `invalid_request` | Telo krši shemo končne točke (`field` pove, kje), cilj zahteve ali `Content-Length` je napačno oblikovan, parameter poti ni veljavno kodiran za URL ali pa je bilo telo odrezano. |
    | 400 | `invalid_json` | Telo ni JSON. |
    | 401 | `unauthorized` | Pri končni točki, ki zahteva sejo, ni glave `Authorization`. |
    | 401 | `invalid_token` | Žeton je napačno oblikovan, potekel, preklican ali pripada izbrisanemu računu; tudi pri končnih točkah, kjer je seja neobvezna. |
    | 404 | `not_found` | Te končne točke ni. To kodo uporabljajo tudi nekatere končne točke: partija, igralec ali seja ne obstaja. |
    | 405 | `method_not_allowed` | Pot obstaja za druge metode (glejte `Allow`). |
    | 408 | `request_timeout` | Telo ni prispelo v 10 sekundah. |
    | 413 | `payload_too_large` | Telo presega `HTTP_BODY_LIMIT` (135.168 bajtov za `POST /gif`). |
    | 414 | `uri_too_long` | Cilj zahteve presega 4096 znakov. |
    | 415 | `unsupported_media_type` | Telo ni `application/json` ali njegov nabor znakov ni UTF-8. |
    | 429 | `rate_limited` | Omejitev števila zahtev (glejte spodaj): `retryAfter` in glava `Retry-After`. |
    | 500 | `internal_error` | Nepričakovana napaka. Strežnik jo zapiše v dnevnik. |
    | 503 | `timeout` | Strežnik ni odgovoril v 30 sekundah (60 sekund za izvoz, 45 sekund za GIF-e pri privzetih nastavitvah). |
    | 503 | `server_busy` | Iskanje seje ali sprememba računa je naletela na zaklenjeno zbirko podatkov (`retryAfter` 1) ali pa je čakalna vrsta za zgoščevanje gesel polna (glejte spodaj). Tudi z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja: pri `POST`, `PUT`, `PATCH` ali `DELETE` v zbirki podatkov čaka preveč zapisovanj; pri kateri koli zahtevi se preveč prejšnjih zahtev še vedno izvaja po poteku časa za obdelavo zahteve ali pa žetona seje ni bilo mogoče preveriti v 5 sekundah. Zahteva ni bila izvedena in njene omejitve števila zahtev niso porabile ničesar. |

    Končne točke za branje (zgodovina partij, partije, igralci, lestvica) in izvoz odgovorijo 503 `busy` z `retryAfter: 1`, kadar je zbirka podatkov ostala zaklenjena.

    Končne točke, ki preverjajo ali zgoščujejo geslo, lahko odgovorijo tudi z eno od teh napak, obe z naključnim `retryAfter` od 5 do 15 sekund:

    - 503 `server_busy`: strežnikova čakalna vrsta za zgoščevanje gesel (`PASSWORD_HASH_QUEUE_MAX`) je polna ali pa se je čakanje izteklo.
    - 429 `rate_limited`: ko je čakalna vrsta napol polna, ima ta odjemalec (naslov IPv4 ali IPv6 /48) že `PASSWORD_HASH_WAITERS_PER_SOURCE` zgoščevanj na čakanju. Ta 429 vrne žetone omejitev števila zahtev, ki jih je zahteva porabila.

    Nič ni bilo spremenjeno in noben neuspel poskus ni bil štet (razen pri `POST /auth/sso/google/link`, kjer sta bila poskus vstopnice in neuspeh računa šteta že pred zgoščevanjem), povezava za ponastavitev gesla pa ostane veljavna.

    Strani HTML svoje napake vrnejo kot strani HTML z istimi statusnimi kodami.

    ## Preverjanje pristnosti

    Končne točke, ki zahtevajo sejo, sprejmejo nosilni žeton (bearer) v glavi `Authorization` (shema `bearerAuth`):

    ```
    Authorization: Bearer sct_L_8GDd7uzfQ3QQWtqrsWXDTsFWzRwIvJcwIGHhjWPS8
    ```

    - **Pridobitev žetona.** Žeton (`sct_`, ki mu sledi 43 znakov base64url) vrne `POST /auth/login`, nato `POST /auth/login/mfa`, kadar je dvofaktorsko preverjanje pristnosti vklopljeno, vrnejo pa ga tudi `POST /auth/sso/google/finish` in `POST /auth/sso/google/link` (prijava z Googlom) ter `POST /auth/sso/complete`. Vse te končne točke odgovorijo z `{ token, expiresAt, user }`. Strežnik hrani samo SHA-256 žetona.
    - **Obvezna seja.** Na manjkajočo glavo je odgovor 401 `unauthorized` z `WWW-Authenticate: Bearer realm="scacelith"`. Na neveljaven žeton je odgovor 401 `invalid_token` z `WWW-Authenticate: Bearer realm="scacelith", error="invalid_token"`. Odjemalec naj pozabi žeton, ki dobi `invalid_token`, in se znova prijavi.
    - **Neobvezna seja.** Pri `GET /games/{id}`, `GET /games/{id}/pgn`, `GET /players/{username}` in `GET /players/{username}/games` je seja neobvezna. Brez glave vrnejo javni pogled; če je glava poslana, mora biti njen žeton veljaven.
    - **Zasedena shramba.** Kadar zbirka podatkov med preverjanjem žetona ostane zaklenjena, končna točka, ki sejo zahteva ali sprejema, odgovori 503 `server_busy` z `retryAfter: 1` (in `Retry-After`); kadar preverjanje traja več kot 5 sekund, odgovori 503 `server_busy` z `retryAfter` od 2 do 5. V obeh primerih o žetonu ni bilo odločeno: obdržite ga in zahtevo pošljite znova.
    - **Življenjska doba.** Seja se konča ob prvem od dveh rokov: `SESSION_MAX_DAYS` (90) dni po prijavi (to je `expiresAt`) ali `SESSION_IDLE_DAYS` (30) dni brez uporabe. Vsaka uporaba odmakne omejitev nedejavnosti; strežnik novo vrednost zapiše največ vsakih 5 minut, v ozadju. Dokler v njegovi zbirki podatkov čaka veliko zapisovanj, to zapisovanje odloži na poznejšo zahtevo, razen v zadnji uri pred iztekom omejitve nedejavnosti, tako da seja, ki je v uporabi, ne poteče. Račun ima lahko največ `MAX_SESSIONS_PER_USER` (10) sej: nova prijava prekliče najstarejše seje nad tem številom.
    - **Preklic.** Odjava (`POST /auth/logout`, `POST /auth/logout-all`, `DELETE /auth/sessions/{id}`) prekliče seje; prav tako jih prekličejo sprememba gesla (druge seje), ponastavitev gesla in izbris računa (vse seje) ter skrbnik. Preklic prek API-ja začne veljati takoj, WebSocket, odprt s preklicano sejo, pa se zapre. Sprememba ali ponastavitev gesla in izbris računa seje prekličejo v isti transakciji kot samo spremembo: če ena od njih ne uspe, ni bila preklicana nobena seja in tudi nič drugega ni bilo spremenjeno. Strežnik rezultate iskanja sej predpomni 30 sekund, zato preklic s skrbniškim ukazom, ki je ločen proces, začne veljati v 30 sekundah.
    - **Obseg.** Žeton pripada enemu strežniku in odpre tudi njegov WebSocket. Nikoli ga ne pošiljajte drugemu strežniku.

    ## Omejitve števila zahtev in druge omejitve

    Omejitve veljajo za enega od dveh obsegov. **Odjemalec:** naslov IPv4 ali IPv6 /64 (v načinu proxy naslov izhaja iz `X-Forwarded-For`, ki ga pošlje naslov iz `TRUSTED_PROXIES`); nekatere omejitve poleg vsakega omrežja /64 štejejo tudi vsako omrežje IPv6 /48 kot celoto. **Igralec:** prijavljeni račun, ne glede na njegov naslov; omejitev, ki se šteje po igralcu, se pri končni točki z neobvezno sejo za zahtevo brez žetona šteje po odjemalcu.

    Vsaka omejitev je vedro žetonov, ki sprejme `limit` zahtev in se neprekinjeno polni s hitrostjo `limit / window`; `retryAfter` je čas do naslednjega žetona. Omejitve, označene kot *deljene*, se štejejo tudi v drsnem oknu enake dolžine, tako da nobeno okno ne vsebuje bistveno več kot `limit` zahtev. Vsaka omejitev se šteje za celoten strežnik. Kadar ena od omejitev končne točke zavrne zahtevo, se žetoni, ki so jih za to zahtevo porabile njene druge omejitve, vrnejo. Odgovor na zavrnitev je 429 `rate_limited` z `retryAfter` in `Retry-After`.

    - **Sloj po naslovu.** Vsaka zahteva (katera koli pot in metoda, vključno s končnimi točkami za preverjanje stanja in nadgradnjami na WebSocket) najprej porabi en žeton iz kvote svojega odjemalca: `HTTP_RATE_PER_IP` (600) na minuto z izbruhom (burst) za pol minute in `HTTP_RATE_PER_PREFIX` (4 x `HTTP_RATE_PER_IP`) za IPv6 /48. Odjemalec ima lahko tudi največ `IP_MAX_INFLIGHT` (32 x `WORKERS`) zahtev v obdelavi (nad tem `retryAfter` 1). Odjemalec, ki po zavrnitvah nadaljuje, je blokiran: `ABUSE_BLOCK_REFUSALS_PER_MIN` (600) zavrnitev v eni minuti ga blokira za 1 minuto, nato za 4, 16 in 60 minut ob vsaki novi blokadi v 6 urah. Zavrnitev omejitev `auth`, `auth_*` in `reauth` šteje 5; zavrnitve omejitev, ki se štejejo po igralcu, ne štejejo nikoli. Naslovi v `ABUSE_EXEMPT` ta sloj preskočijo, ne pa tudi kvote računa ali omejitev končnih točk.
    - **Kvota računa.** Vsaka zahteva z veljavnim žetonom seje se šteje tudi v kvoto svojega računa: `USER_RATE_PER_MIN` (120) na minuto za vse končne točke skupaj, ne glede na naslov, z izbruhom (burst) za pol minute.
    - **Omejitve končnih točk:**

    | Omejitev | Privzeto | Šteje se po | Končne točke |
    |---|---|---|---|
    | `auth` | `AUTH_RATE_PER_IP` (20) / 10 min, deljena | odjemalcu in `AUTH_RATE_PER_PREFIX` (5 x `AUTH_RATE_PER_IP`) po IPv6 /48 | `POST /auth/register`, `/auth/login`, `/auth/login/mfa`, `/auth/verify-email/resend`, `/auth/password/forgot`, `/auth/password/reset`, `/auth/sso/google/link`, `/auth/sso/complete`; `POST /verify-email`, `/reset-password`, `/confirm-email-change` |
    | `auth_register` | `AUTH_REGISTER_PER_HOUR` (10) / uro, deljena | odjemalcu, trikrat toliko po IPv6 /48 | `POST /auth/register` |
    | `auth_mail` | `AUTH_MAIL_PER_HOUR` (10) / uro, deljena | odjemalcu, trikrat toliko po IPv6 /48 | `POST /auth/verify-email/resend` |
    | `auth_forgot` | `AUTH_FORGOT_PER_HOUR` (3) / uro, deljena | odjemalcu, trikrat toliko po IPv6 /48 | `POST /auth/password/forgot` |
    | `auth_forgot_day` | `AUTH_FORGOT_PER_DAY` (10) / 24 ur, deljena | odjemalcu, trikrat toliko po IPv6 /48 | `POST /auth/password/forgot` |
    | `auth_reset` | `AUTH_RESET_PER_HOUR` (10) / uro, deljena | odjemalcu, trikrat toliko po IPv6 /48 | `POST /auth/password/reset`, `POST /reset-password` |
    | `reauth` | številke kot pri `auth`, lastno vedro, deljena | odjemalcu in IPv6 /48 | `POST /account/password`, `/account/mfa/totp/setup`, `/account/mfa/totp/enable`, `/account/mfa/totp/disable`, `/account/mfa/recovery-codes`, `/account/email`, `/account/export`, `/account/delete` |
    | `reauth_user` | `AUTH_REAUTH_PER_USER` (10) / 10 min, deljena | igralcu | iste končne točke kot `reauth` |
    | `account` | 60 / min | igralcu | `GET /account/me`, `PUT /account/preferences` |
    | `account_games` | 60 / min | igralcu | `GET /account/games` |
    | `account_export` | 5 / uro, deljena | igralcu | `POST /account/export` (preveri se pred `reauth`; šteje vsak poskus) |
    | `sessions` | 60 / min | igralcu | `POST /auth/logout`, `/auth/logout-all`, `GET /auth/sessions`, `DELETE /auth/sessions/{id}` |
    | `public_read` | 60 / min | igralcu (odjemalcu brez žetona) | `GET /players/{username}`, `/players/{username}/games`, `/games/{id}`, `/games/{id}/pgn` (eno vedro za vse štiri) |
    | `gif` | 30 / min | igralcu | `GET /games/{id}/gif`, `POST /gif` (eno vedro za obe) |
    | `gif_user_min`, `gif_user_hour` | `GIF_USER_RENDERS_PER_MIN` (4) / min in `GIF_USER_RENDERS_PER_HOUR` (30) / uro, deljeni | igralcu | isti dve, samo kadar je treba GIF izdelati (ne iz predpomnilnika) |
    | `gif_ip_min`, `gif_ip_hour` | `GIF_IP_RENDERS_PER_MIN` (12) / min in `GIF_IP_RENDERS_PER_HOUR` (120) / uro, deljeni | odjemalcu (vsi njegovi računi skupaj), trikrat toliko po IPv6 /48 | isti dve, kot zgoraj |
    | `reports` | 30 / uro | igralcu | `POST /reports` |
    | `sso_start` | 30 / 10 min, deljena | odjemalcu in 90 po IPv6 /48 | `POST /auth/sso/google/start`, `POST /auth/sso/google/web/start` (skupaj) |
    | `sso_finish` | 30 / min | odjemalcu | `POST /auth/sso/google/finish` |
    | `page` | 60 / min | odjemalcu | `GET /verify-email`, `/reset-password`, `/confirm-email-change` |

    `GET /info` in `GET /leaderboard` nimata lastne omejitve: samo sloj po naslovu. Končna točka z več omejitvami jih preverja v vrstnem redu, navedenem v njenem opisu.

    Strežnik zahtevo obdela v tem vrstnem redu: sloj po naslovu, nato končne točke za preverjanje stanja; ujemanje končne točke; preverjanja, da strežnik ne zaostaja močno (503 `server_busy`): preveč prejšnjih zahtev se še izvaja po poteku časa za obdelavo zahteve in, pri `POST`, `PUT`, `PATCH` ali `DELETE`, zaostanek zapisovanj v zbirki podatkov; preverjanje pristnosti, v 5 sekundah (nad tem 503 `server_busy`); kvota računa, če zahteva vsebuje sejo; omejitve končne točke; telo; sama končna točka (končne točke za GIF-e porabijo svoje omejitve upodabljanja samo, kadar morajo GIF izdelati). Zahteva, zavrnjena zaradi svojega žetona seje, torej ne porabi nobenega žetona končne točke, zahteva z neveljavnim telesom pa jih porabi.

    Druge omejitve, ki jih uveljavljajo končne točke same:

    - **Neuspele prijave z istim prijavnim imenom** (uporabniško ime ali e-poštni naslov): od `AUTH_FAILURES_PER_ACCOUNT` (5) neuspehov naprej mora vsak poskus čakati dvakrat dlje kot prejšnji (2 s, 4 s in tako naprej, do 15 minut): 429 `too_many_attempts` z `retryAfter`. Števec se ponastavi po eni uri brez neuspehov.
    - **Neuspeli drugi faktorji pri prijavi:** enako pravilo od 5. napačne kode računa naprej. En korak prijave sprejme največ 5 napačnih kod.
    - **Kode drugega faktorja posameznega računa:** največ `AUTH_MFA_PER_ACCOUNT` (10) kod (iz aplikacije ali obnovitvenih, pravilnih ali napačnih) na 15 minut, s katerega koli naslova, pri prijavi in pri ponovnih preverjanjih pristnosti; nad tem 429 `too_many_attempts`, še preden se koda preveri, zato se obnovitvena koda ne porabi.
    - **Neuspela ponovna preverjanja pristnosti računa** (napačno geslo ali koda): enako pravilo (429 `too_many_attempts`), skupno vsem končnim točkam, ki ponovno preverjajo pristnost.
    - **E-poštna sporočila:** eno sporočilo za potrditev ali ponastavitev na naslov vsakih 5 minut (odgovor ostane enak); eno obvestilo »nekdo je poskusil uporabiti vaš naslov« na naslov na uro.
    - **Prijave kršitev:** `REPORTS_PER_DAY` (5) na igralca v 24 urah: 429 `report_limit`.

    ## Dokaz o delu

    `POST /auth/register` vedno zahteva dokaz o delu, kadar je `POW_REGISTER_BITS` večji od 0 (privzeto 18; `GET /info` ga vrne kot `pow.register`). `POST /auth/login` in `POST /auth/sso/google/link` (ena vrsta izziva za obe) ga zahtevata samo 5 minut po tem, ko strežnik zazna val neuspelih prijav (`POW_LOGIN_TRIGGER_PER_MIN`, nato `POW_LOGIN_BITS`); odjemalec to izve iz odgovora.

    1. Odgovor na zahtevo brez dokaza (ali z zavrnjenim dokazom) je 428 `pow_required` z `reason` in izzivom `pow` `{ challenge, bits, expiresAt }`.
    2. Poiščite nonce: decimalni niz z največ 20 števkami, pri katerem se `SHA-256(challenge + ":" + nonce)` začne z `bits` ničelnimi biti (najprej najpomembnejši bit prvega bajta). Za 18 bitov je treba v povprečju izračunati približno 260.000 zgoščenih vrednosti.
    3. Isto zahtevo pošljite znova z `"pow": { "challenge": "...", "nonce": "123456" }` v telesu.

    Izziv velja 2 minuti in samo enkrat, za eno končno točko in eno omrežje odjemalca (naslov IPv4 ali IPv6 /64). Podpisan je, zato strežnik o njem ne hrani ničesar, dokler se ne vrne. `reason` pove, zakaj je bil dokaz zavrnjen: `required`, `malformed`, `signature`, `endpoint`, `network`, `expired`, `bits`, `work` ali `replayed`.

    ## Ponovno preverjanje pristnosti

    Spremembe računa znova zahtevajo geslo in, kadar je dvofaktorsko preverjanje pristnosti vklopljeno, drugi faktor:

    - samo geslo: `POST /account/password` in `POST /account/mfa/totp/setup`;
    - geslo in kodo iz aplikacije, obnovitvena koda pa je zavrnjena: `POST /account/mfa/recovery-codes`;
    - geslo in kodo iz aplikacije ali obnovitveno kodo: `POST /account/mfa/totp/disable`, `POST /account/email`, `POST /account/export` in `POST /account/delete`.

    V telesih `code` vsebuje 6-mestno kodo iz aplikacije, `recoveryCode` pa obnovitveno kodo (`xxxx-xxxx-xx`; velike in male črke, presledki in vezaji niso pomembni). Obnovitveno kodo je mogoče poslati tudi v `code`, kjer so obnovitvene kode sprejete. Vsaka koda deluje enkrat: uporabljena koda iz aplikacije je zavrnjena do naslednjega 30-sekundnega koraka, obnovitvena koda pa po uporabi ne velja več.

    | Status | `error` | Kdaj |
    |---|---|---|
    | 403 | `invalid_password` | Napačno geslo ali pa je bilo geslo med izvajanjem zahteve ponastavljeno ali spremenjeno (nič ni bilo spremenjeno). |
    | 403 | `mfa_code_required` | Dvofaktorsko preverjanje pristnosti je vklopljeno, poslana pa ni bila niti `code` niti `recoveryCode`. |
    | 403 | `invalid_code` | Napačna ali že uporabljena koda. |
    | 400 | `password_not_set` | Račun samo s prijavo z Googlom še nima gesla (»Pozabljeno geslo« ga nastavi). |
    | 429 | `too_many_attempts` | Preveč neuspehov ali preveč preizkušenih kod pri tem računu. |
    | 503 / 429 | `server_busy` / `rate_limited` | Čakalna vrsta za zgoščevanje gesel je zasedena. |

    Napačna gesla in kode se štejejo v števec neuspehov računa in se zabeležijo kot varnostni dogodki.

    ## Vrata za metrike

    Poleg vrat API strežnik na vratih za metrike odgovarja prek navadnega HTTP (`METRICS_PORT`, 9464, vezana na `METRICS_BIND`, privzeto 127.0.0.1; naj ostanejo zasebna). Niso del tega API-ja: `GET /healthz` odgovori `ok`; `GET /readyz` odgovori `ready`, ko je zagon končan (vsak shard je znova predvajal svoj dnevnik in poslušalci so vezani), in do začetka zaustavitve, sicer 503 `not ready`; `GET /metrics` streže metrike Prometheus in, če je nastavljen `METRICS_TOKEN`, zahteva `Authorization: Bearer <token>` s točno tem žetonom.
  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: Izvorna koda strežnika Scacelith in njegova referenčna dokumentacija (API.md, PROTOCOL.md, CONFIG.md).
  url: https://github.com/DarkCenobyte/scacelith-chess-server
servers:
  - url: https://caissa.scacelith.com/api/v1
    description: Uradni strežnik.
  - url: https://{host}:{port}/api/v1
    description: Kateri koli strežnik Scacelith, na primer skupnostni strežnik.
    variables:
      host:
        default: caissa.scacelith.com
        description: Javno ime gostitelja strežnika (`SERVER_PUBLIC_HOST`).
      port:
        default: "443"
        description: Javna vrata API (`PUBLIC_API_PORT`, sicer `API_PORT`).
tags:
  - name: server-info
    x-displayName: Podatki o strežniku
    description: Kaj mora odjemalec vedeti, preden se prijavi ali poveže.
  - name: health
    x-displayName: Preverjanje stanja
    description: |-
      Delovanje in pripravljenost strežnika, za nadzor. Odgovarjajo na vratih API pred preverjanjem pristnosti in pred vsemi omejitvami končnih točk, vendar kot vsaka zahteva porabijo žeton sloja po naslovu; gostitelja za nadzor je mogoče navesti v `ABUSE_EXEMPT`. Za vsako končno točko delujeta obe poti: v korenu strežnika in pod `/api/v1`.

      Vrata za metrike imajo lastne končne točke za preverjanje stanja, opisane v uvodu.
  - name: auth
    x-displayName: Registracija in prijava
    description: |-
      Ustvarjanje računa, prijava z geslom (in drugim faktorjem), e-poštna sporočila za potrditev in za ponastavitev gesla.

      `POST /auth/login`, `POST /auth/login/mfa`, `POST /auth/sso/google/finish`, `POST /auth/sso/google/link` in `POST /auth/sso/complete` vrnejo sejo `{ token, expiresAt, user }` (ali, pri prvih od njih, drugi korak).
  - name: google-sign-in
    x-displayName: Prijava z Googlom
    description: |-
      Na voljo, kadar `GET /info` vrne `sso.google: true`; sicer vse spodnje končne točke odgovorijo 404 `sso_disabled`. Spletno mesto se prijavi na enak način s svojim odjemalcem, kadar je `sso.googleWeb` true (spodaj, **Na spletnem mestu**). Igra se prijavi prek sistemskega brskalnika s potekom za nameščene aplikacije po RFC 8252 (odjemalec vrste »Desktop app«): Google brskalnik preusmeri nazaj k poslušalcu igre na `127.0.0.1`, nikoli na ta strežnik, igra pa nikoli ne vidi nobene Googlove poverilnice.

      1. Odjemalec posluša na `127.0.0.1:0` (vrata izbere sistem) in ustvari par PKCE: `codeVerifier` s 43 do 128 znaki `[A-Za-z0-9._~-]` in `codeChallenge = BASE64URL(SHA-256(codeVerifier))`, ki ima 43 znakov in nima zapolnitve.
      2. `POST /auth/sso/google/start` z izzivom in vrati vrne Googlov URL in `state` poskusa. Odjemalec URL preveri (spodaj) in ga odpre v brskalniku.
      3. Google brskalnik preusmeri na `http://127.0.0.1:<port>/oauth2/google/<tag>?code=...&state=...`. Odjemalec sprejme samo `state` iz odgovora na začetek in po preusmeritvi z `error=` ne pošlje ničesar.
      4. `POST /auth/sso/google/finish` z ID-jem poskusa, `codeVerifier`, `state` in `code` (ter `iss`, če ga je Google poslal).
      5. Odgovor je seja, korak dvofaktorskega preverjanja pristnosti (nadaljujte s `POST /auth/login/mfa`), `needsUsername` za novega igralca (nadaljujte s `POST /auth/sso/complete`) ali `needsPassword`, kadar naslov uporablja račun z geslom (nadaljujte s `POST /auth/sso/google/link`).

      **Oznaka izvora.** URI za preusmeritev vsebuje oznako strežnika, ki ga je igralec dodal, tako da igra zavrne URL, ki ga je drug strežnik pridobil za svoje igralce. Izvor je gostitelj z malimi črkami (dobesedni naslov IPv6 v oglatih oklepajih), `:` in decimalna vrata API, vedno zapisana, tudi 443: strežnik vzame `SERVER_PUBLIC_HOST` in svoja javna vrata API, igra pa naslov, s katerim je povezana. Oznaka je prvih 22 znakov base64url (brez zapolnitve) vrednosti SHA-256(UTF-8 `"scacelith-sso-origin-v1\n"` + izvor). URI za preusmeritev je `"http://127.0.0.1:" + port + "/oauth2/google/" + tag`: vedno dobesedni naslov IPv4 in vrata poslušalca igre (od 1024 do 65535). Strežnik od odjemalca nikoli ne vzame URI-ja, gostitelja ali poti.

      | Izvor | Oznaka |
      |---|---|
      | `play.scacelith.example:443` | `IhcScoV7eDOzTEcSnqPUPt` |
      | `localhost:8443` | `TFGx7zQ_8QlGZW5zpqznCr` |
      | `[::1]:8443` | `XToJm0DG5PjciEVmZa9Cho` |
      | `127.0.0.1:50443` | `3r653wM5ZjYsHcAJljmCwY` |

      Prijava z Googlom torej deluje samo za igralce, ki so strežnik dodali natanko pod `SERVER_PUBLIC_HOST` in njegovimi javnimi vrati API.

      **Kaj igra preveri, preden odpre brskalnik.** `authUrl` se začne natanko z `https://accounts.google.com/o/oauth2/v2/auth?`, je v natisljivem ASCII in krajši od 4096 znakov, njegova poizvedba pa ima natanko en `response_type=code`, natanko en `redirect_uri`, enak URI-ju, ki ga igra izračuna iz svojih vrat in svoje oznake izvora, natanko en `state`, enak `state` iz odgovora, `code_challenge_method=S256` in 43-znakovni `code_challenge`. Sicer igra ustavi svojega poslušalca in ne odpre ničesar. `finish` in `link` pošlje samo strežniku, ki je odgovoril na `start`.

      **Na spletnem mestu.** Spletno mesto (uradno: <https://scacelith.com/>) se prijavi z odjemalcem vrste »Web application« istega Googlovega projekta (`GOOGLE_WEB_CLIENT_ID`). Google brskalnik preusmeri nazaj na eno stran spletnega mesta, ki jo je nastavil upravljavec (`GOOGLE_WEB_REDIRECT_URI`, navedena med »Authorized redirect URIs« odjemalca), ta stran pa kodo pošlje na `finish`; strežnik tudi tu od odjemalca ne vzame nobenega URI-ja za preusmeritev.

      1. Stran ustvari par PKCE kot zgoraj, preverjevalnik hrani v zavihku (`sessionStorage`) in pošlje `POST /auth/sso/google/web/start` z izzivom.
      2. `authUrl` preveri tako kot igra, le da mora biti njegov `redirect_uri` stran z njenega lastnega izvora, nato pa ga odpre. Noben Googlov skript se ne naloži.
      3. Google brskalnik preusmeri na `GOOGLE_WEB_REDIRECT_URI?code=...&state=...`. Stran sprejme samo `state` iz odgovora na začetek in po preusmeritvi z `error=` ne pošlje ničesar.
      4. `finish`, `link` in `complete` sledijo kot pri igri. Poskus si zapomni, da ga je začelo spletno mesto: koda se zamenja z odjemalcem in stranjo spletnega mesta, žeton ID pa mora navajati ta odjemalec (koda ali žeton ID odjemalca igre ne uspe z 502 `sso_failed`, in obratno).

      Googlov račun iz igre in s spletnega mesta doseže isti račun Scacelith: povezava hrani Googlov subjekt (`sub`), ki določa Googlov račun ne glede na odjemalca. Izvor spletnega mesta mora biti v `CORS_ORIGINS` (sicer `check-config` opozori).

      **Kateri račun doseže prijava z Googlom:** račun, ki je s tem Googlovim računom že povezan; sicer aktiven račun z naslovom, ki ga je potrdil Google (z geslom: `finish` odgovori `needsPassword`, povezava pa se shrani šele, ko uspešno prestaneta preverjanje njegovo geslo in nato, če je vklopljen, njegov drugi faktor; brez gesla: 409 `sso_account_exists`); sicer nov račun (`needsUsername`). Googlov račun se z obstoječim računom nikoli ne poveže samo na podlagi naslova. Na naslov računa je poslano sporočilo, ko prijava z Googlom ustvari račun in ko je dodana obstoječemu računu.
  - name: sessions
    x-displayName: Seje
    description: Prijavljene naprave računa in odjava.
  - name: account
    x-displayName: Račun
    description: Pogled računa, njegove nastavitve in geslo.
  - name: two-step-verification
    x-displayName: Dvofaktorsko preverjanje pristnosti
    description: Aplikacije za preverjanje pristnosti (TOTP, RFC 6238) s SHA-1, 6 števkami, 30-sekundnimi koraki in toleranco enega koraka v vsako smer ter obnovitvene kode za enkratno uporabo.
  - name: email-change
    x-displayName: Sprememba e-poštnega naslova
    description: Sprememba naslova računa, potrjena s povezavo, poslano na novi naslov.
  - name: data-export
    x-displayName: Izvoz podatkov
    description: Vse, kar strežnik hrani o računu, v eni datoteki JSON.
  - name: account-deletion
    x-displayName: Izbris računa
    description: Dokončen izbris računa.
  - name: game-history
    x-displayName: Zgodovina partij
    description: Lastne partije prijavljenega igralca, filtrirane in razdeljene na strani.
  - name: games
    x-displayName: Partije in PGN
    description: |-
      Zapisi partij s potezami in urami ter njihove datoteke PGN.

      **Kode partij.** `status`: 1 `WhiteWins`, 2 `BlackWins`, 3 `Draw`, 4 `Aborted` (shranjene so samo končane partije). `result`: `1-0`, `0-1`, `1/2-1/2` ali `*` (preklicana). `reason` z imenom (`termination`) in besedami, s katerimi se konča zapis potez v datoteki PGN (v datoteki v angleščini, spodaj s prevodom); kode 7, 14 in 21 so remiji (igralec, ki mu je zmanjkalo časa, ki je predal ali ki je zapustil partijo, je imel nasprotnika, ki ni mogel dati mata: FIDE 6.9 in 5.1.2; pravilo navaja `DESIGN.md` 6.3), s kodama 4 in 13 pa strežnik nikoli ne konča partije (spadata na skupni seznam razlogov):

      | Koda | `termination` | Besede v PGN |
      |---|---|---|
      | 1 | `Checkmate` | Checkmate (mat) |
      | 2 | `Resignation` | Resignation (predaja) |
      | 3 | `Timeout` | Loss on time (poraz na čas) |
      | 4 | `IllegalMoves` | Second illegal move (forfeit) (druga nepravilna poteza, izguba partije) |
      | 5 | `Stalemate` | Stalemate (pat) |
      | 6 | `InsufficientMaterial` | Dead position (insufficient material) (mrtva pozicija, nezadosten material) |
      | 7 | `TimeoutVsInsufficient` | Flag fall, but the opponent cannot checkmate (padec zastavice, a nasprotnik ne more dati mata) |
      | 8 | `FivefoldRepetition` | Fivefold repetition (petkratna ponovitev pozicije) |
      | 9 | `SeventyFiveMoves` | 75-move rule (pravilo 75 potez) |
      | 10 | `ThreefoldClaim` | Threefold repetition (claimed) (trikratna ponovitev pozicije, na zahtevo) |
      | 11 | `FiftyMoveClaim` | 50-move rule (claimed) (pravilo 50 potez, na zahtevo) |
      | 12 | `Agreement` | Draw by agreement (remi po dogovoru) |
      | 13 | `IllegalMovesVsInsufficient` | Second illegal move, but the opponent cannot checkmate (druga nepravilna poteza, a nasprotnik ne more dati mata) |
      | 14 | `ResignationVsInsufficient` | Resignation, but the opponent cannot checkmate (predaja, a nasprotnik ne more dati mata) |
      | 20 | `Abandonment` | Abandoned (disconnected for too long) (zapuščena partija, predolgo brez povezave) |
      | 21 | `AbandonmentVsInsufficient` | Abandoned, but the opponent cannot checkmate (zapuščena partija, a nasprotnik ne more dati mata) |
      | 22 | `Aborted` | Game aborted (partija je preklicana) |
      | 23 | `NoShow` | Aborted: first move not played in time (preklicano: prva poteza ni bila pravočasno odigrana) |
      | 24 | `Forfeit` | Forfeit (fair play violation) (poraz zaradi kršitve pravil poštene igre) |
      | 25 | `ServerAborted` | Aborted by the server (partijo je preklical strežnik) |
      | 26 | `BothDisconnected` | Aborted: both players disconnected (preklicano: oba igralca sta izgubila povezavo) |

      Koda 14 je nova v podrazličici 1 protokola: odjemalec, napisan pred njo, naj neznano kodo prikaže kot navaden konec partije z njenim `result`.
  - name: gifs
    x-displayName: Animirani GIF-i
    description: |-
      Partija kot animirani GIF, za shranjevanje ali deljenje: šahovnica, gledana od zgoraj, ena sličica na pozicijo od začetne do končne pozicije, imena in ratingi igralcev nad šahovnico, zadnja poteza pod njo, na zadnji sličici pa rezultat in način, kako se je partija končala.

      **Strošek, predpomnilnik in kvote.** GIF se izdela v lastni niti za upodabljanje, nikoli v nitih, ki izvajajo partije, in z najnižjo prioriteto CPE: `GIF_THREADS` niti (privzeto `WORKERS`), ki se zaženejo s prvim GIF-om in ustavijo po minuti brez njega. Do `GIF_QUEUE_MAX` (4 x `WORKERS`) GIF-ov čaka na nit, vsak največ `GIF_QUEUE_TIMEOUT_MS` (10 s); upodabljanje lahko traja `GIF_RENDER_TIMEOUT_MS` (30 s). Strežnik izdelane GIF-e hrani v predpomnilniku velikosti `GIF_CACHE_MB` MB (32 x `WORKERS`), pri čemer se najprej odstranijo najdlje neuporabljeni; GIF iz predpomnilnika ali GIF, ki se že izdeluje za drugo zahtevo, ne porabi upodabljanja. Ključ predpomnilnika zajema vse, kar spremeni sliko, vključno z imeni.

      Vsaka zahteva se šteje v `gif` (30 na minuto na igralca za obe končni točki). GIF, ki ga je treba izdelati, se šteje tudi v omejitve upodabljanja: na igralca `GIF_USER_RENDERS_PER_MIN` (4) na minuto in `GIF_USER_RENDERS_PER_HOUR` (30) na uro; na odjemalca, za vse njegove račune skupaj, `GIF_IP_RENDERS_PER_MIN` (12) na minuto in `GIF_IP_RENDERS_PER_HOUR` (120) na uro ter trikrat toliko na IPv6 /48. Odjemalec naj preneseno datoteko raje obdrži, kot da jo znova zahteva, po 429 ali 503 pa naj počaka `retryAfter`.

      Velikosti: `small` (polja po 32 px: 284 x 350 slikovnih pik), `medium` (48 px: 424 x 515), `large` (72 px: 628 x 762); brez koordinat 268 x 342, 400 x 503 in 600 x 748. Začetna pozicija je prikazana vsaj 1 s (oziroma toliko, kolikor znaša zamik, če je daljši), končna pozicija 3 s, GIF pa se ponavlja; po prvi sličici se shrani samo del slike, ki se spremeni. Partija s 40 potezami zavzame približno 135, 205 in 325 KiB (small, medium, large), partija s 150 potezami pa 0,5 MiB, 0,8 MiB in 1,2 MiB.
  - name: players
    x-displayName: Igralci
    description: Javni profili in nedavne partije. Samo javni podatki, nikoli e-poštni naslov, seja, sankcija ali raven integritete. Izbrisan račun nima profila.
  - name: leaderboard
    x-displayName: Lestvica
    description: Najboljši igralci vsake uradne kategorije.
  - name: reports
    x-displayName: Prijave kršitev
    description: Prijava nasprotnika iz nedavne partije moderatorjem.
  - name: pages
    x-displayName: Strani HTML
    description: |-
      Strani za brskalnik, ki se odprejo s povezavami v e-poštnih sporočilih. Njihove povezave kažejo na `https://<SERVER_PUBLIC_HOST>` (z `:<PUBLIC_API_PORT>`, če ta ni 443), zunaj `/api/v1`.

      - Strani ne izvajajo JavaScripta in ne nalagajo zunanjih virov. Strežejo se z `Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'`.
      - `GET` samo prikaže gumb ali obrazec, tako da pregledovalnik e-pošte, ki odpre povezavo, te ne porabi. Sprememba se zgodi ob `POST`: obrazec, poslan kot `application/x-www-form-urlencoded` (sprejeto je tudi telo JSON), z žetonom povezave v skritem polju. Polje, podano dvakrat, je zavrnjeno.
      - Tudi napake (omejitve števila zahtev, neveljavna polja) so strani HTML, z naslovom »Request refused« (zahteva zavrnjena) ali, od 500 naprej, »Server error« (napaka strežnika). V JSON odgovorijo samo napake, odkrite, preden je stran prepoznana (predolg ali napačno oblikovan cilj zahteve, sloj po naslovu).
x-tagGroups:
  - name: server
    x-displayName: Strežnik
    tags:
      - server-info
      - health
  - name: accounts
    x-displayName: Računi
    tags:
      - auth
      - google-sign-in
      - sessions
      - account
      - two-step-verification
      - email-change
      - data-export
      - account-deletion
  - name: games
    x-displayName: Partije
    tags:
      - game-history
      - games
      - gifs
  - name: community
    x-displayName: Igralci in prijave kršitev
    tags:
      - players
      - leaderboard
      - reports
  - name: browser-pages
    x-displayName: Strani za brskalnik
    tags:
      - pages
paths:
  /info:
    get:
      operationId: getServerInfo
      tags:
        - server-info
      summary: Pridobitev imena strežnika, različic, vrat in pravil registracije
      description: |-
        Kar odjemalec potrebuje, preden se prijavi ali poveže: ime in ID strežnika, različice protokola WebSocket in kje je WebSocket, pravila registracije, uradne tempe igre in omejitve, ki jih lahko odjemalec preveri, preden pošlje obrazec.

        **Omejitve:** samo sloj po naslovu.
      security: []
      responses:
        '200':
          description: Opis strežnika.
          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`: sloj po naslovu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /auth/register:
    post:
      operationId: registerAccount
      tags:
        - auth
      summary: Ustvarjanje računa
      description: |-
        Ustvari račun: ko je njegov e-poštni naslov potrjen s povezavo, poslano nanj, ali takoj, če potrditev e-pošte ni vklopljena.

        **S potrditvijo e-pošte** (`REQUIRE_EMAIL_VERIFICATION`, privzeto): 202 `verification_sent`. Račun še ne obstaja: registracija čaka 24 ur in medtem zadrži svoje uporabniško ime. Na naslov se pošlje povezava, veljavna teh 24 ur, največ ena na naslov vsakih 5 minut (`POST /auth/verify-email/resend` pošlje novo); račun se ustvari, z že potrjenim naslovom, ko je povezava uporabljena (gumb na strani `/verify-email`), nato se igralec lahko prijavi. Do takrat je odgovor na prijavo s tem uporabniškim imenom `invalid_credentials`, tako kot za neznan račun, javni profil pa ne obstaja. Nova registracija z istim naslovom nadomesti čakajočo. Odgovor je enak, kadar naslov že uporablja drug račun: povezava se takrat ne pošlje, temveč lastnik tega računa dobi obvestilo (največ eno na uro), uporabniško ime pa je zadržano na enak način, tako da nič ne razkrije, ali ima naslov račun. Registracija, katere povezava ni bila uporabljena, se po 24 urah zavrže, njeno uporabniško ime pa je spet prosto.

        **Brez potrditve e-pošte** (`REQUIRE_EMAIL_VERIFICATION=false`): 201 `ready`, račun je ustvarjen takoj in z njim se je mogoče prijaviti.

        **Pravila.** `username`: od `USERNAME_MIN` do `USERNAME_MAX` znakov (od 3 do 20), črke, števke, `_` in `-`, na začetku črka ali števka (`GET /info` jih vrne kot `limits`); rezervirana imena (`admin`, `moderator`, `deleted` ...) in nekatere predpone so zavrnjeni; enolično ne glede na velike in male črke. `email`: brez začetnih in končnih presledkov in shranjen z malimi črkami, navaden ASCII z domeno, ki vsebuje piko. `password`: najmanj `PASSWORD_MIN_LENGTH` (10) znakov in največ 256 bajtov UTF-8; ne sme vsebovati uporabniškega imena ali lokalnega dela e-poštnega naslova in ne sme biti pogosto geslo.

        **Napake**, preverjene v tem vrstnem redu: 403 `registration_closed`; 400 `invalid_username`; 400 `invalid_email`; 400 `weak_password`; 409 `username_taken`; 428 `pow_required`; napake čakalne vrste za zgoščevanje gesel; 409 `email_taken` (samo brez potrditve e-pošte).

        **Omejitve:** `auth`, nato `auth_register` (10 registracij na uro na odjemalca). **Dokaz o delu:** vedno, kadar je `POW_REGISTER_BITS` večji od 0.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
            examples:
              first:
                summary: Prvi poskus, brez dokaza o delu
                value:
                  username: alice
                  email: alice@example.org
                  password: correct horse battery
              withPow:
                summary: Ponovno poslano z dokazom o delu
                value:
                  username: alice
                  email: alice@example.org
                  password: correct horse battery
                  pow:
                    challenge: eyJ2IjoxLCJlIjoicmVnaXN0ZXIiLCJiIjoxOCwieCI6MTc5MDg4Mjk5MTIwMH0.6Ku_D7kw3S69OYxj-9KxsXszRCwz6ECnOQ-B6-v2erM
                    nonce: '123456'
      responses:
        '201':
          description: Račun je ustvarjen in z njim se je mogoče prijaviti (na tem strežniku ni potrditve e-pošte).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    const: ready
              example:
                status: ready
        '202':
          description: Registracija čaka na svojo potrditveno povezavo (ali pa ima naslov že račun; odgovor tega ne razkrije).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerificationSentStatus'
              example:
                status: verification_sent
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_request` (s `field`), `invalid_json`, `invalid_username`, `invalid_email` ali `weak_password` z `reason`: `too_short`, `too_long`, `contains_username`, `contains_email` ali `too_common`.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`registration_closed`: registracija je na tem strežniku zaprta (`GET /info` vrne `registration: closed`).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`username_taken`: uporabniško ime že ima neki račun ali pa ga zadržuje čakajoča registracija z drugim naslovom. `email_taken`: naslov uporablja drug račun (samo brez potrditve e-pošte).'
        '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`: omejitev `auth` ali `auth_register` ali čakalna vrsta za zgoščevanje gesel.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (čakalna vrsta za zgoščevanje gesel ali pa je zbirka podatkov ostala zaklenjena), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /auth/login:
    post:
      operationId: logIn
      tags:
        - auth
      summary: Prijava z geslom
      description: |-
        Prijava z uporabniškim imenom ali e-poštnim naslovom in geslom. Možna sta dva odgovora, oba s statusom 200: seja ali, kadar je dvofaktorsko preverjanje pristnosti vklopljeno, drugi korak, ki ga je treba v 5 minutah dokončati s `POST /auth/login/mfa`.

        Neznan račun, napačno geslo in račun brez gesla dobijo enak odgovor po enakem času: 401 `invalid_credentials`. Preverjanja računa (`banned`, `email_unverified`) sledijo šele po pravilnem geslu. Od `AUTH_FAILURES_PER_ACCOUNT` (5) neuspehov z istim prijavnim imenom naprej mora vsak poskus počakati (429 `too_many_attempts`).

        **Omejitev:** `auth`. **Dokaz o delu:** samo med valom neuspelih prijav (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: Seja ali drugi korak prijave z dvofaktorskim preverjanjem pristnosti.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginAnswer'
              examples:
                session:
                  summary: Prijava je uspela
                  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: Dvofaktorsko preverjanje pristnosti je vklopljeno
                  value:
                    mfaRequired: true
                    mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                    expiresIn: 300
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_credentials`: neznan račun, napačno geslo ali račun brez gesla.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: 'Samo po pravilnem geslu: `banned` z `until` (ms od epohe Unix, `null` za trajno blokado) ali `email_unverified` (starejši račun, katerega naslov še ni potrjen).'
        '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` (zamik po neuspelih poskusih za to prijavno ime) ali `rate_limited` (omejitev `auth`, čakalna vrsta za zgoščevanje gesel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (čakalna vrsta za zgoščevanje gesel ali pa je zbirka podatkov ostala zaklenjena), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /auth/login/mfa:
    post:
      operationId: logInWithSecondFactor
      tags:
        - auth
      summary: Dokončanje prijave z drugim faktorjem
      description: |-
        Drugi korak prijave z dvofaktorskim preverjanjem pristnosti, potem ko je bil odgovor na `POST /auth/login` ali na prijavo z Googlom (`POST /auth/sso/google/finish` ali `POST /auth/sso/google/link`) `mfaRequired`. Pošljite `code` (6-mestno kodo iz aplikacije ali obnovitveno kodo) ali `recoveryCode`. Obnovitvena koda, uporabljena tukaj, je porabljena.

        En korak sprejme največ 5 napačnih kod; korak se konča tudi, ko je geslo ponastavljeno ali spremenjeno. Po koraku z geslom pri povezovanju z Googlom se povezava shrani šele, ko koda tukaj uspešno prestane preverjanje.

        **Omejitve:** `auth` in največ `AUTH_MFA_PER_ACCOUNT` (10) kod na 15 minut za račun, s katerega koli naslova; nad tem se koda ne preveri, zato se obnovitvena koda ne porabi.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MfaLoginRequest'
            examples:
              authenticator:
                summary: Koda iz aplikacije
                value:
                  mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                  code: '123456'
              recovery:
                summary: Obnovitvena koda
                value:
                  mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                  recoveryCode: j7v5-3ezx-zn
      responses:
        '200':
          description: Prijava je uspela.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionAnswer'
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_request`: poslana ni bila niti `code` niti `recoveryCode` ali pa telo krši shemo; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_mfa_token`: korak je potekel, je bil uporabljen ali se je končal po 5 napačnih kodah ali pa je bilo geslo po prvem koraku ponastavljeno ali spremenjeno (znova se prijavite). `invalid_code`: napačna ali že uporabljena koda.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`banned` (z `until`), `email_unverified`.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_already_linked`: samo po koraku z geslom pri povezovanju z Googlom, kadar je bil Googlov račun medtem povezan z drugim računom.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: samo po koraku z geslom pri povezovanju z Googlom, kadar se je račun medtem spremenil; prijavo začnite znova.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts`: zamik po neuspelih poskusih računa ali pa je njegovih `AUTH_MFA_PER_ACCOUNT` kod v zadnjih 15 minutah porabljenih. `rate_limited`: omejitev `auth`.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` z `retryAfter: 1`: zbirka podatkov je ostala zaklenjena (po koraku povezovanja z Googlom ni bilo nič povezano: prijavo začnite znova). `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /auth/logout:
    post:
      operationId: logOut
      tags:
        - sessions
      summary: Odjava te seje
      description: |-
        Prekliče sejo uporabljenega žetona. WebSocket, odprt z njim, se zapre. Brez telesa (ali `{}`).

        **Omejitev:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Odjava je uspela.
          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`: telo, ki ni `{}`; `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`: omejitev `sessions` ali kvota računa.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (zbirka podatkov je ostala zaklenjena), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /auth/logout-all:
    post:
      operationId: logOutEverywhere
      tags:
        - sessions
      summary: Odjava vseh sej
      description: |-
        Prekliče vse seje računa, vključno s to. WebSocketi, odprti z njimi, se zaprejo. Brez telesa (ali `{}`).

        **Omejitev:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Vse seje so odjavljene.
          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`: telo, ki ni `{}`; `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`: omejitev `sessions` ali kvota računa.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (zbirka podatkov je ostala zaklenjena), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /auth/verify-email/resend:
    post:
      operationId: resendVerificationEmail
      tags:
        - auth
      summary: Ponovno pošiljanje potrditvene povezave
      description: |-
        Znova pošlje povezavo za potrditev e-poštnega naslova. Odgovor je 202 `accepted` ne glede na naslov, tako da nikoli ne razkrije, ali ga uporablja račun ali registracija.

        Zahteva učinkuje največ enkrat vsakih 5 minut na naslov. Registracija, ki čaka s tem naslovom, znova dobi svojih 24 ur, ne glede na to, ali naslov uporablja drug račun, tako da njeno uporabniško ime v obeh primerih ostane zadržano enako dolgo. Povezava se pošlje samo za to registracijo, kadar naslov nima računa (nova povezava, veljavna 24 ur, nadomesti prejšnjo), ali za aktiven, nepotrjen račun s tem naslovom.

        **Omejitve:** `auth`, nato `auth_mail` (10 na uro na odjemalca).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmailRequest'
            example:
              email: alice@example.org
      responses:
        '202':
          description: Sprejeto (ne glede na naslov).
          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`: omejitev `auth` ali `auth_mail` (ne glede na naslov).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` z `retryAfter: 1`: zbirka podatkov je ostala zaklenjena med iskanjem računa ali shranjevanjem povezave nepotrjenega računa (podaljšanje čakajoče registracije se izvede po najboljših močeh in zaradi njega zahteva nikoli ne spodleti); 5 minut za naslov se kljub temu začne šteti, zato nova zahteva v tem času ne naredi ničesar. `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /auth/password/forgot:
    post:
      operationId: requestPasswordReset
      tags:
        - auth
      summary: Pošiljanje povezave za ponastavitev gesla
      description: |-
        Pošlje povezavo za ponastavitev gesla, veljavno eno uro, ki odpre stran `/reset-password`. Odgovor je 202 `accepted` ne glede na naslov. Povezava se pošlje samo aktivnemu računu, največ enkrat vsakih 5 minut na naslov. Račun samo s prijavo z Googlom na ta način nastavi svoje prvo geslo.

        Obnovitev gesla ima najstrožje omejitve v API-ju, vse štete za celoten strežnik: 3 zahteve na uro (`AUTH_FORGOT_PER_HOUR`) in 10 na 24 ur (`AUTH_FORGOT_PER_DAY`) na odjemalca (naslov IPv4 ali IPv6 /64), trikrat toliko na IPv6 /48, poleg omejitve `auth` (20 na 10 minut) in enega sporočila na naslov vsakih 5 minut. Niti 202 niti 429 ne razkrije, ali naslov uporablja kateri koli račun. Nastavitev novega gesla ima lastno omejitev (`auth_reset`).

        **Omejitve:** `auth`, `auth_forgot`, nato `auth_forgot_day`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmailRequest'
            example:
              email: alice@example.org
      responses:
        '202':
          description: Sprejeto (ne glede na naslov).
          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`: omejitev `auth`, `auth_forgot` ali `auth_forgot_day` (ne glede na naslov).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` z `retryAfter: 1`: zbirka podatkov je ostala zaklenjena med iskanjem računa ali shranjevanjem povezave; 5 minut za naslov se kljub temu začne šteti, zato nova zahteva v tem času ne pošlje povezave. `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /auth/password/reset:
    post:
      operationId: resetPassword
      tags:
        - auth
      summary: Nastavitev novega gesla z žetonom povezave za ponastavitev
      description: |-
        Nastavi novo geslo z žetonom povezave za ponastavitev gesla (stran `/reset-password` naredi enako). Za novo geslo veljajo pravila registracije.

        Ponastavitev ima te učinke, v eni transakciji (vse ali nobenega): prekliče vse seje in razveljavi čakajočo spremembo e-poštnega naslova; druge povezave za ponastavitev gesla tega računa prenehajo delovati; naslov šteje kot potrjen (povezava je to dokazala); lastnik dobi e-poštno sporočilo. Dvofaktorsko preverjanje pristnosti ostane nespremenjeno. Po napaki čakalne vrste za zgoščevanje gesel, 503 ali 500 ni bilo nič spremenjeno: staro geslo in seje ostanejo, povezava pa ostane veljavna.

        **Omejitve:** `auth`, nato `auth_reset` (10 na uro na odjemalca, 30 na IPv6 /48, deljena s stranjo: vsak poskus zgosti geslo).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasswordResetRequest'
            example:
              token: A0ATfmXCDKFC8UkSxLJCD85K7au5nMIrZ2P3K-1VWQg
              newPassword: a much better passphrase
      responses:
        '200':
          description: Geslo je spremenjeno in vse seje so odjavljene.
          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`: povezava je neveljavna, uporabljena ali potekla ali pa je bila poslana na naslov, ki ga račun nima več. `weak_password` (z `reason`). `invalid_request`, `invalid_json`.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: omejitev `auth` ali `auth_reset` ali čakalna vrsta za zgoščevanje gesel.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: čakalna vrsta za zgoščevanje gesel ali pa je zbirka podatkov ostala zaklenjena (`retryAfter: 1`, nič ni bilo spremenjeno). `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /auth/sso/google/start:
    post:
      operationId: startGoogleSignIn
      tags:
        - google-sign-in
      summary: Začetek prijave z Googlom
      description: |-
        Začne poskus prijave z Googlom za izziv PKCE in vrata poslušalca igre na `127.0.0.1`. Odgovor vrne Googlov URL, ki ga je treba odpreti v sistemskem brskalniku, in `state` poskusa; poskus velja 10 minut.

        `authUrl` vsebuje `client_id`, `redirect_uri` (sestavljen iz `redirectPort` in oznake izvora strežnika), `response_type=code`, `scope=openid email profile`, `state`, `nonce`, `code_challenge` (S256 strežnikovega lastnega preverjevalnika za Google) s `code_challenge_method=S256` in `prompt=select_account`. Igra ga preveri, preden ga odpre (glejte opis razdelka).

        **Omejitev:** `sso_start`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SsoStartRequest'
            example:
              codeChallenge: 6e7diXEYxG7OTYw7STfNOltEeLxAilPthC_txzaE0xA
              redirectPort: 51234
      responses:
        '200':
          description: Poskus.
          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`: prijava z Googlom na tem strežniku ni na voljo.'
        '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`: omejitev `sso_start`.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (zbirka podatkov je ostala zaklenjena), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /auth/sso/google/web/start:
    post:
      operationId: startGoogleWebSignIn
      tags:
        - google-sign-in
      summary: Začetek prijave z Googlom na spletnem mestu
      description: |-
        Začne poskus prijave z Googlom za spletno mesto, za izziv PKCE njegove strani. Odgovor je enak kot za igro: Googlov URL, ki ga je treba odpreti, in `state` poskusa; poskus velja 10 minut. Na voljo, kadar `GET /info` vrne `sso.googleWeb: true`.

        `authUrl` vsebuje `client_id` spletnega mesta in `redirect_uri=GOOGLE_WEB_REDIRECT_URI`, z enakimi drugimi parametri kot za igro. Stran ga preveri, preden ga odpre (glejte **Na spletnem mestu** v opisu razdelka).

        **Omejitev:** `sso_start`, deljena z začetkom prijave v igri.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SsoWebStartRequest'
            example:
              codeChallenge: 6e7diXEYxG7OTYw7STfNOltEeLxAilPthC_txzaE0xA
      responses:
        '200':
          description: Poskus.
          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`: manjkajoč ali napačno oblikovan `codeChallenge` ali katero koli drugo polje (vključno z `redirectPort`).'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: prijava z Googlom na tem strežniku ni na voljo ali pa ni na voljo spletnim mestom.'
        '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`: omejitev `sso_start` (deljena z začetkom prijave v igri).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (zbirka podatkov je ostala zaklenjena), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /auth/sso/google/finish:
    post:
      operationId: finishGoogleSignIn
      tags:
        - google-sign-in
      summary: Predaja Googlovega odgovora strežniku
      description: |-
        Strežniku preda `code` in `state`, ki ju je Google poslal poslušalcu igre ali strani spletnega mesta, skupaj z ID-jem poskusa in preverjevalnikom PKCE. ID poskusa je brez preverjevalnika neuporaben, koda pa je neuporabna brez strežnikovega lastnega preverjevalnika PKCE in skrivnosti odjemalca.

        Strežnik preveri poskus (neznan, uporabljen ali potekel: 410), nato preverjevalnik (napačen pusti poskus uporaben), nato poskus enkrat uporabi, preveri `state` in `iss`, zamenja kodo pri Googlu in preveri žeton ID. Odgovor (200) je eden od naslednjih:

        - `{ token, expiresAt, user }`: prijava v povezani račun je uspela;
        - `{ mfaRequired, mfaToken, expiresIn }`: nadaljujte s `POST /auth/login/mfa`;
        - `{ needsUsername, ssoTicket, suggestedUsername }`: nov račun; v 10 minutah nadaljujte s `POST /auth/sso/complete`. `suggestedUsername` izhaja iz Googlovega imena ali naslova in je `""`, kadar nič ne ustreza;
        - `{ needsPassword, linkTicket, username, expiresIn }`: račun s tem naslovom ima geslo; nadaljujte s `POST /auth/sso/google/link`. Nič še ni povezano.

        **Omejitev:** `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: Seja, drugi korak ali naslednji korak prve prijave z Googlom.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SsoFinishAnswer'
              examples:
                session:
                  summary: Prijava v povezani račun je uspela
                  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: Dvofaktorsko preverjanje pristnosti je vklopljeno
                  value:
                    mfaRequired: true
                    mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                    expiresIn: 300
                needsUsername:
                  summary: Nov igralec
                  value:
                    needsUsername: true
                    ssoTicket: sso_gYIKKhwnz1AqWfFxOC3R-ZvCACw06q74fEMtGEOJQWU
                    suggestedUsername: alice
                needsPassword:
                  summary: Naslov uporablja račun z geslom
                  value:
                    needsPassword: true
                    linkTicket: sso_ZfEI6iZjePTHbnbevupaD9O7GjBCzcL-GJu2XGOdFGc
                    username: alice
                    expiresIn: 600
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_verifier`: preverjevalnik se ne ujema z izzivom poskusa (poskus ostane uporaben). `sso_email_unverified`: Google ni potrdil naslova. `registration_closed`. `account_disabled`. Samo za povezan račun: `banned` (z `until`), `email_unverified`.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: prijava z Googlom na tem strežniku ni na voljo.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_account_exists`: naslov uporablja aktiven račun brez gesla (ta ni imenovan).'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: neznan, uporabljen ali potekel poskus.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: omejitev `sso_finish`.'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/BadGateway'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (zbirka podatkov je ostala zaklenjena), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /auth/sso/google/link:
    post:
      operationId: linkGoogleAccount
      tags:
        - google-sign-in
      summary: Povezovanje Googla z obstoječim računom z geslom tega računa
      description: |-
        Potem ko je `finish` odgovoril z `needsPassword`: poveže Google z obstoječim računom z geslom tega računa, vnesenim v igri. Odgovor (200) je seja (povezava je shranjena in igralec prijavljen) ali, kadar je dvofaktorsko preverjanje pristnosti vklopljeno, `{ mfaRequired, mfaToken, expiresIn }`: nadaljujte s `POST /auth/login/mfa`, povezava pa se shrani šele, ko tam koda uspešno prestane preverjanje.

        Napačno geslo pusti vstopnico uporabno, skupno za 5 poskusov. Poskus se porabi tik pred preverjanjem gesla: 429 `too_many_attempts` ali 428 `pow_required` ne porabi nobenega, zavrnitev čakalne vrste za zgoščevanje gesel (503 `server_busy`, 429 `rate_limited`) pa je enega že porabila in šteje kot neuspeh računa. Zamik po neuspelih poskusih uporablja isti števec kot `POST /auth/login`.

        **Omejitev:** `auth` (s štetjem po IPv6 /48). **Dokaz o delu:** kot pri `POST /auth/login`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SsoLinkRequest'
            example:
              linkTicket: sso_ZfEI6iZjePTHbnbevupaD9O7GjBCzcL-GJu2XGOdFGc
              password: correct horse battery
              clientLabel: Scacelith 1.4 (Windows)
      responses:
        '200':
          description: Seja ali drugi korak prijave.
          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`: napačno geslo; vstopnica ostane veljavna za skupno 5 poskusov.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`banned` (z `until`), samo po pravilnem geslu.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: ta strežnik ne ponuja prijave z Googlom.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_already_linked`: Googlov račun je bil medtem povezan z drugim računom.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: neznana, že uporabljena ali potekla vstopnica, peto napačno geslo, račun, katerega stanje ali naslov se je spremenil po `finish`, ali račun, katerega stanje, naslov, geslo ali dvofaktorsko preverjanje pristnosti se je spremenilo med shranjevanjem povezave. Znova začnite prijavo.'
        '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` (zamik po neuspelih poskusih računa) ali `rate_limited` (omejitev `auth`, čakalna vrsta za zgoščevanje gesel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: čakalna vrsta za zgoščevanje gesel ali pa je zbirka podatkov ostala zaklenjena (`retryAfter: 1`, nič ni bilo povezano). `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /auth/sso/complete:
    post:
      operationId: completeGoogleSignUp
      tags:
        - google-sign-in
      summary: Ustvarjanje računa ob prvi prijavi z Googlom
      description: |-
        Ko `finish` odgovori z `needsUsername`: ustvari račun z izbranim uporabniškim imenom (veljajo pravila registracije) in ga prijavi. Račun nima gesla: `hasPassword` je `false`, geslo pa mu nastavi »Pozabljeno geslo?« (`POST /auth/password/forgot`).

        **Omejitev:** `auth` (s štetjem po IPv6 /48).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SsoCompleteRequest'
            example:
              ssoTicket: sso_gYIKKhwnz1AqWfFxOC3R-ZvCACw06q74fEMtGEOJQWU
              username: alice
              clientLabel: Scacelith 1.4 (Windows)
      responses:
        '200':
          description: Račun je ustvarjen in prijavljen.
          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`: ta strežnik ne ponuja prijave z Googlom.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`username_taken` (uporabniško ime že ima neki račun ali pa ga zaseda registracija na čakanju z drugim naslovom), `sso_already_linked`, `email_taken`.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: neznana, že uporabljena ali potekla vstopnica.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: omejitev `auth`.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (zbirka podatkov je ostala zaklenjena), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /auth/sessions:
    get:
      operationId: listSessions
      tags:
        - sessions
      summary: Seznam prijavljenih naprav
      description: |-
        Aktivne seje računa (prijavljene naprave), od nazadnje uporabljene naprej. `current` označuje sejo, ki pošilja zahtevo. `lastSeenAt` se posodobi največ na vsakih 5 minut (pozneje, dokler strežnik močno zaostaja); `expiresAt` je absolutni konec, omejitev nedejavnosti pa lahko sejo konča prej.

        **Omejitev:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Aktivne seje.
          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`: omejitev `sessions` ali kvota računa.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (zbirka podatkov je ostala zaklenjena), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /auth/sessions/{id}:
    delete:
      operationId: revokeSession
      tags:
        - sessions
      summary: Odjava ene naprave
      description: |-
        Odjavi eno sejo računa; odjaviti je mogoče tudi trenutno sejo. WebSocket, odprt z njo, se zapre. Brez telesa (ali `{}`).

        **Omejitev:** `sessions`.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/SessionId'
      responses:
        '200':
          description: Seja je odjavljena.
          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`: telo, ki ni `{}`, ali `id`, ki ni veljavno kodiran za URL; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: ta račun nima aktivne seje s tem ID-jem.'
        '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`: omejitev `sessions` ali kvota računa.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (zbirka podatkov je ostala zaklenjena), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /account/me:
    get:
      operationId: getAccount
      tags:
        - account
      summary: Pridobivanje računa, njegovih ratingov in aktivnih sankcij
      description: |-
        Račun, kot ga vidi njegov igralec: pogled računa (tudi `user` v vsakem odgovoru na prijavo), en zapis ratinga za vsako kategorijo, v kateri je igralec igral partije za rating, aktivne sankcije in blokada. Raven integritete sistema proti goljufanju ni nikoli prikazana.

        **Omejitev:** `account`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Račun.
          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` ali `invalid_token` (tudi kadar je bil račun izbrisan).'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: omejitev `account` ali kvota računa.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (zbirka podatkov je ostala zaklenjena), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /account/preferences:
    put:
      operationId: updatePreferences
      tags:
        - account
      summary: Sprejemanje ali zavračanje neposrednih izzivov
      description: |-
        Nastavi, ali lahko drugi igralci tega igralca izzovejo po imenu. Z `none` so neposredni izzivi zavrnjeni: izzivalec izve, da igralec ni na voljo. Brez ponovnega preverjanja pristnosti.

        **Omejitev:** `account`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Preferences'
            example:
              acceptChallenges: none
      responses:
        '200':
          description: Nastavitve, ki zdaj veljajo.
          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`: omejitev `account` ali kvota računa.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (zbirka podatkov je ostala zaklenjena), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /account/password:
    post:
      operationId: changePassword
      tags:
        - account
      summary: Sprememba gesla
      description: |-
        Spremeni geslo. Potrebno je trenutno geslo, drugi faktor pa ne, tudi kadar je dvofaktorsko preverjanje pristnosti vklopljeno. Za novo geslo veljajo pravila registracije (preverijo se po trenutnem geslu).

        Sprememba ima v eni transakciji (vse ali nobenega) te učinke: prekliče vse druge seje (ta ostane prijavljena), prekliče čakajočo spremembo e-poštnega naslova, povezave za ponastavitev gesla tega računa prenehajo delovati, lastniku pa se pošlje e-poštno sporočilo.

        **Ponovno preverjanje pristnosti:** samo geslo. **Omejitve:** `reauth`, nato `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: Geslo je spremenjeno.
          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` (račun samo s prijavo z Googlom), `weak_password` (z `reason`), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password`: napačno trenutno geslo ali ponastavitev oziroma sprememba gesla, ki je bila izvedena med preverjanjem zahteve.'
        '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` (neuspela ponovna preverjanja pristnosti računa) ali `rate_limited` (omejitev `reauth` ali `reauth_user`, kvota računa, čakalna vrsta za zgoščevanje gesel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: čakalna vrsta za zgoščevanje gesel ali pa je zbirka podatkov ostala zaklenjena (`retryAfter: 1`, nič ni bilo spremenjeno). `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /account/mfa/totp/setup:
    post:
      operationId: startTotpSetup
      tags:
        - two-step-verification
      summary: Začetek vklopa dvofaktorskega preverjanja pristnosti
      description: |-
        Shrani nov čakajoči skrivni ključ, ki nadomesti morebitni prejšnji čakajoči ključ, in ga vrne za aplikacijo za preverjanje pristnosti (kot besedilo in kot URI `otpauth://`, ki ga prikažete kot kodo QR). Dvofaktorsko preverjanje pristnosti še ni vklopljeno: vklopi ga `POST /account/mfa/totp/enable` s kodo tega skrivnega ključa.

        **Ponovno preverjanje pristnosti:** samo geslo. **Omejitve:** `reauth`, nato `reauth_user`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasswordOnlyRequest'
            example:
              password: correct horse battery
      responses:
        '200':
          description: Čakajoči skrivni ključ.
          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` (račun samo s prijavo z Googlom), `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` (preveri se pred geslom).'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (neuspela ponovna preverjanja pristnosti računa) ali `rate_limited` (omejitev `reauth` ali `reauth_user`, kvota računa, čakalna vrsta za zgoščevanje gesel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (čakalna vrsta za zgoščevanje gesel ali pa je zbirka podatkov ostala zaklenjena), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /account/mfa/totp/enable:
    post:
      operationId: enableTotp
      tags:
        - two-step-verification
      summary: Dokončanje vklopa dvofaktorskega preverjanja pristnosti in pridobitev obnovitvenih kod
      description: |-
        Vklopi dvofaktorsko preverjanje pristnosti s kodo čakajočega skrivnega ključa (natanko 6 števk). Gesla tu ne zahteva; vneseno je bilo pri nastavitvi. Odgovor vsebuje 10 obnovitvenih kod, ki so prikazane samo tokrat.

        **Omejitve:** `reauth`, nato `reauth_user`; napačna koda se šteje med neuspela ponovna preverjanja pristnosti računa.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TotpEnableRequest'
            example:
              code: '123456'
      responses:
        '200':
          description: Dvofaktorsko preverjanje pristnosti je vklopljeno.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                  - recoveryCodes
                properties:
                  status:
                    const: mfa_enabled
                  recoveryCodes:
                    $ref: '#/components/schemas/RecoveryCodeList'
              example:
                status: mfa_enabled
                recoveryCodes:
                  - j7v5-3ezx-zn
                  - 4kqm-8w2p-hd
                  - x0ra-c6tn-5g
                  - mb3s-9yzj-k1
                  - 2hvd-pq7e-w8
                  - c5ng-0tka-xr
                  - zz4y-b1me-7q
                  - 8pjh-6dsw-3n
                  - t9xc-2gfk-0v
                  - q6wa-ner5-jy
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_request`: `code` nima natanko 6 števk ali pa telo krši shemo; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_code`: napačna koda (preverite čas naprave).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`mfa_already_enabled`; `mfa_setup_required`: ni čakajočega skrivnega ključa (najprej pokličite `POST /account/mfa/totp/setup`).'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (neuspela ponovna preverjanja pristnosti računa) ali `rate_limited` (omejitev `reauth` ali `reauth_user`, kvota računa).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (zbirka podatkov je ostala zaklenjena), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /account/mfa/totp/disable:
    post:
      operationId: disableTotp
      tags:
        - two-step-verification
      summary: Izklop dvofaktorskega preverjanja pristnosti
      description: |-
        Izklopi dvofaktorsko preverjanje pristnosti. Skrivni ključ in obnovitvene kode se izbrišejo, lastnik pa prejme e-poštno sporočilo.

        **Ponovno preverjanje pristnosti:** geslo in koda iz aplikacije ali obnovitvena koda (obvezen je eden od `code` ali `recoveryCode`). **Omejitve:** `reauth`, nato `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: Dvofaktorsko preverjanje pristnosti je izklopljeno.
          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` (račun samo s prijavo z Googlom), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`mfa_code_required` (niti `code` niti `recoveryCode`, preveri se pred geslom), `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` (neuspela ponovna preverjanja pristnosti ali preveč preizkušenih kod na tem računu) ali `rate_limited` (omejitev `reauth` ali `reauth_user`, kvota računa, čakalna vrsta za zgoščevanje gesel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (čakalna vrsta za zgoščevanje gesel ali pa je zbirka podatkov ostala zaklenjena), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /account/mfa/recovery-codes:
    post:
      operationId: regenerateRecoveryCodes
      tags:
        - two-step-verification
      summary: Zamenjava obnovitvenih kod
      description: |-
        Zamenja obnovitvene kode z 10 novimi; stare prenehajo delovati.

        **Ponovno preverjanje pristnosti:** geslo in koda iz aplikacije v `code` (obnovitvena koda je zavrnjena s 403 `invalid_code`). **Omejitve:** `reauth`, nato `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: Nove obnovitvene kode, prikazane samo tokrat.
          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` (račun samo s prijavo z Googlom), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password`, `mfa_code_required`, `invalid_code` (tudi za obnovitveno kodo).'
        '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` (neuspela ponovna preverjanja pristnosti ali preveč preizkušenih kod na tem računu) ali `rate_limited` (omejitev `reauth` ali `reauth_user`, kvota računa, čakalna vrsta za zgoščevanje gesel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (čakalna vrsta za zgoščevanje gesel ali pa je zbirka podatkov ostala zaklenjena), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /account/email:
    post:
      operationId: changeEmail
      tags:
        - email-change
      summary: Sprememba e-poštnega naslova
      description: |-
        Spremeni e-poštni naslov računa. Iz `newEmail` se odstranijo presledki na začetku in koncu, pretvori se v male črke, nato pa se preveri kot naslov pri registraciji.

        **S potrditvijo e-pošte** (privzeto): 202 `verification_sent`. Na novi naslov se pošlje povezava, veljavna 24 ur; odpre `/confirm-email-change`, naslov pa se spremeni šele, ko igralec pritisne gumb na tej strani. Do takrat `GET /account/me` prikazuje novi naslov kot `pendingEmail`. Nova zahteva nadomesti čakajočo; sprememba ali ponastavitev gesla jo prekliče. Na posamezni novi naslov se pošlje največ ena povezava na vsakih 5 minut, ne glede na to, kdo jo zahteva (zahteva za spremembo, ki že čaka, ohrani prej poslano povezavo, ki ostane veljavna); odgovor je enak. Trenutni naslov prejme obvestilo, da je bila zahtevana sprememba na zakrit naslov (`a***@example.org`). Odgovor in `pendingEmail` sta enaka tudi, kadar novi naslov že uporablja drug račun: takrat se povezava ne pošlje, zato se ta sprememba nikoli ne dokonča, lastnik tega naslova pa namesto tega prejme obvestilo (največ eno na uro).

        Ko je povezava potrjena, se naslov spremeni in velja za potrjenega, naprave ostanejo prijavljene, prej poslane povezave (potrditev, ponastavitev gesla, druge spremembe) prenehajo delovati, prejšnji naslov pa prejme obvestilo, v katerem je novi naslov zakrit.

        **Brez potrditve e-pošte** (`REQUIRE_EMAIL_VERIFICATION=false`): naslov se spremeni takoj (200 `email_changed`), prejšnji naslov pa prejme obvestilo. Če naslov uporablja drug račun, je odgovor 409 `email_taken`, lastnik tega naslova pa prejme obvestilo.

        **Ponovno preverjanje pristnosti:** geslo in pri dvofaktorskem preverjanju pristnosti še koda iz aplikacije ali obnovitvena koda. `invalid_email` in `same_email` se preverita pred geslom, zato se ne štejeta kot neuspeh. **Omejitve:** `reauth`, nato `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: Naslov je bil spremenjen takoj (na tem strežniku ni potrditve e-pošte).
          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: Novi naslov, kot je shranjen.
              example:
                status: email_changed
                email: alice.new@example.org
        '202':
          description: Na novi naslov je bila poslana povezava za potrditev (ali pa naslov pripada drugemu računu; odgovor tega ne razkrije).
          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` (trenutni naslov računa), `password_not_set` (račun samo s prijavo z Googlom), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password` (tudi kadar je sprememba ali ponastavitev gesla prehitela zahtevo: povezava se ne pošlje), `mfa_code_required`, `invalid_code`.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`email_taken`: samo brez potrditve e-pošte.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (neuspela ponovna preverjanja pristnosti ali preveč preizkušenih kod na tem računu) ali `rate_limited` (omejitev `reauth` ali `reauth_user`, kvota računa, čakalna vrsta za zgoščevanje gesel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: čakalna vrsta za zgoščevanje gesel ali pa je zbirka podatkov ostala zaklenjena (`retryAfter: 1`: nič ni bilo spremenjeno, isto zahtevo pa je mogoče poslati znova). `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /account/export:
    post:
      operationId: exportAccountData
      tags:
        - data-export
      summary: Prenos podatkov računa
      description: |-
        Vse, kar strežnik hrani o računu, kot ena datoteka JSON za shranjevanje (`format` `scacelith-account-export`, `version` 1). Izvoz zabeleži varnostni dogodek (`account_exported`). Tabela `notes` v dokumentu igralcu v preprosti angleščini pove, kaj je izpuščeno.

        **Nikoli v izvozu:** zgoščena vrednost gesla, skrivni ključ dvofaktorskega preverjanja pristnosti in obnovitvene kode; kateri koli žeton seje ali povezave ali njegova zgoščena vrednost; podatki sistema proti goljufanju (raven in ocena integritete, anomalije, analiza partij, teža prijave kršitve); prijave kršitev, ki so jih o igralcu vložili drugi igralci; identitete moderatorjev; zasebni podatki drugih igralcev (nasprotniki so navedeni z javnim imenom in ratingom, nič ne razkriva, ali je bil kateri drug igralec sankcioniran, in ni vključen noben naslov IP, ki bi lahko pripadal drugi osebi).

        **Ponovno preverjanje pristnosti:** geslo in pri dvofaktorskem preverjanju pristnosti še koda iz aplikacije ali obnovitvena koda. **Omejitve:** `account_export` (5 na uro na igralca za ves strežnik; šteje vsak poskus, tudi neuspeli; preveri se najprej), nato `reauth` in `reauth_user`. **Časovna omejitev:** 60 sekund.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReauthCredentials'
            example:
              password: correct horse battery
              code: '123456'
      responses:
        '200':
          description: Dokument izvoza kot priloga.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            Content-Disposition:
              description: '`attachment; filename="scacelith-account-<username>.json"`. Znaki uporabniškega imena, ki niso črke, števke, `_`, `.` ali `-`, se nadomestijo z `_`.'
              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` (račun samo s prijavo z Googlom), `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` (neuspela ponovna preverjanja pristnosti ali preveč preizkušenih kod na tem računu) ali `rate_limited` (omejitev `account_export`, `reauth` ali `reauth_user`, kvota računa, čakalna vrsta za zgoščevanje gesel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (zbirka podatkov je ostala zaklenjena, z glavo `Retry-After: 1`), `server_busy` (čakalna vrsta za zgoščevanje gesel), `timeout` (60 sekund). Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /account/delete:
    post:
      operationId: deleteAccount
      tags:
        - account-deletion
      summary: Izbris računa
      description: |-
        Izbriše račun; tega ni mogoče razveljaviti.

        - Vse seje se takoj prekličejo: žeton seje od takrat naprej dobi 401 `invalid_token`.
        - Uporabniško ime postane `deleted#<id>`, v računu in v vsakem zapisu partije.
        - Izbrišejo se: e-poštni naslov, zgoščena vrednost gesla, skrivni ključ dvofaktorskega preverjanja pristnosti in obnovitvene kode, seje in žetoni povezav, povezava z Googlovim računom, zapis integritete sistema proti goljufanju ter naslovi IP, shranjeni z varnostnimi dogodki.
        - Ratingi in partije se ohranijo. Partije ostanejo berljive pod anonimnim imenom, `GET /players/{username}` pa za nekdanje ime odgovori z 404.

        **Ponovno preverjanje pristnosti:** geslo in pri dvofaktorskem preverjanju pristnosti še koda iz aplikacije ali obnovitvena koda. **Omejitve:** `reauth`, nato `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: Račun je izbrisan.
          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` (račun samo s prijavo z Googlom), `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` (neuspela ponovna preverjanja pristnosti ali preveč preizkušenih kod na tem računu) ali `rate_limited` (omejitev `reauth` ali `reauth_user`, kvota računa, čakalna vrsta za zgoščevanje gesel).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (čakalna vrsta za zgoščevanje gesel ali pa je zbirka podatkov ostala zaklenjena), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /account/games:
    get:
      operationId: listAccountGames
      tags:
        - game-history
      summary: Seznam igralčevih partij s filtri in straničenjem
      description: |-
        Partije prijavljenega igralca, od najnovejše naprej, filtrirane in razdeljene na strani, s številom partij, ki ustrezajo filtru. Vsi parametri poizvedbe so neobvezni, prazna vrednost pa velja za izpuščeno.

        Straničenje: `next` prejšnje strani pošljite kot `before`; na zadnji strani je `next` enak `null`. `total` šteje partije, ki ustrezajo filtru, na vseh straneh.

        **Omejitev:** `account_games`.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/BeforeQuery'
        - $ref: '#/components/parameters/LimitQuery'
        - name: category
          in: query
          required: false
          description: ID uradne kategorije (`3+2` ali `3%2B2`) ali `custom` za vse partije z drugim tempom igre.
          schema:
            type: string
            pattern: '^\s*([0-9]+[+ ][0-9]+|custom)\s*$'
          example: '3+2'
        - name: rated
          in: query
          required: false
          description: Samo partije za rating (`true`) ali prijateljske partije (`false`).
          schema:
            type: string
            enum:
              - 'true'
              - 'false'
        - name: result
          in: query
          required: false
          description: Samo dobljene, izgubljene ali z remijem končane partije, z vidika igralca. Preklicane partije se prikažejo samo brez tega filtra.
          schema:
            type: string
            enum:
              - win
              - loss
              - draw
      responses:
        '200':
          description: Ena stran zgodovine.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HistoryPage'
              example:
                games:
                  - id: 4100000000001
                    category: '3+2'
                    rated: true
                    timeControl: '180+2'
                    white:
                      name: alice
                      rating: 1500
                      ratingAfter: 1510
                      ratingDiff: 10
                    black:
                      name: bob
                      rating: 1520
                      ratingAfter: 1510
                      ratingDiff: -10
                    color: white
                    status: 1
                    reason: 2
                    result: 1-0
                    termination: Resignation
                    plies: 41
                    startedAt: 1790620205000
                    endedAt: 1790620611000
                    baseMs: 180000
                    incMs: 2000
                    outcome: win
                next: null
                total: 1
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_cursor` (`before` ni ID partije), `invalid_limit` ali `invalid_filter` (`category`, `rated` ali `result`), vse z `field`, ki poimenuje parameter.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: omejitev `account_games` ali kvota računa.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (zbirka podatkov je ostala zaklenjena; `retryAfter: 1` v telesu, brez glave `Retry-After`), `server_busy` (iskanje seje je naletelo na zaklenjeno zbirko podatkov), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /games/{id}:
    get:
      operationId: getGame
      tags:
        - games
      summary: Pridobivanje zapisa partije s potezami in urami
      description: |-
        En zapis partije s potezami in urami. Brez žetona seje ali z žetonom igralca, ki partije ni igral, je odgovor javni. Če je igralec, ki mu žeton pripada, igral partijo, odgovor doda še `you` in `reportable`.

        **Omejitev:** `public_read` (po igralcu z žetonom seje, po odjemalcu brez njega).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
      responses:
        '200':
          description: Zapis partije.
          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`: ni pozitivno celo število z največ 16 števkami (manjše od 2^53); `invalid_request`: parameter poti ni veljavno kodiran za URL.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: poslan je bil žeton seje, ki ni veljaven.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: take partije ni.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: omejitev `public_read` ali kvota računa.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (zbirka podatkov je ostala zaklenjena; `retryAfter: 1` v telesu, brez glave `Retry-After`), `server_busy` (iskanje seje je naletelo na zaklenjeno zbirko podatkov), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /games/{id}/pgn:
    get:
      operationId: getGamePgn
      tags:
        - games
      summary: Prenos partije kot datoteke PGN
      description: |-
        Ista partija kot datoteka PGN: ena partija s konci vrstic `\n` in besedilom potez v vrsticah, krajših od 80 stolpcev. Odgovor je enak z žetonom seje ali brez njega.

        Oznake v tem vrstnem redu: `Event` (`<SERVER_NAME> rated <category>` ali `<SERVER_NAME> casual <category>`), `Site` (`SERVER_PUBLIC_HOST`), `Date` (datum začetka po UTC), `Round`, `White`, `Black`, `Result` (`*` za preklicano partijo), `UTCDate` in `UTCTime` (začetek), `WhiteElo` in `BlackElo` (ratinga na začetku ali `-`), `WhiteRatingDiff` in `BlackRatingDiff` (spremembi, na primer `+10` in `-10`, v vsaki partiji za rating, `+0`, kadar pravila ratinga pustijo rating nespremenjen; prijateljska partija, partija s tempom igre po meri ali preklicana partija nima nobene od njiju), `TimeControl` (v sekundah), `Termination` (standardna vrednost PGN: `normal`; `time forfeit`, padec zastavice, tudi kadar se partija konča z remijem; `abandoned`; `rules infraction`, druga nepravilna poteza ali poraz zaradi kršitve pravil poštene igre; `unterminated`, preklicana partija), `PlyCount`, `ScacelithGameId` (desetiški ID).

        Vsaka poteza ima `{[%clk h:mm:ss.f] [%emt h:mm:ss.f]}`: uro igralca, ki je potezo odigral, po potezi in čas, obračunan za potezo, v desetinkah sekunde (odrezano). Vrednost je izpuščena, kadar je zapis nima. Po zadnji potezi sledi razlog za konec partije z besedami, nato rezultat.

        **Omejitev:** `public_read` (po igralcu z žetonom seje, po odjemalcu brez njega).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
      responses:
        '200':
          description: Datoteka PGN.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            Content-Disposition:
              description: '`attachment; filename="scacelith-<id>.pgn"`.'
              schema:
                type: string
                pattern: '^attachment; filename="scacelith-[1-9][0-9]{0,15}\.pgn"$'
              example: attachment; filename="scacelith-4100000000001.pgn"
          content:
            application/x-chess-pgn:
              schema:
                type: string
                description: 'Besedilo PGN v UTF-8 (`Content-Type: application/x-chess-pgn; charset=utf-8`).'
              example: |
                [Event "Scacelith rated 3+2"]
                [Site "caissa.scacelith.com"]
                [Date "2026.09.28"]
                [Round "-"]
                [White "alice"]
                [Black "bob"]
                [Result "1-0"]
                [UTCDate "2026.09.28"]
                [UTCTime "18:30:05"]
                [WhiteElo "1500"]
                [BlackElo "1520"]
                [WhiteRatingDiff "+10"]
                [BlackRatingDiff "-10"]
                [TimeControl "180+2"]
                [Termination "normal"]
                [PlyCount "2"]
                [ScacelithGameId "4100000000001"]

                1. e4 {[%clk 0:03:00.0] [%emt 0:00:00.0]} 1... e5 {[%clk 0:03:00.3]
                [%emt 0:00:01.7]} {Resignation} 1-0
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_game_id`: ni pozitivno celo število z največ 16 števkami (manjše od 2^53); `invalid_request`: parameter poti ni veljavno kodiran za URL.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: poslan je bil žeton seje, ki ni veljaven.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: take partije ni.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: omejitev `public_read` ali kvota računa.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`internal_error`: med drugim, kadar shranjenih potez ni mogoče ponovno odigrati do shranjenega konca (strežnik to zabeleži v dnevnik).'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (zbirka podatkov je ostala zaklenjena; `retryAfter: 1` v telesu, brez glave `Retry-After`), `server_busy` (iskanje seje je naletelo na zaklenjeno zbirko podatkov), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /games/{id}/gif:
    get:
      operationId: getGameGif
      tags:
        - gifs
      summary: Prenos partije s tega strežnika kot animiranega GIF-a
      description: |-
        Partija kot animirani GIF. Imena in ratingi so tisti iz zapisa partije (ratingi na začetku, izbrisan račun kot `deleted#<id>`), prav tako rezultat in način konca (`Resignation`, `Loss on time` ...).

        Preverjanja potekajo v tem vrstnem redu: `gif_disabled`, možnosti slike (`size`, `orientation`, `delay`, `coords`), ID partije, partija, njena dolžina.

        **Potrebna je seja:** kvote se štejejo po računih. **Omejitve:** `gif` pri vsaki zahtevi; `gif_user_min`, `gif_user_hour`, `gif_ip_min` in `gif_ip_hour` samo, kadar je treba GIF izdelati (glejte opis razdelka). **Časovna omejitev:** `GIF_QUEUE_TIMEOUT_MS` + `GIF_RENDER_TIMEOUT_MS` + 5 sekund (privzeto 45 sekund).
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
        - name: size
          in: query
          required: false
          description: 'Velikost slike: `small` (polja po 32 px), `medium` (48 px) ali `large` (72 px).'
          schema:
            type: string
            enum:
              - small
              - medium
              - large
            default: medium
        - name: orientation
          in: query
          required: false
          description: Barva, ki je na šahovnici spodaj.
          schema:
            type: string
            enum:
              - white
              - black
            default: white
        - name: delay
          in: query
          required: false
          description: Milisekunde na potezo (od 1 do 6 desetiških števk).
          schema:
            type: integer
            minimum: 100
            maximum: 3000
            default: 500
        - name: coords
          in: query
          required: false
          description: Ali so črke linij in številke vrst narisane okoli šahovnice (`1`) ali ne (`0`).
          schema:
            type: string
            enum:
              - '1'
              - '0'
            default: '1'
      responses:
        '200':
          $ref: '#/components/responses/GifFile'
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_option` z `field` (`size`, `orientation`, `delay` ali `coords`): vrednost zunaj dovoljenih. `invalid_game_id`. `invalid_request`: parameter poti ni veljavno kodiran za URL.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: take partije ni. `gif_disabled`: strežnik je izklopil GIF-e (`GIF_ENABLED=false`; žeton omejitve `gif` se vrne).'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '422':
          $ref: '#/components/responses/UnprocessableContent'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: omejitev `gif`, omejitev upodabljanja ali kvota računa.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`render_failed`: GIF-a ni bilo mogoče izdelati (strežnik razlog zabeleži v dnevnik). `internal_error`.'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` z `retryAfter` (od 3 do 10 sekund) in `Retry-After`: čakalna vrsta za upodabljanje je polna ali pa je GIF na prosto nit čakal `GIF_QUEUE_TIMEOUT_MS` (10 sekund); vsi žetoni omejitev, ki jih je zahteva porabila, se vrnejo. `busy`: zbirka podatkov je ostala zaklenjena (`retryAfter: 1` samo v telesu). `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /gif:
    post:
      operationId: renderPgnGif
      tags:
        - gifs
      summary: Izdelava animiranega GIF-a poljubne partije, poslane kot PGN
      description: |-
        Enaka slika kot pri `GET /games/{id}/gif` za poljubno partijo, poslano kot besedilo PGN: partijo, ki jo je shranila igra, izvoz z drugega spletnega mesta, ročno vneseno partijo. Uporabi se samo prva partija v besedilu.

        - Bralnik PGN sprejme, kar sprejme bralnik same igre: vsak PGN, ki ga zapiše ta strežnik, in običajne izvoze drugih spletnih mest (komentarji, variante, NAG-i in zapisi ure se preskočijo; številke potez in SAN se berejo prizanesljivo; oznaka `FEN` določa začetno pozicijo, razen če je `SetUp` enak `"0"`).
        - Imena in ratingi se vzamejo iz oznak `White`, `Black`, `WhiteElo` in `BlackElo`. Črke z diakritičnimi znamenji jih izgubijo, drugi znaki zunaj natisljivega ASCII postanejo `?`, dolga imena pa se skrajšajo (48 znakov). Rezultat se vzame iz oznake `Result`, sicer s konca besedila potez. Oznaka `Termination` se prikaže, razen če je `normal`: takrat končna pozicija sama pove, kako se je partija končala (mat, pat).
        - Ta končna točka sama preveri svoje telo: na neznano polje ali na `pgn`, ki manjka ali ni niz, odgovori s 400 `invalid_request` z `field`; na napačno možnost odgovori s 400 `invalid_option` (`delayMs` mora biti število JSON, `coords` logična vrednost; `null` je zavrnjen).

        **Potrebna je seja:** kvote se štejejo po računih. **Omejitve:** kot pri `GET /games/{id}/gif`. **Omejitev velikosti telesa:** 135.168 bajtov ne glede na `HTTP_BODY_LIMIT` (PGN kot niz JSON, vključno z ubežnimi zaporedji). **Časovna omejitev:** privzeto 45 sekund.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GifRequest'
            examples:
              short:
                summary: Kratka ročno vnesena partija brez koordinat
                value:
                  pgn: 1. f3 e5 2. g4 Qh4# 0-1
                  coords: false
              options:
                summary: Datoteka PGN z vsemi možnostmi
                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` z `field` (neznano polje ali `pgn` manjka oziroma ni niz; brez `field`, kadar telo ni objekt); `invalid_json`; `invalid_option` z `field` (`size`, `orientation`, `delayMs` ali `coords`); `invalid_pgn` z `line` in `column` (od 1, stolpci v znakih) ter sporočilom bralnika v `message`: nepravilna ali dvoumna poteza, pokvarjena oznaka, neznana različica šaha, več kot 65.536 bajtov.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`gif_disabled`: strežnik je izklopil GIF-e (`GIF_ENABLED=false`; žeton omejitve `gif` se vrne).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
          description: '`payload_too_large`: telo, večje od 135.168 bajtov.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '422':
          $ref: '#/components/responses/UnprocessableContent'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: omejitev `gif`, omejitev upodabljanja ali kvota računa.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`render_failed`: GIF-a ni bilo mogoče izdelati (strežnik razlog zabeleži v dnevnik). `internal_error`.'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` z `retryAfter` (od 3 do 10 sekund) in `Retry-After`: čakalna vrsta za upodabljanje je polna ali pa je GIF predolgo čakal na prosto nit; vsi žetoni omejitev, ki jih je zahteva porabila, se vrnejo. `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /players/{username}:
    get:
      operationId: getPlayer
      tags:
        - players
      summary: Pridobivanje javnega profila igralca
      description: |-
        Javni profil igralca: ratingi v uradnih kategorijah (v vrstnem redu strežnika) in števila partij. `games.total` šteje vse shranjene partije, vključno s prijateljskimi in preklicanimi; `games.rated`, `wins`, `draws` in `losses` so seštevki prek zapisov ratinga, torej samo partije za rating. Odgovor je enak z žetonom seje ali brez njega.

        **Omejitev:** `public_read` (po igralcu z žetonom seje, po odjemalcu brez njega).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Username'
      responses:
        '200':
          description: Profil.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlayerProfile'
              example:
                username: alice
                createdAt: 1790882839743
                ratings:
                  - category: '3+2'
                    rating: 1510
                    provisional: true
                    games: 2
                    wins: 1
                    draws: 1
                    losses: 0
                    peak: 1510
                games:
                  total: 3
                  rated: 2
                  wins: 1
                  draws: 1
                  losses: 0
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_username`: ni od 2 do 24 znakov iz `[A-Za-z0-9_.-]`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: poslan je bil žeton seje, ki ni veljaven.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: takega igralca ni ali pa je račun izbrisan.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: omejitev `public_read` ali kvota računa.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (zbirka podatkov je ostala zaklenjena; `retryAfter: 1` v telesu, brez glave `Retry-After`), `server_busy` (iskanje seje je naletelo na zaklenjeno zbirko podatkov), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /players/{username}/games:
    get:
      operationId: listPlayerGames
      tags:
        - players
      summary: Seznam nedavnih partij igralca
      description: |-
        Nedavne partije igralca, od najnovejše naprej, razdeljene na strani kot zgodovina, vendar brez filtrov in skupnega števila. `color` je barva tega igralca. `next` je ID zadnje partije vedno, kadar je stran polna, zato je naslednja stran lahko prazna. Igralec se poišče najprej: za neznanega igralca je odgovor 404 ne glede na poizvedbo.

        **Omejitev:** `public_read` (po igralcu z žetonom seje, po odjemalcu brez njega).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Username'
        - $ref: '#/components/parameters/BeforeQuery'
        - $ref: '#/components/parameters/LimitQuery'
      responses:
        '200':
          description: Ena stran igralčevih partij.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlayerGamesPage'
              example:
                username: alice
                games:
                  - id: 4100000000001
                    category: '3+2'
                    rated: true
                    timeControl: '180+2'
                    white:
                      name: alice
                      rating: 1500
                      ratingAfter: 1510
                      ratingDiff: 10
                    black:
                      name: bob
                      rating: 1520
                      ratingAfter: 1510
                      ratingDiff: -10
                    color: white
                    status: 1
                    reason: 2
                    result: 1-0
                    termination: Resignation
                    plies: 41
                    startedAt: 1790620205000
                    endedAt: 1790620611000
                next: 4100000000001
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_username`, `invalid_cursor` (`before` ni ID partije), `invalid_limit` (zadnji dve brez `field`).'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: poslan je bil žeton seje, ki ni veljaven.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: takega igralca ni ali pa je račun izbrisan.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: omejitev `public_read` ali kvota računa.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (zbirka podatkov je ostala zaklenjena; `retryAfter: 1` v telesu, brez glave `Retry-After`), `server_busy` (iskanje seje je naletelo na zaklenjeno zbirko podatkov), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /leaderboard:
    get:
      operationId: getLeaderboard
      tags:
        - leaderboard
      summary: Pridobivanje najboljših igralcev kategorije
      description: |-
        100 najboljših zapisov ratinga uradne kategorije z vsaj `minGames` (`PROVISIONAL_GAMES`) upoštevanimi partijami, brez izbrisanih računov in potrjenih goljufov. Strežnik vsako lestvico znova izračuna največ na vsakih 10 sekund; `updatedAt` pove, kdaj. Za posamezno kategorijo hkrati teče samo en izračun: medtem druge zahteve dobijo prejšnji seznam ali pa počakajo na izračun, če seznama še ni.

        **Omejitve:** samo sloj po naslovu.
      security: []
      parameters:
        - name: category
          in: query
          required: true
          description: ID uradne kategorije (`3+2` ali `3%2B2`).
          schema:
            type: string
            pattern: '^\s*[0-9]+[+ ][0-9]+\s*$'
          example: '3+2'
        - name: limit
          in: query
          required: false
          description: Število igralcev, od 1 do 100. Večje število z največ 3 števkami šteje kot 100; prazna vrednost velja za izpuščeno.
          schema:
            type: integer
            minimum: 1
            maximum: 999
            default: 100
      responses:
        '200':
          description: Lestvica.
          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` (manjka ali ni uradna kategorija), `invalid_limit`.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: sloj po naslovu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (zbirka podatkov je ostala zaklenjena; `retryAfter: 1` v telesu, brez glave `Retry-After`), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /reports:
    post:
      operationId: reportPlayer
      tags:
        - reports
      summary: Prijava nasprotnika iz nedavne partije
      description: |-
        Prijavi nasprotnika iz ene od igralčevih lastnih partij, ki se je končala v zadnjih 7 dneh. Prijava kršitve sama po sebi nikoli ne spremeni ratinga, sankcije ali ravni integritete. Zviša prioriteto pregleda, ki jo vidijo moderatorji, in razen pri `abuse` zahteva analizo partije s šahovskim programom.

        Prijava istega nasprotnika za isto partijo dobi enak odgovor in ničesar ne spremeni; odgovor nikoli ne razkrije ničesar o računu, zoper katerega je bila vložena prijava. `GET /games/{id}` igralcema partije vnaprej pove, ali bi bila prijava sprejeta (`reportable`).

        Ta končna točka sama preveri svoje telo, v vrstnem redu `gameId`, `reported`, `category`, `comment`: ob neuspehu odgovori s 400 `invalid_request` brez `field`. Polja, ki jih ne pozna, prezre.

        **Omejitve:** `reports` (po igralcu), nato `REPORTS_PER_DAY` (5) prijav kršitev na igralca v 24 urah (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: Prijava kršitve je prejeta (ali pa je bila že vložena).
          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` (brez `field`), `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`report_not_allowed`: ni nasprotnik prijavitelja v partiji, ki se je končala v zadnjih 7 dneh.'
        '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`, z glavo `Retry-After`): `REPORTS_PER_DAY` prijav kršitev v zadnjih 24 urah. `rate_limited`: omejitev `reports` ali kvota računa.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (iskanje seje je naletelo na zaklenjeno zbirko podatkov), `timeout`. Tudi `server_busy` z `retryAfter` od 2 do 5, kadar strežnik močno zaostaja (glejte uvod): zahteva ni bila izvedena.'
  /verify-email:
    servers:
      - url: https://caissa.scacelith.com
        description: Uradni strežnik (strani so v korenu, zunaj `/api/v1`).
      - url: https://{host}:{port}
        description: Kateri koli strežnik Scacelith (strani so v korenu, zunaj `/api/v1`).
        variables:
          host:
            default: caissa.scacelith.com
            description: Javno ime gostitelja strežnika (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Javna vrata API (`PUBLIC_API_PORT`, sicer `API_PORT`).
    get:
      operationId: showVerifyEmailPage
      tags:
        - pages
      summary: Prikaz strani za potrditev e-poštnega naslova
      description: |-
        Stran povezave za potrditev e-poštnega naslova (registracija ali ponovno poslano e-poštno sporočilo za potrditev). Prikaže samo gumb »Confirm my e-mail address« (Potrdi moj e-poštni naslov), da povezave ne porabi pregledovalnik e-pošte, ki jo odpre; gumb pošlje obrazec na `POST /verify-email`.

        **Omejitev:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Stran z gumbom za potrditev.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Povezava je neveljavna ali potekla (stran HTML).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Omejitev `page` (stran HTML) ali sloj po naslovu (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitVerifyEmailPage
      tags:
        - pages
      summary: Potrditev e-poštnega naslova
      description: |-
        Obrazec strani za potrditev. Potrdi naslov; pri novi registraciji takrat ustvari račun in igralec se lahko prijavi.

        **Omejitev:** `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: Naslov je potrjen (pri novi registraciji pa je račun ustvarjen).
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Povezava je neveljavna, že uporabljena ali potekla ali pa je obrazec neveljaven (stran HTML).
        '408':
          $ref: '#/components/responses/HtmlRequestTimeout'
        '409':
          $ref: '#/components/responses/HtmlConflict'
          description: Povezava nove registracije, katere uporabniško ime ali naslov je medtem prevzel drug račun; račun se ne ustvari (stran HTML).
        '413':
          $ref: '#/components/responses/HtmlPayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/HtmlUnsupportedMediaType'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Omejitev `auth` (stran HTML) ali sloj po naslovu (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Zbirka podatkov je ostala zaklenjena (`Retry-After: 1`) ali pa strežnik močno zaostaja (`Retry-After` od 2 do 5, glejte uvod): nič ni bilo spremenjeno in povezava še vedno deluje. Tudi potek časa za obdelavo zahteve.'
  /reset-password:
    servers:
      - url: https://caissa.scacelith.com
        description: Uradni strežnik (strani so v korenu, zunaj `/api/v1`).
      - url: https://{host}:{port}
        description: Kateri koli strežnik Scacelith (strani so v korenu, zunaj `/api/v1`).
        variables:
          host:
            default: caissa.scacelith.com
            description: Javno ime gostitelja strežnika (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Javna vrata API (`PUBLIC_API_PORT`, sicer `API_PORT`).
    get:
      operationId: showResetPasswordPage
      tags:
        - pages
      summary: Prikaz obrazca za ponastavitev gesla
      description: |-
        Stran povezave za ponastavitev gesla: obrazec za novo geslo (geslo dvakrat). Obrazec se pošlje na `POST /reset-password`.

        **Omejitev:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Obrazec za novo geslo.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Povezava je neveljavna (tudi kadar je bila poslana na naslov, ki ga račun nima več).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Omejitev `page` (stran HTML) ali sloj po naslovu (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitResetPasswordPage
      tags:
        - pages
      summary: Nastavitev novega gesla z obrazcem za ponastavitev
      description: |-
        Obrazec strani za ponastavitev; naredi isto kot `POST /auth/password/reset`, v eni transakciji: vse naprave se odjavijo, čakajoča sprememba e-poštnega naslova se prekliče, druge povezave za ponastavitev prenehajo delovati, naslov velja za potrjenega, lastnik pa prejme e-poštno sporočilo.

        Najprej se preveri povezava, nato, ali sta gesli enaki, nato pravila za gesla. Kadar je strežnik zaseden, se obrazec vrne z `Retry-After`, povezava pa ostane veljavna.

        **Omejitve:** `auth`, nato `auth_reset` (deljena s `POST /auth/password/reset`).
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ResetPasswordForm'
            example:
              token: A0ATfmXCDKFC8UkSxLJCD85K7au5nMIrZ2P3K-1VWQg
              newPassword: a much better passphrase
              confirmPassword: a much better passphrase
          application/json:
            schema:
              $ref: '#/components/schemas/ResetPasswordForm'
            example:
              token: A0ATfmXCDKFC8UkSxLJCD85K7au5nMIrZ2P3K-1VWQg
              newPassword: a much better passphrase
              confirmPassword: a much better passphrase
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Geslo je spremenjeno, vse naprave pa so odjavljene.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Znova obrazec z napako (gesli se razlikujeta, šibko geslo), povezava je neveljavna ali pa je obrazec neveljaven (stran HTML).
        '408':
          $ref: '#/components/responses/HtmlRequestTimeout'
        '413':
          $ref: '#/components/responses/HtmlPayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/HtmlUnsupportedMediaType'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Omejitev `auth` ali `auth_reset` (stran HTML), znova obrazec z `Retry-After`, kadar ima ta odjemalec v čakalni vrsti preveč zgoščevanj gesel (povezava ostane veljavna), ali sloj po naslovu (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Znova obrazec z `Retry-After`, kadar je strežnik zaseden (čakalna vrsta za zgoščevanje gesel ali pa je zbirka podatkov ostala zaklenjena); povezava ostane veljavna. Kadar strežnik močno zaostaja (glejte uvod), namesto tega stran »Server error« (napaka strežnika) z `Retry-After` (od 2 do 5): nič ni bilo izvedeno in povezava ostane veljavna. Tudi potek časa za obdelavo zahteve.'
  /confirm-email-change:
    servers:
      - url: https://caissa.scacelith.com
        description: Uradni strežnik (strani so v korenu, zunaj `/api/v1`).
      - url: https://{host}:{port}
        description: Kateri koli strežnik Scacelith (strani so v korenu, zunaj `/api/v1`).
        variables:
          host:
            default: caissa.scacelith.com
            description: Javno ime gostitelja strežnika (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Javna vrata API (`PUBLIC_API_PORT`, sicer `API_PORT`).
    get:
      operationId: showConfirmEmailChangePage
      tags:
        - pages
      summary: Prikaz strani za potrditev spremembe e-poštnega naslova
      description: |-
        Stran povezave za spremembo e-poštnega naslova: prikaže novi naslov in ime računa ter gumb »Use this e-mail address« (Uporabi ta e-poštni naslov), ki pošlje obrazec na `POST /confirm-email-change`.

        **Omejitev:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Stran z novim naslovom in gumbom za njegovo potrditev.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Povezava je neveljavna ali potekla (tudi kadar se je naslov računa od zahteve spremenil).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Omejitev `page` (stran HTML) ali sloj po naslovu (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitConfirmEmailChangePage
      tags:
        - pages
      summary: Potrditev novega e-poštnega naslova
      description: |-
        Obrazec strani za spremembo e-poštnega naslova. Naslov se spremeni in velja za potrjenega; naprave ostanejo prijavljene; prej poslane povezave (potrditev, ponastavitev gesla, druge spremembe) prenehajo delovati; prejšnji naslov prejme obvestilo, v katerem je novi naslov zakrit.

        **Omejitev:** `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: Naslov je spremenjen.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Povezava je neveljavna, že uporabljena ali potekla ali pa je obrazec neveljaven (stran HTML).
        '408':
          $ref: '#/components/responses/HtmlRequestTimeout'
        '409':
          $ref: '#/components/responses/HtmlConflict'
          description: Naslov je medtem prevzel drug račun (stran HTML).
        '413':
          $ref: '#/components/responses/HtmlPayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/HtmlUnsupportedMediaType'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: Omejitev `auth` (stran HTML) ali sloj po naslovu (JSON `rate_limited`).
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Zbirka podatkov je ostala zaklenjena (`Retry-After: 1`) ali pa strežnik močno zaostaja (`Retry-After` od 2 do 5, glejte uvod): nič ni bilo spremenjeno in povezava še vedno deluje. Tudi potek časa za obdelavo zahteve.'
  /healthz:
    servers:
      - url: https://caissa.scacelith.com
        description: Uradni strežnik, v korenu.
      - url: https://caissa.scacelith.com/api/v1
        description: Uradni strežnik, pod `/api/v1`.
      - url: https://{host}:{port}
        description: Kateri koli strežnik Scacelith, v korenu.
        variables:
          host:
            default: caissa.scacelith.com
            description: Javno ime gostitelja strežnika (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Javna vrata API (`PUBLIC_API_PORT`, sicer `API_PORT`).
      - url: https://{host}:{port}/api/v1
        description: Kateri koli strežnik Scacelith, pod `/api/v1`.
        variables:
          host:
            default: caissa.scacelith.com
            description: Javno ime gostitelja strežnika (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Javna vrata API (`PUBLIC_API_PORT`, sicer `API_PORT`).
    get:
      operationId: getLiveness
      tags:
        - health
      summary: Preverjanje, ali proces teče
      description: |-
        Odgovori z 200 `ok`, dokler proces teče. Deluje tudi `HEAD`. Na vse druge metode odgovori s 405 `method_not_allowed` z `Allow: GET, HEAD` (tudi na `OPTIONS`).

        **Omejitve:** samo sloj po naslovu.
      security: []
      responses:
        '200':
          description: Proces teče.
          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`: sloj po naslovu.'
  /readyz:
    servers:
      - url: https://caissa.scacelith.com
        description: Uradni strežnik, v korenu.
      - url: https://caissa.scacelith.com/api/v1
        description: Uradni strežnik, pod `/api/v1`.
      - url: https://{host}:{port}
        description: Kateri koli strežnik Scacelith, v korenu.
        variables:
          host:
            default: caissa.scacelith.com
            description: Javno ime gostitelja strežnika (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Javna vrata API (`PUBLIC_API_PORT`, sicer `API_PORT`).
      - url: https://{host}:{port}/api/v1
        description: Kateri koli strežnik Scacelith, pod `/api/v1`.
        variables:
          host:
            default: caissa.scacelith.com
            description: Javno ime gostitelja strežnika (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Javna vrata API (`PUBLIC_API_PORT`, sicer `API_PORT`).
    get:
      operationId: getReadiness
      tags:
        - health
      summary: Preverjanje, ali strežnik sprejema igralce
      description: |-
        Odgovori z 200 `ready`, kadar strežnik sprejema igralce, in s 503 `not_ready`, dokler se zaganja ali ustavlja. Deluje tudi `HEAD`. Na vse druge metode odgovori s 405 `method_not_allowed` z `Allow: GET, HEAD` (tudi na `OPTIONS`).

        **Omejitve:** samo sloj po naslovu.
      security: []
      responses:
        '200':
          description: Strežnik sprejema igralce.
          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`: sloj po naslovu.'
        '503':
          description: Strežnik se zaganja ali ustavlja.
          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: |-
        Žeton seje (`sct_`, ki mu sledi 43 znakov base64url) iz odgovora na prijavo, v glavi `Authorization`: `Authorization: Bearer <token>`. Predpona je natanko `Bearer` (razlikujejo se velike in male črke) in en presledek; na vrednost brez nje ali na žeton, ki nima od 1 do 512 natisljivih znakov ASCII, je odgovor 401 `invalid_token` kot pri vsakem drugem neveljavnem žetonu. Prazna glava velja, kot da glave ni.
  parameters:
    GameId:
      name: id
      in: path
      required: true
      description: ID partije, pozitivno desetiško celo število z največ 16 števkami brez vodilnih ničel, manjše od 2^53.
      schema:
        type: integer
        format: int64
        minimum: 1
        maximum: 9007199254740991
      example: 4100000000001
    SessionId:
      name: id
      in: path
      required: true
      description: ID seje, kot ga vrne `GET /auth/sessions`, zapisan brez vodilnih ničel (`03` ni seja 3).
      schema:
        type: integer
        format: int64
        minimum: 1
      example: 3
    Username:
      name: username
      in: path
      required: true
      description: Uporabniško ime igralca, ne glede na velike in male črke. Strežnik sprejme vsako ime, dolgo od 2 do 24 znakov iz `[A-Za-z0-9_.-]` (najširše pravilo, ki ga je kdaj dovoljeval).
      schema:
        type: string
        pattern: '^[A-Za-z0-9_.-]{2,24}$'
      example: alice
    BeforeQuery:
      name: before
      in: query
      required: false
      description: ID partije; navedene so samo starejše partije. Pošljite `next` prejšnje strani. Prazna vrednost velja za izpuščeno.
      schema:
        type: integer
        format: int64
        minimum: 1
        maximum: 9007199254740991
      example: 4100000000002
    LimitQuery:
      name: limit
      in: query
      required: false
      description: Velikost strani, od 1 do 50. Večje število z največ 3 števkami šteje kot 50; prazna vrednost velja za izpuščeno.
      schema:
        type: integer
        minimum: 1
        maximum: 999
        default: 20
    LinkTokenQuery:
      name: token
      in: query
      required: false
      description: Žeton povezave iz e-poštnega sporočila (43 znakov base64url). Če manjka ali je napačen, se prikaže stran »link invalid or expired« (povezava je neveljavna ali potekla).
      schema:
        type: string
        pattern: '^[A-Za-z0-9_-]{43}$'
      example: asfioO4P0r_Zn5DxWzcQuF6Drgxu7wXGjCyS-UFbTaw
  headers:
    CacheControl:
      description: Vsak odgovor strežnika prepoveduje predpomnjenje.
      schema:
        type: string
        const: no-store
    RetryAfter:
      description: Število sekund, ki jih je treba počakati pred ponovnim poskusom; enaka vrednost kot `retryAfter` v telesu.
      schema:
        type: integer
        minimum: 1
      example: 30
    WwwAuthenticate:
      description: Izziv sheme Bearer, z `error="invalid_token"` pri neveljavnem žetonu.
      schema:
        type: string
        enum:
          - Bearer realm="scacelith"
          - Bearer realm="scacelith", error="invalid_token"
    ConnectionClose:
      description: Strežnik po tem odgovoru zapre povezavo.
      schema:
        type: string
        const: close
  responses:
    BadRequest:
      description: '`invalid_request` (z `field`, kadar je napaka v polju telesa) ali `invalid_json`.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalidRequest:
              summary: Polje krši shemo
              value:
                error: invalid_request
                message: '"password" is required'
                field: password
            invalidJson:
              summary: Telo ni JSON
              value:
                error: invalid_json
                message: The body is not valid JSON.
            weakPassword:
              summary: Šibko geslo
              value:
                error: weak_password
                message: The password must have at least 10 characters.
                reason: too_short
            invalidPgn:
              summary: PGN, ki ga ni mogoče prebrati
              value:
                error: invalid_pgn
                message: illegal move 'Ke3'
                line: 1
                column: 13
    Unauthorized:
      description: '`unauthorized` (brez glave `Authorization`) ali `invalid_token` (žeton je napačno oblikovan, potekel, preklican ali pripada izbrisanemu računu).'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        WWW-Authenticate:
          $ref: '#/components/headers/WwwAuthenticate'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            unauthorized:
              summary: Brez seje
              value:
                error: unauthorized
                message: Log in first.
            invalidToken:
              summary: Neveljaven žeton
              value:
                error: invalid_token
                message: The session is invalid or has expired; log in again.
    Forbidden:
      description: Zahteva je zavrnjena.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalidPassword:
              summary: Napačno geslo pri ponovnem preverjanju pristnosti
              value:
                error: invalid_password
                message: Wrong password.
            banned:
              summary: Blokiran račun
              value:
                error: banned
                message: This account is banned.
                until: 1791487639708
    NotFound:
      description: '`not_found` ali funkcija, ki jo je ta strežnik izklopil.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: not_found
            message: No such game.
    RequestTimeout:
      description: '`request_timeout`: telo ni prispelo v 10 sekundah. Strežnik zapre povezavo.'
      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: Zahteva je v sporu s stanjem strežnika.
      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`: neznan, že uporabljen ali potekel poskus prijave z Googlom ali vstopnica; znova začnite prijavo.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: sso_expired
            message: This sign-in has expired; start again from Scacelith.
    PayloadTooLarge:
      description: '`payload_too_large`: telo presega `HTTP_BODY_LIMIT` (privzeto 16.384 bajtov). Strežnik zapre povezavo.'
      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`: cilj zahteve presega 4096 znakov.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: uri_too_long
            message: The URL is too long.
    UnsupportedMediaType:
      description: '`unsupported_media_type`: telo ni `application/json` ali pa njegov nabor znakov ni 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`: partija ima več kot `GIF_MAX_PLIES` (600) polpotez.'
      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`: rešite izziv `pow` in isto zahtevo pošljite znova s `pow: { challenge, nonce }`. `reason` pove, zakaj: `required`, `malformed`, `signature`, `endpoint`, `network`, `expired`, `bits`, `work` ali `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` (omejitev števila zahtev, kvota računa ali čakalna vrsta za zgoščevanje gesel) ali `too_many_attempts` (omejitev na ravni računa), z `retryAfter` in glavo `Retry-After`.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            rateLimited:
              summary: Omejitev števila zahtev
              value:
                error: rate_limited
                message: Too many requests; try again later.
                retryAfter: 30
            tooManyAttempts:
              summary: Preveč neuspelih poskusov na tem računu
              value:
                error: too_many_attempts
                message: Too many attempts; wait before trying again.
                retryAfter: 4
    InternalError:
      description: '`internal_error`: nepričakovana napaka. Strežnik jo zabeleži v dnevnik.'
      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`: napačen `state` ali `iss` ali pa je Google zavrnil kodo ali poslal žeton ID, ki ne prestane preverjanja (vključno s kodo ali žetonom ID drugega odjemalca, igre ali spletnega mesta). Nikoli ne vsebuje Googlovega besedila.'
      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` ali `timeout`: poskusite znova po `retryAfter`, kadar je naveden.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            serverBusy:
              summary: Strežnik je zaseden
              value:
                error: server_busy
                message: The server is busy; try again in a few seconds.
                retryAfter: 9
            busy:
              summary: Zbirka podatkov je ostala zaklenjena (branje)
              value:
                error: busy
                message: Try again shortly.
                retryAfter: 1
            timeout:
              summary: Strežnik je potreboval preveč časa
              value:
                error: timeout
                message: The server took too long to answer; try again.
    GifFile:
      description: Animirani GIF kot priloga.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Content-Disposition:
          description: '`attachment; filename="scacelith-<id>.gif"` za partijo s tega strežnika, `attachment; filename="scacelith-game.gif"` za PGN, poslan s `POST /gif`.'
          schema:
            type: string
            pattern: '^attachment; filename="scacelith-([1-9][0-9]{0,15}|game)\.gif"$'
          example: attachment; filename="scacelith-4100000000001.gif"
        Content-Length:
          description: Velikost datoteke v bajtih.
          schema:
            type: integer
            minimum: 1
      content:
        image/gif:
          schema:
            type: string
            format: binary
    HtmlPage:
      description: Stran HTML.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlBadRequest:
      description: Stran HTML, ki pojasni zavrnitev.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlRequestTimeout:
      description: Obrazec ni prispel v 10 sekundah (stran HTML). Strežnik zapre povezavo.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Connection:
          $ref: '#/components/headers/ConnectionClose'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlConflict:
      description: Stran HTML, ki pojasni spor.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlPayloadTooLarge:
      description: Obrazec presega `HTTP_BODY_LIMIT` (stran HTML). Strežnik zapre povezavo.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Connection:
          $ref: '#/components/headers/ConnectionClose'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlUnsupportedMediaType:
      description: Telo ni niti obrazec (`application/x-www-form-urlencoded`) niti JSON ali pa njegov nabor znakov ni UTF-8 (stran HTML).
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlTooManyRequests:
      description: Omejitev števila zahtev (stran HTML), z `Retry-After`.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlInternalError:
      description: Nepričakovana napaka (stran HTML z naslovom »Server error«, tj. napaka strežnika). Strežnik jo zabeleži v dnevnik.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlServiceUnavailable:
      description: Strežnik je zaseden (zbirka podatkov je ostala zaklenjena ali pa strežnik močno zaostaja; z `Retry-After`) ali pa je potreboval preveč časa (stran HTML z naslovom »Server error«, tj. napaka strežnika).
      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: Enotna oblika vseh odgovorov z napako v API-ju JSON.
      required:
        - error
        - message
      properties:
        error:
          type: string
          pattern: '^[a-z][a-z0-9_]*$'
          description: Koda napake v obliki `snake_case`. Odjemalec na njeni podlagi izbere, kaj prikaže.
          examples:
            - rate_limited
        message:
          type: string
          description: Angleški stavek, namenjen dnevnikom in kot nadomestilo.
        retryAfter:
          type: integer
          minimum: 1
          description: Število sekund, ki jih je treba počakati pred ponovnim poskusom; prisotno samo pri zavrnitvah, ki s časom minejo. Odgovor ima takrat tudi glavo `Retry-After` z isto vrednostjo, razen pri 503 `busy` branj zgodovine, partije, igralca in lestvice.
        field:
          type: string
          description: Polje ali parameter poizvedbe, ki povzroča napako (`invalid_request`, `invalid_option` ter `invalid_cursor`, `invalid_limit` in `invalid_filter` pri `GET /account/games`). Gnezdeno polje je zapisano s piko (`pow.nonce`).
        reason:
          type: string
          description: 'Zakaj: pravilo, ki ga krši `weak_password`, ali zakaj je bil dokaz o delu zahtevan ali zavrnjen (`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: 'Samo `banned`: konec blokade (ms od epohe Unix), `null` za trajno blokado.'
        line:
          type: integer
          minimum: 1
          description: 'Samo `invalid_pgn`: vrstica napake, od 1.'
        column:
          type: integer
          minimum: 1
          description: 'Samo `invalid_pgn`: stolpec napake, od 1, v znakih.'
    PowChallenge:
      type: object
      description: Izziv dokaza o delu (`pow` odgovora 428 `pow_required`).
      required:
        - challenge
        - bits
        - expiresAt
      properties:
        challenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{16,400}\.[A-Za-z0-9_-]{43}$'
          description: Podpisani izziv, ki ga pošljete nazaj nespremenjenega.
        bits:
          type: integer
          minimum: 1
          maximum: 26
          description: Število vodilnih ničelnih bitov, ki jih mora imeti `SHA-256(challenge + ":" + nonce)`.
        expiresAt:
          type: integer
          format: int64
          description: Čas poteka izziva (ms od epohe Unix), 2 minuti po izdaji.
    PowAnswer:
      type: object
      description: Odgovor na izziv dokaza o delu.
      additionalProperties: false
      required:
        - challenge
        - nonce
      properties:
        challenge:
          type: string
          minLength: 16
          maxLength: 512
          description: '`challenge` iz odgovora 428, nespremenjen.'
        nonce:
          type: string
          pattern: '^[0-9]{1,20}$'
          description: Desetiški niz z največ 20 števkami, tak, da se `SHA-256(challenge + ":" + nonce)` začne z `bits` ničelnimi biti.
    ServerInfo:
      type: object
      description: Kar odjemalec potrebuje, preden se prijavi ali poveže.
      required:
        - name
        - serverId
        - motd
        - protocol
        - wsPort
        - wsPath
        - registration
        - emailVerification
        - sso
        - mfa
        - pow
        - categories
        - limits
      properties:
        name:
          type: string
          description: Ime strežnika (`SERVER_NAME`).
        serverId:
          type:
            - string
            - 'null'
          format: uuid
          description: UUID, ki ga zbirka podatkov dobi ob prvem zagonu. Ob ponovnih zagonih ostane enak. Je `null`, kadar ga zbirka podatkov ne more podati.
        motd:
          type: string
          description: Sporočilo dneva (`SERVER_MOTD`), lahko prazno.
        protocol:
          type: object
          description: Protokol WebSocket (`PROTOCOL.md`).
          required:
            - min
            - max
            - schema
            - subprotocol
          properties:
            min:
              type: integer
              minimum: 1
              description: Najstarejša različica protokola, ki jo strežnik podpira.
            max:
              type: integer
              minimum: 1
              description: Najnovejša različica protokola, ki jo strežnik podpira.
            schema:
              type: integer
              format: int64
              minimum: 0
              maximum: 4294967295
              description: 'Prstni odtis sheme protokola (prvi 4 bajti SHA-256 njene kanonične oblike kot nepredznačeno celo število: `0xc649140f`, 3326678031, za podrazličico protokola 2); samo informativno, nikoli se ne primerja.'
            subprotocol:
              type: string
              description: Identifikator podprotokola WebSocket (`Sec-WebSocket-Protocol`).
        wsPort:
          type: integer
          minimum: 0
          maximum: 65535
          description: Vrata WebSocket, ki jih uporabljajo igralci (`PUBLIC_WS_PORT`, sicer `WS_PORT`, sicer `API_PORT`).
        wsPath:
          type: string
          const: /ws
          description: Pot WebSocketa.
        registration:
          type: string
          enum:
            - open
            - closed
          description: Ali je mogoče ustvarjati nove račune.
        emailVerification:
          type: boolean
          description: Ali novi računi potrjujejo svoj naslov (`REQUIRE_EMAIL_VERIFICATION`).
        sso:
          type: object
          required:
            - google
            - googleWeb
          properties:
            google:
              type: boolean
              description: Ali je prijava z Googlom na voljo igri.
            googleWeb:
              type: boolean
              description: Ali je prijava z Googlom na voljo tudi spletnemu mestu (`POST /auth/sso/google/web/start`).
        mfa:
          type: boolean
          const: true
          description: Dvofaktorsko preverjanje pristnosti je vedno na voljo.
        pow:
          type: object
          required:
            - register
          properties:
            register:
              type: integer
              minimum: 0
              maximum: 26
              description: Biti dokaza o delu, ki jih zahteva registracija (0, če dokaz ni potreben).
        categories:
          type: array
          description: Uradni tempi igre, tj. tisti za rating (`RATED_CATEGORIES`), v vrstnem redu strežnika. Vsak drug tempo igre je `custom`.
          items:
            $ref: '#/components/schemas/Category'
        limits:
          type: object
          description: Pravila, ki jih odjemalec lahko preveri, preden pošlje obrazec.
          required:
            - usernameMin
            - usernameMax
            - usernamePattern
            - passwordMinLength
            - passwordMaxBytes
            - customTimeControls
            - reportsPerDay
            - wsMaxMessageBytes
          properties:
            usernameMin:
              type: integer
              description: Najmanjša dolžina uporabniškega imena (`USERNAME_MIN`).
            usernameMax:
              type: integer
              description: Največja dolžina uporabniškega imena (`USERNAME_MAX`).
            usernamePattern:
              type: string
              description: Regularni izraz, ki mu mora ustrezati novo uporabniško ime.
            passwordMinLength:
              type: integer
              description: Najmanjša dolžina gesla v znakih (`PASSWORD_MIN_LENGTH`).
            passwordMaxBytes:
              type: integer
              description: Največja dolžina gesla v bajtih UTF-8.
            customTimeControls:
              type: boolean
              description: Ali smejo izzivi in zasebne partije uporabljati tempe igre po meri.
            reportsPerDay:
              type: integer
              description: Koliko prijav kršitev lahko igralec vloži v 24 urah (`REPORTS_PER_DAY`).
            wsMaxMessageBytes:
              type: integer
              description: Največje sporočilo WebSocket, ki ga odjemalec sme poslati, v bajtih.
      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: Uradni tempo igre.
      required:
        - id
        - baseSec
        - incSec
      properties:
        id:
          type: string
          pattern: '^[0-9]+\+[0-9]+$'
          description: 'ID kategorije: minute in sekunde dodatka (`3+2`).'
        baseSec:
          type: number
          minimum: 0
          description: Osnovni čas v sekundah.
        incSec:
          type: number
          minimum: 0
          description: Dodatek na potezo v sekundah.
    AccountView:
      type: object
      description: Račun, kot ga vidi njegov igralec (`user` v odgovorih na prijavo in v `GET /account/me`).
      required:
        - id
        - username
        - email
        - emailVerified
        - mfaEnabled
        - googleLinked
        - hasPassword
        - acceptChallenges
        - createdAt
        - lastLoginAt
        - pendingEmail
      properties:
        id:
          type: integer
          format: int64
          description: ID računa.
        username:
          type: string
          description: Uporabniško ime.
        email:
          type: string
          format: email
          description: E-poštni naslov.
        emailVerified:
          type: boolean
          description: Ali je naslov potrjen.
        mfaEnabled:
          type: boolean
          description: Ali je dvofaktorsko preverjanje pristnosti vklopljeno.
        googleLinked:
          type: boolean
          description: Ali je povezan Googlov račun.
        hasPassword:
          type: boolean
          description: '`false` za račun, ustvarjen prek Googla, ki še nima nastavljenega gesla.'
        acceptChallenges:
          type: string
          enum:
            - all
            - none
          description: Ali račun sprejema neposredne izzive po imenu (glejte `PUT /account/preferences`).
        createdAt:
          type: integer
          format: int64
          description: Čas ustvarjenja računa (ms od epohe Unix).
        lastLoginAt:
          type:
            - integer
            - 'null'
          format: int64
          description: Zadnja prijava (ms od epohe Unix) ali `null`.
        pendingEmail:
          type:
            - string
            - 'null'
          format: email
          description: Novi naslov spremembe e-poštnega naslova, ki čaka na potrditev prek povezave, ali `null`.
    SessionAnswer:
      type: object
      description: Nova seja.
      required:
        - token
        - expiresAt
        - user
      properties:
        token:
          type: string
          pattern: '^sct_[A-Za-z0-9_-]{43}$'
          description: Žeton seje za glavo `Authorization` in WebSocket.
        expiresAt:
          type: integer
          format: int64
          description: Absolutni konec seje (ms od epohe Unix); omejitev nedejavnosti jo lahko konča prej.
        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: Drugi korak prijave z dvofaktorskim preverjanjem pristnosti; nadaljujte 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: Žeton koraka za `POST /auth/login/mfa`.
        expiresIn:
          type: integer
          const: 300
          description: Preostale sekunde za dokončanje koraka.
    LoginAnswer:
      description: Seja ali drugi korak prijave z dvofaktorskim preverjanjem pristnosti.
      oneOf:
        - $ref: '#/components/schemas/SessionAnswer'
        - $ref: '#/components/schemas/MfaChallenge'
    SsoNeedsUsername:
      type: object
      description: Prva prijava z Googlom; nadaljujte 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: Vstopnica za `POST /auth/sso/complete`, veljavna 10 minut.
        suggestedUsername:
          type: string
          description: Uporabniško ime, sestavljeno iz imena pri Googlu ali iz naslova, ali `""`, kadar nič ne ustreza.
    SsoNeedsPassword:
      type: object
      description: Naslov uporablja račun z geslom; nadaljujte s `POST /auth/sso/google/link`. Nič še ni povezano.
      required:
        - needsPassword
        - linkTicket
        - username
        - expiresIn
      properties:
        needsPassword:
          type: boolean
          const: true
        linkTicket:
          type: string
          pattern: '^sso_[A-Za-z0-9_-]{43}$'
          description: Vstopnica za `POST /auth/sso/google/link`.
        username:
          type: string
          description: Uporabniško ime tega računa (vidi ga samo tisti, ki je Googlu dokazal lastništvo naslova).
        expiresIn:
          type: integer
          const: 600
          description: Koliko sekund vstopnica ostane veljavna.
    SsoFinishAnswer:
      description: Odgovor `POST /auth/sso/google/finish`.
      oneOf:
        - $ref: '#/components/schemas/SessionAnswer'
        - $ref: '#/components/schemas/MfaChallenge'
        - $ref: '#/components/schemas/SsoNeedsUsername'
        - $ref: '#/components/schemas/SsoNeedsPassword'
    SsoStartAnswer:
      type: object
      description: Poskus prijave z Googlom.
      required:
        - attemptId
        - authUrl
        - state
        - expiresIn
      properties:
        attemptId:
          type: string
          pattern: '^sso_[A-Za-z0-9_-]{43}$'
          description: Poskus za `POST /auth/sso/google/finish`.
        authUrl:
          type: string
          format: uri
          description: URL Googla, ki ga po preverjanju odprete v sistemskem brskalniku.
        state:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: '`state`, ki ga mora Google poslati nazaj.'
        expiresIn:
          type: integer
          const: 600
          description: Koliko sekund poskus ostane veljaven.
    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: Uporabniško ime; nato še pravila strežnika (glejte opis).
        email:
          type: string
          minLength: 1
          maxLength: 254
          description: E-poštni naslov. Presledki na začetku in koncu se odstranijo, shrani pa se z malimi črkami.
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Geslo; nato še pravila za gesla.
        pow:
          $ref: '#/components/schemas/PowAnswer'
    LoginRequest:
      type: object
      additionalProperties: false
      required:
        - login
        - password
      properties:
        login:
          type: string
          minLength: 1
          maxLength: 254
          description: Uporabniško ime ali e-poštni naslov (vsako besedilo z `@`).
        password:
          type: string
          minLength: 1
          maxLength: 1024
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
        pow:
          $ref: '#/components/schemas/PowAnswer'
    ClientLabel:
      type: string
      maxLength: 64
      description: Neobvezno. Prikaže se na seznamu prijavljenih naprav (na primer `Scacelith 1.4 (Windows)`).
    MfaLoginRequest:
      type: object
      additionalProperties: false
      required:
        - mfaToken
      properties:
        mfaToken:
          type: string
          minLength: 1
          maxLength: 64
          description: '`mfaToken` iz odgovora na prijavo (ali iz odgovora na prijavo z Googlom).'
        code:
          type: string
          maxLength: 32
          description: Neobvezno. 6-mestna koda iz aplikacije ali obnovitvena koda.
        recoveryCode:
          type: string
          maxLength: 32
          description: Neobvezno. Obnovitvena koda. Potreben je eden od `code` in `recoveryCode`.
    EmailRequest:
      type: object
      additionalProperties: false
      required:
        - email
      properties:
        email:
          type: string
          minLength: 1
          maxLength: 254
          description: E-poštni naslov.
    PasswordResetRequest:
      type: object
      additionalProperties: false
      required:
        - token
        - newPassword
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: Parameter `token` povezave za ponastavitev.
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Novo geslo; veljajo pravila za gesla pri registraciji.
    SsoStartRequest:
      type: object
      additionalProperties: false
      required:
        - codeChallenge
        - redirectPort
      properties:
        codeChallenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: Izziv PKCE S256 odjemalčevega `codeVerifier` (`BASE64URL(SHA-256(codeVerifier))`).
        redirectPort:
          type: integer
          minimum: 1024
          maximum: 65535
          description: Vrata odjemalčevega poslušalca na `127.0.0.1`.
    SsoWebStartRequest:
      type: object
      additionalProperties: false
      required:
        - codeChallenge
      properties:
        codeChallenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: Izziv PKCE S256 za `codeVerifier` strani (`BASE64URL(SHA-256(codeVerifier))`).
    SsoFinishRequest:
      type: object
      additionalProperties: false
      required:
        - attemptId
        - codeVerifier
        - state
        - code
      properties:
        attemptId:
          type: string
          minLength: 1
          maxLength: 64
          description: '`attemptId` iz `POST /auth/sso/google/start`.'
        codeVerifier:
          type: string
          pattern: '^[A-Za-z0-9._~-]{43,128}$'
          description: Preverjevalnik PKCE; njegov SHA-256 se mora ujemati z izzivom, podanim `start`.
        state:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: '`state`, kot ga je Google poslal nazaj.'
        code:
          type: string
          minLength: 1
          maxLength: 2048
          pattern: '^[\x21-\x7E]+$'
          description: '`code`, kot ga je Google poslal nazaj (natisljivi znaki ASCII brez presledkov).'
        iss:
          type: string
          minLength: 1
          maxLength: 256
          description: Neobvezno. `iss`, kot ga je Google poslal nazaj, če ga je.
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
    SsoLinkRequest:
      type: object
      additionalProperties: false
      required:
        - linkTicket
        - password
      properties:
        linkTicket:
          type: string
          minLength: 1
          maxLength: 64
          description: '`linkTicket` iz `finish`, ki velja 10 minut.'
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Geslo računa.
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
        pow:
          $ref: '#/components/schemas/PowAnswer'
    SsoCompleteRequest:
      type: object
      additionalProperties: false
      required:
        - ssoTicket
        - username
      properties:
        ssoTicket:
          type: string
          minLength: 1
          maxLength: 64
          description: '`ssoTicket` iz `finish`, ki velja 10 minut.'
        username:
          type: string
          minLength: 1
          maxLength: 64
          description: Uporabniško ime; veljajo pravila registracije.
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
    SessionList:
      type: object
      required:
        - sessions
      properties:
        sessions:
          type: array
          description: Aktivne seje, od nazadnje uporabljene naprej.
          items:
            $ref: '#/components/schemas/SessionEntry'
    SessionEntry:
      type: object
      description: Aktivna seja (prijavljena naprava).
      required:
        - id
        - createdAt
        - lastSeenAt
        - expiresAt
        - clientLabel
        - current
      properties:
        id:
          type: integer
          format: int64
          description: ID seje za `DELETE /auth/sessions/{id}`.
        createdAt:
          type: integer
          format: int64
          description: Čas prijave (ms od epohe Unix).
        lastSeenAt:
          type: integer
          format: int64
          description: Zadnja uporaba (ms od epohe Unix), posodobljena največ na vsakih 5 minut (pozneje, dokler strežnik močno zaostaja).
        expiresAt:
          type: integer
          format: int64
          description: Absolutni konec (ms od epohe Unix); omejitev nedejavnosti lahko sejo konča prej.
        clientLabel:
          type:
            - string
            - 'null'
          description: '`clientLabel` prijave ali `null`, kadar ga prijava ni poslala.'
        current:
          type: boolean
          description: Ali je to seja, ki pošilja zahtevo.
    AccountMe:
      type: object
      required:
        - user
        - ratings
        - sanctions
        - ban
      properties:
        user:
          $ref: '#/components/schemas/AccountView'
        ratings:
          type: array
          description: En zapis za vsako kategorijo, v kateri je igralec igral partije za rating.
          items:
            $ref: '#/components/schemas/RatingSummary'
        sanctions:
          type: array
          description: Aktivne sankcije.
          items:
            $ref: '#/components/schemas/ActiveSanction'
        ban:
          description: '`{ until }` med blokado, sicer `null`.'
          oneOf:
            - $ref: '#/components/schemas/Ban'
            - type: 'null'
    RatingSummary:
      type: object
      description: Zapis ratinga ene kategorije.
      required:
        - category
        - rating
        - games
        - wins
        - draws
        - losses
        - peak
        - provisional
      properties:
        category:
          type: string
          description: ID kategorije.
        rating:
          type: integer
          description: Rating.
        games:
          type: integer
          minimum: 0
          description: Partije, odigrane v kategoriji (upoštevane ali ne).
        wins:
          type: integer
          minimum: 0
        draws:
          type: integer
          minimum: 0
        losses:
          type: integer
          minimum: 0
        peak:
          type: integer
          description: Najvišji doseženi rating.
        provisional:
          type: boolean
          description: '`true`, dokler rating še ni določen ali ima manj kot `PROVISIONAL_GAMES` upoštevanih partij (igra ga prikaže kot `1510?`).'
    ActiveSanction:
      type: object
      required:
        - kind
        - reason
        - startsAt
        - endsAt
      properties:
        kind:
          type: string
          enum:
            - ban
            - mm_block
            - warning
          description: Vrsta sankcije.
        reason:
          type:
            - string
            - 'null'
          description: Navedeni razlog, če obstaja.
        startsAt:
          type: integer
          format: int64
          description: Začetek (ms od epohe Unix).
        endsAt:
          type:
            - integer
            - 'null'
          format: int64
          description: Konec (ms od epohe Unix), `null`, kadar je sankcija trajna.
    Ban:
      type: object
      required:
        - until
      properties:
        until:
          type:
            - integer
            - 'null'
          format: int64
          description: Konec blokade (ms od epohe Unix), `null`, kadar je trajna.
    Preferences:
      type: object
      additionalProperties: false
      required:
        - acceptChallenges
      properties:
        acceptChallenges:
          type: string
          enum:
            - all
            - none
          description: '`all` za sprejemanje neposrednih izzivov po imenu, `none` za njihovo zavračanje.'
    PasswordChangeRequest:
      type: object
      additionalProperties: false
      required:
        - currentPassword
        - newPassword
      properties:
        currentPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Trenutno geslo.
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Novo geslo; veljajo pravila za gesla pri registraciji.
    PasswordOnlyRequest:
      type: object
      additionalProperties: false
      required:
        - password
      properties:
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Geslo računa.
    TotpEnableRequest:
      type: object
      additionalProperties: false
      required:
        - code
      properties:
        code:
          type: string
          pattern: '^[0-9]{6}$'
          description: Koda čakajočega skrivnega ključa, natanko 6 števk.
    ReauthCredentials:
      type: object
      additionalProperties: false
      required:
        - password
      properties:
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Geslo računa.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: Neobvezno; potrebno pri dvofaktorskem preverjanju pristnosti (ali `recoveryCode`). Koda iz aplikacije ali obnovitvena koda.
        recoveryCode:
          type: string
          minLength: 1
          maxLength: 32
          description: Neobvezno. Obnovitvena koda (`xxxx-xxxx-xx`; velike in male črke, presledki in vezaji niso pomembni).
    RecoveryCodesRequest:
      type: object
      additionalProperties: false
      required:
        - password
        - code
      properties:
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Geslo računa.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: Koda iz aplikacije (obnovitvena koda je zavrnjena).
    EmailChangeRequest:
      type: object
      additionalProperties: false
      required:
        - newEmail
        - password
      properties:
        newEmail:
          type: string
          minLength: 1
          maxLength: 254
          description: Novi naslov; presledki na začetku in koncu se odstranijo, pretvori se v male črke, nato pa se preveri kot naslov pri registraciji.
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Geslo računa.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: Neobvezno; potrebno pri dvofaktorskem preverjanju pristnosti (ali `recoveryCode`). Koda iz aplikacije ali obnovitvena koda.
        recoveryCode:
          type: string
          minLength: 1
          maxLength: 32
          description: Neobvezno. Obnovitvena koda.
    TotpSetup:
      type: object
      description: Čakajoči skrivni ključ za aplikacijo za preverjanje pristnosti.
      required:
        - secret
        - uri
        - algorithm
        - digits
        - period
      properties:
        secret:
          type: string
          pattern: '^[A-Z2-7]+$'
          description: Skrivni ključ v base32 za vnos v aplikacijo.
        uri:
          type: string
          format: uri
          description: URI `otpauth://totp/...` za prikaz kot koda QR.
        algorithm:
          type: string
          const: SHA1
        digits:
          type: integer
          const: 6
        period:
          type: integer
          const: 30
          description: Sekunde na korak.
    RecoveryCodeList:
      type: array
      description: 10 obnovitvenih kod, prikazanih samo tokrat. Vsaka deluje enkrat.
      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: Ena stran partije.
      required:
        - name
        - rating
        - ratingAfter
        - ratingDiff
      properties:
        name:
          type: string
          description: Ime v zapisu partije (`deleted#<id>` za izbrisan račun).
        rating:
          type:
            - integer
            - 'null'
          description: Rating na začetku, `null`, kadar ni znan.
        ratingAfter:
          type:
            - integer
            - 'null'
          description: Rating po partiji. Partija za rating ga ima vedno; `null` samo pri partiji, ki ne šteje za rating (prijateljska, s tempom igre po meri, preklicana).
        ratingDiff:
          type:
            - integer
            - 'null'
          description: Sprememba ratinga, `0`, kadar pravila ratinga pustijo rating nespremenjen (ničelni rezultat po pravilih FIDE, nasprotnik brez določenega ratinga, prve partije igralca brez določenega ratinga); `null` kot pri `ratingAfter`.
    GameSummary:
      type: object
      description: Povzetek shranjene partije.
      required:
        - id
        - category
        - rated
        - timeControl
        - white
        - black
        - status
        - reason
        - result
        - termination
        - plies
        - startedAt
        - endedAt
      properties:
        id:
          type: integer
          format: int64
          minimum: 1
          maximum: 9007199254740991
          description: ID partije.
        category:
          type: string
          description: ID uradne kategorije (`3+2`) ali `custom`.
        rated:
          type: boolean
          description: Ali partija šteje za rating.
        timeControl:
          type: string
          pattern: '^[0-9]+\+[0-9]+$'
          description: Tempo igre v sekundah (`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` (glejte kode partij).'
        reason:
          type: integer
          minimum: 0
          maximum: 255
          description: Koda razloga za konec (glejte kode partij).
        result:
          type: string
          enum:
            - 1-0
            - 0-1
            - 1/2-1/2
            - '*'
          description: Rezultat, `*` za preklicano partijo.
        termination:
          $ref: '#/components/schemas/Termination'
        plies:
          type: integer
          minimum: 0
          description: Število polpotez.
        startedAt:
          type: integer
          format: int64
          description: Začetek (ms od epohe Unix).
        endedAt:
          type: integer
          format: int64
          description: Konec (ms od epohe Unix).
    Termination:
      type: string
      description: Ime razloga za konec (glejte kode partij); `Unknown` za kodo, ki je protokol ne pozna.
      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: Povzetek partije z vidika enega igralca.
      allOf:
        - $ref: '#/components/schemas/GameSummary'
        - type: object
          required:
            - color
          properties:
            color:
              type: string
              enum:
                - white
                - black
              description: Barva igralca.
    HistoryGameSummary:
      description: Partija iz zgodovine prijavljenega igralca.
      allOf:
        - $ref: '#/components/schemas/PlayerGameSummary'
        - type: object
          required:
            - baseMs
            - incMs
            - outcome
          properties:
            baseMs:
              type: integer
              format: int64
              minimum: 0
              description: Osnovni čas v milisekundah.
            incMs:
              type: integer
              format: int64
              minimum: 0
              description: Dodatek v milisekundah.
            outcome:
              type: string
              enum:
                - win
                - loss
                - draw
                - aborted
              description: Izid z vidika igralca.
    HistoryPage:
      type: object
      required:
        - games
        - next
        - total
      properties:
        games:
          type: array
          description: Partije na strani, od najnovejše naprej.
          items:
            $ref: '#/components/schemas/HistoryGameSummary'
        next:
          type:
            - integer
            - 'null'
          format: int64
          description: ID, ki ga pošljete kot `before` za naslednjo stran, ali `null` na zadnji strani.
        total:
          type: integer
          minimum: 0
          description: Število partij, ki ustrezajo filtru, na vseh straneh.
    MoveRecord:
      type: object
      description: Ena polpoteza partije.
      required:
        - uci
        - spentMs
        - clockMs
      properties:
        uci:
          type: string
          pattern: '^[a-h][1-8][a-h][1-8][nbrq]?$'
          description: Poteza v notaciji UCI, pri promociji z dodanim `n`, `b`, `r` ali `q`.
        spentMs:
          type:
            - integer
            - 'null'
          minimum: 0
          description: Čas, obračunan za potezo (ms), `null`, kadar ga zapis nima.
        clockMs:
          type:
            - integer
            - 'null'
          minimum: 0
          description: Ura igralca, ki je potezo odigral, po potezi (ms), `null`, kadar je zapis nima.
    PgnTagsView:
      type: object
      description: Glavne oznake PGN za prikaz. `Termination` je tu ime razloga za konec; datoteka PGN uporablja standardne vrednosti PGN.
      required:
        - Event
        - Site
        - Date
        - Round
        - White
        - Black
        - Result
        - WhiteElo
        - BlackElo
        - TimeControl
        - Termination
        - PlyCount
      properties:
        Event:
          type: string
          description: '`<SERVER_NAME> rated <category>` ali `<SERVER_NAME> casual <category>`.'
        Site:
          type: string
          description: '`SERVER_PUBLIC_HOST`.'
        Date:
          type: string
          pattern: '^[0-9]{4}\.[0-9]{2}\.[0-9]{2}$'
          description: Datum začetka po UTC.
        Round:
          type: string
          const: '-'
        White:
          type: string
        Black:
          type: string
        Result:
          type: string
          enum:
            - 1-0
            - 0-1
            - 1/2-1/2
            - '*'
        WhiteElo:
          description: Rating na začetku ali `"-"`.
          oneOf:
            - type: integer
            - type: string
              const: '-'
        BlackElo:
          description: Rating na začetku ali `"-"`.
          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: Zapis partije s potezami in urami. Ima polja povzetka partije, brez `color` in `outcome`.
      allOf:
        - $ref: '#/components/schemas/GameSummary'
        - type: object
          required:
            - baseMs
            - incMs
            - statusName
            - rematchOf
            - moves
            - pgn
          properties:
            baseMs:
              type: integer
              format: int64
              minimum: 0
              description: Osnovni čas v milisekundah.
            incMs:
              type: integer
              format: int64
              minimum: 0
              description: Dodatek v milisekundah.
            statusName:
              type: string
              enum:
                - WhiteWins
                - BlackWins
                - Draw
                - Aborted
              description: Ime vrednosti `status`.
            rematchOf:
              type:
                - integer
                - 'null'
              format: int64
              description: ID partije, katere revanša je ta partija, ali `null`.
            moves:
              type: array
              description: En vnos za vsako polpotezo.
              items:
                $ref: '#/components/schemas/MoveRecord'
            pgn:
              $ref: '#/components/schemas/PgnTagsView'
            you:
              type: string
              enum:
                - white
                - black
              description: Samo, kadar je igralec, ki mu pripada žeton seje, igral to partijo; njegova barva.
            reportable:
              type: boolean
              description: Samo, kadar je igralec, ki mu pripada žeton seje, igral to partijo; `true`, kadar bi `POST /reports` zdaj sprejel prijavo nasprotnika za to partijo (partija se je končala v zadnjih 7 dneh, dnevna kvota ni porabljena in zoper nasprotnika za to partijo še ni bila vložena prijava).
    PlayerProfile:
      type: object
      required:
        - username
        - createdAt
        - ratings
        - games
      properties:
        username:
          type: string
          description: Uporabniško ime, kot ga je zapisal igralec.
        createdAt:
          type: integer
          format: int64
          description: Čas ustvarjenja računa (ms od epohe Unix).
        ratings:
          type: array
          description: Samo uradne kategorije v vrstnem redu strežnika.
          items:
            $ref: '#/components/schemas/RatingSummary'
        games:
          type: object
          required:
            - total
            - rated
            - wins
            - draws
            - losses
          properties:
            total:
              type: integer
              minimum: 0
              description: Vse shranjene partije, vključno s prijateljskimi in preklicanimi.
            rated:
              type: integer
              minimum: 0
              description: Partije za rating, seštete prek zapisov ratinga.
            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: Uporabniško ime, kot ga je zapisal igralec.
        games:
          type: array
          description: Partije na strani, od najnovejše naprej.
          items:
            $ref: '#/components/schemas/PlayerGameSummary'
        next:
          type:
            - integer
            - 'null'
          format: int64
          description: ID zadnje partije, kadar je stran polna (naslednja stran je takrat lahko prazna), sicer `null`.
    Leaderboard:
      type: object
      required:
        - category
        - minGames
        - updatedAt
        - players
      properties:
        category:
          type: string
          description: ID kategorije.
        minGames:
          type: integer
          minimum: 0
          description: Število upoštevanih partij, ki jih zapis potrebuje za uvrstitev na lestvico (`PROVISIONAL_GAMES`).
        updatedAt:
          type: integer
          format: int64
          description: Čas izračuna lestvice (ms od epohe Unix).
        players:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/LeaderboardEntry'
    LeaderboardEntry:
      type: object
      required:
        - rank
        - username
        - rating
        - games
        - wins
        - draws
        - losses
        - peak
      properties:
        rank:
          type: integer
          minimum: 1
        username:
          type: string
        rating:
          type: integer
        games:
          type: integer
          minimum: 0
        wins:
          type: integer
          minimum: 0
        draws:
          type: integer
          minimum: 0
        losses:
          type: integer
          minimum: 0
        peak:
          type: integer
    GifRequest:
      type: object
      additionalProperties: false
      required:
        - pgn
      properties:
        pgn:
          type: string
          description: Besedilo PGN, največ 65.536 bajtov UTF-8. Uporabi se samo prva partija.
        size:
          type: string
          enum:
            - small
            - medium
            - large
          default: medium
          description: Neobvezno. Velikost slike.
        orientation:
          type: string
          enum:
            - white
            - black
          default: white
          description: Neobvezno. Barva, ki je na šahovnici spodaj.
        delayMs:
          type: integer
          minimum: 100
          maximum: 3000
          default: 500
          description: Neobvezno. Milisekunde na potezo, število JSON s celoštevilsko vrednostjo.
        coords:
          type: boolean
          default: true
          description: Neobvezno. Ali so črke linij in številke vrst narisane okoli šahovnice.
    ReportRequest:
      type: object
      required:
        - gameId
        - reported
        - category
      properties:
        gameId:
          description: Partija, podana kot celo število ali kot niz z 1 do 16 števkami.
          oneOf:
            - type: integer
              format: int64
              minimum: 1
              maximum: 9007199254740991
            - type: string
              pattern: '^[0-9]{1,16}$'
        reported:
          type: string
          minLength: 1
          maxLength: 24
          description: Uporabniško ime nasprotnika, kot je v zapisu partije ali kot je zdaj, ne glede na velike in male črke. Ne sme biti prazno.
        category:
          type: string
          enum:
            - cheating
            - abuse
            - other
          description: Na kaj se prijava kršitve nanaša.
        comment:
          type:
            - string
            - 'null'
          description: Neobvezno. Največ 500 znakov po odstranitvi kontrolnih znakov (razen tabulatorja in pomika v novo vrstico) ter presledkov na začetku in koncu besedila.
    AccountExport:
      type: object
      description: Vse, kar strežnik hrani o računu (časi v ms od epohe Unix).
      required:
        - format
        - version
        - exportedAt
        - server
        - notes
        - account
        - ratings
        - ratingRefunds
        - sessions
        - securityEvents
        - sanctions
        - conduct
        - reportsFiled
        - games
      properties:
        format:
          type: string
          const: scacelith-account-export
        version:
          type: integer
          const: 1
        exportedAt:
          type: integer
          format: int64
          description: Čas izdelave izvoza.
        server:
          type: object
          required:
            - name
            - host
          properties:
            name:
              type: string
              description: '`SERVER_NAME`.'
            host:
              type: string
              description: '`SERVER_PUBLIC_HOST`.'
        notes:
          type: array
          description: Kaj datoteka vsebuje in kaj izpušča, v preprosti angleščini za igralca.
          items:
            type: string
        account:
          description: Pogled računa iz `GET /account/me`, z `googleEmail`.
          allOf:
            - $ref: '#/components/schemas/AccountView'
            - type: object
              required:
                - googleEmail
              properties:
                googleEmail:
                  type:
                    - string
                    - 'null'
                  description: Naslov povezanega Googlovega računa ali `null`.
        ratings:
          type: array
          description: Celotni zapisi ratinga.
          items:
            $ref: '#/components/schemas/ExportRating'
        ratingRefunds:
          type: array
          description: Točke ratinga, vrnjene, potem ko je bilo ugotovljeno, da je nasprotnik goljufal, seštete po dnevih (UTC) in kategorijah, od najnovejših naprej. Niti partije niti goljufi niso navedeni.
          items:
            $ref: '#/components/schemas/RatingRefund'
        sessions:
          type: array
          description: Vse shranjene seje, od najnovejše naprej, brez kakršnega koli žetona. Čiščenje po pravilih hrambe izbriše potečeno sejo, odjavljeno pa dan po odjavi.
          items:
            $ref: '#/components/schemas/ExportSession'
        securityEvents:
          type: array
          description: Varnostni dogodki, od najnovejšega naprej, hranjeni `RETENTION_SECURITY_DAYS` dni.
          items:
            $ref: '#/components/schemas/SecurityEvent'
        sanctions:
          type: array
          description: Vse sankcije, vključno z odpravljenimi. Ime moderatorja ni nikoli vključeno.
          items:
            $ref: '#/components/schemas/ExportSanction'
        conduct:
          type: array
          description: Dogodki vedenja, zabeleženi za igralčeve zapuščene in preklicane partije ter partije brez pravočasne prve poteze, hranjeni 30 dni.
          items:
            $ref: '#/components/schemas/ConductEvent'
        reportsFiled:
          type: array
          description: Prijave kršitev, ki jih je vložil igralec.
          items:
            $ref: '#/components/schemas/FiledReport'
        games:
          type: object
          required:
            - total
            - list
          properties:
            total:
              type: integer
              minimum: 0
              description: Število partij.
            list:
              type: array
              description: Vse partije, od najnovejše naprej, kot povzetki iz `GET /account/games`. Poteze so na voljo prek `GET /games/{id}`, PGN pa prek `GET /games/{id}/pgn`.
              items:
                $ref: '#/components/schemas/HistoryGameSummary'
    ExportRating:
      description: Celoten zapis ratinga.
      allOf:
        - $ref: '#/components/schemas/RatingSummary'
        - type: object
          required:
            - rated
            - countedGames
            - updatedAt
          properties:
            rated:
              type: boolean
              description: Ali je igralec že zapustil fazo brez določenega ratinga.
            countedGames:
              type: integer
              minimum: 0
              description: Partije, ki so bile upoštevane v ratingu.
            updatedAt:
              type: integer
              format: int64
              description: Čas zadnje spremembe zapisa.
    RatingRefund:
      type: object
      required:
        - day
        - category
        - points
      properties:
        day:
          type: integer
          format: int64
          description: 00.00 UTC tega dne (ms od epohe Unix).
        category:
          type: string
        points:
          type: integer
          description: Točke, vrnjene tistega dne v tisti kategoriji.
    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: Čas odjave seje ali `null`.
        clientLabel:
          type:
            - string
            - 'null'
        ip:
          type:
            - string
            - 'null'
          description: Naslov IP prijave, izbrisan po `RETENTION_IP_DAYS` dneh.
    SecurityEvent:
      type: object
      description: |-
        Varnostni dogodek. `ip` je naveden samo za dejanja, opravljena v prijavljenem stanju, z geslom računa (in drugim faktorjem) ali s povezavo, poslano na njegov naslov: `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` in `account_exported`; vse druge vrste imajo `ip: null`, `ip` pa se izbriše po `RETENTION_IP_DAYS` dneh.

        `detail` ohrani samo ta polja: `login`: `method`; `sso_login`, `sso_account_created`: `provider`; `sso_linked`: `provider` in `method` (`password` ali `password+totp`); `login_failed`: `failures`; `login_lockout`: `retryAfterMs`; `mfa_failed`: `attempts`; `recovery_code_used`: `remaining`; `reauth_failed`: `factor`; `session_revoked` in `sessions_revoked_all`: `reason`; `email_change_refused`: `reason`; `sanction_auto`: `kind`, `gameId`, `until`. Dogodek `moderator_action` ohrani samo `{ action }`, za dejanja `ban`, `unban`, `reset_mfa`, `verify_email` in `revoke_sessions` (druga dejanja moderatorjev so izpuščena). Vse druge vrste imajo `detail: null`. Dogodki `rating_refund` so izpuščeni: njihove točke so v `ratingRefunds`.
      required:
        - kind
        - at
        - ip
        - detail
      properties:
        kind:
          type: string
          description: Vrsta dogodka (`login`, `password_changed` ...).
        at:
          type: integer
          format: int64
          description: Čas dogodka.
        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: Trenutno javno ime igralca, zoper katerega je bila vložena prijava.
        category:
          type: string
          enum:
            - cheating
            - abuse
            - other
        comment:
          type:
            - string
            - 'null'
        createdAt:
          type: integer
          format: int64
        status:
          type: string
          enum:
            - open
            - closed
          description: Ali je prijava kršitve še odprta (ali je bil igralec, zoper katerega je bila vložena, sankcioniran, ni navedeno).
    LinkTokenForm:
      type: object
      additionalProperties: false
      required:
        - token
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: Žeton povezave iz e-poštnega sporočila (skrito polje obrazca strani).
    ResetPasswordForm:
      type: object
      additionalProperties: false
      required:
        - token
        - newPassword
        - confirmPassword
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: Žeton povezave za ponastavitev (skrito polje obrazca).
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Novo geslo; veljajo pravila za gesla pri registraciji.
        confirmPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Ponovno novo geslo; biti mora enako.
    HtmlDocument:
      type: string
      contentMediaType: text/html
      description: Stran HTML v UTF-8 (`text/html; charset=utf-8`), brez JavaScripta ali zunanjih virov.
