openapi: 3.1.1
info:
  title: Scacelith sunucusu HTTPS API'si
  version: "1.1.0"
  summary: "Her Scacelith sunucusunun HTTPS API'si: resmî sunucu ve topluluk sunucuları."
  description: |-
    Her Scacelith sunucusu, resmî `caissa.scacelith.com` de her topluluk sunucusu da, bu HTTPS API'sini sunar. Scacelith oyunu bu API'yi, oynanan oyunun kendisi dışındaki her şey için kullanır: kayıt ve giriş (iki faktörlü kimlik doğrulama ve Google dahil), hesap sayfası, oyun geçmişi, oyunların PGN indirmeleri ve animasyonlu GIF'leri, giriş yapılmış cihazlar, veri indirme, hesabın silinmesi ve bildirimler.

    Canlı oyun (eşleştirme, meydan okumalar, hamleler, saatler) aynı sunucunun WebSocket'i (`wss://<host>/ws`) üzerinden yürür; bu WebSocket, bu API'den alınan bir oturum token'ıyla açılır ve burada değil, `PROTOCOL.md` belgesinde açıklanır.

    Aşağıdaki varsayılan değerler, yapılandırmasına dokunulmamış bir sunucunun değerleridir; bir topluluk sunucusu bunları değiştirebilir (her ayar `CONFIG.md` belgesinde listelenir).

    ## Temel URL, port ve aktarım

    - Resmî sunucu: `https://caissa.scacelith.com/api/v1`, TCP portu 443.
    - Bir topluluk sunucusu: `https://<SERVER_PUBLIC_HOST>[:<port>]/api/v1`. `API_PORT` varsayılan olarak 443'tür; bir NAT ya da proxy arkasında oyuncuların kullandığı port `PUBLIC_API_PORT` ayarıdır.
    - Oyunun WebSocket'i varsayılan olarak aynı porttadır (`WS_PORT` onu başka bir porta taşır; `GET /info` istemciye nerede olduğunu bildirir).
    - TLS üzerinden HTTP/1.1. `TLS_MODE=proxy` ile TLS'yi sunucunun önündeki bir ters proxy sonlandırır. `TLS_MODE=off` (düz HTTP) yalnızca yerel geliştirme içindir.
    - E-postalardaki bağlantılardan açılan HTML sayfaları (`/verify-email`, `/reset-password`, `/confirm-email-change`) `/api/v1` dışında, sunucunun kökünde bulunur. Sağlık kontrolü uç noktaları hem kökte hem `/api/v1` altında yanıt verir. Google ile girişin sunucuda bir sayfası yoktur: Google tarayıcıyı `127.0.0.1` üzerindeki oyunun kendisine ya da girişi başlatan web sitesinin bir sayfasına (`GOOGLE_WEB_REDIRECT_URI`) geri gönderir.
    - Sondaki eğik çizgi yok sayılır (`/api/v1/info/` ile `/api/v1/info` aynıdır) ve yol parametrelerinin URL kodlaması çözülür (geçerli bir URL kodlaması olmayan parametre 400 `invalid_request` ile yanıtlanır).

    ## İstekler

    - **JSON gövdeleri.** `POST`, `PUT` ve `DELETE` istekleri `Content-Type: application/json` ile bir JSON nesnesi taşır. UTF-8 dışında bir `charset` reddedilir, başka herhangi bir içerik türü de (415 `unsupported_media_type`). Boş bir gövde `{}` sayılır; parametresiz uç noktaların aldığı da budur (`POST /auth/logout`, `POST /auth/logout-all`, `DELETE /auth/sessions/{id}`).
    - **Boyut ve süre.** Bir gövde `HTTP_BODY_LIMIT` bayt ile sınırlıdır (varsayılan 16.384; `POST /gif` için ayrı bir 135.168 bayt sınırı vardır): aşılırsa 413 `payload_too_large`. Gövde 10 saniye içinde gelmelidir: gelmezse 408 `request_timeout`. Bu iki hatadan herhangi birinin ardından sunucu bağlantıyı kapatır.
    - **Katı şemalar.** Uç noktanın tanımadığı bir alan reddedilir, isteğe bağlı olarak işaretlenmemiş bir alan zorunludur, türler ve uzunluklar denetlenir. Bu hatalardan herhangi biri 400 `invalid_request` ile yanıtlanır; `field` ilgili alanın adını verir (iç içe bir alan için noktalı ad, örneğin `pow.nonce`). Dizeler kontrol karakteri içeremez. Uzunluklar UTF-16 kod birimi olarak sayılır; bu yüzden Temel Çok Dilli Düzlem dışındaki bir karakter (bir emoji) iki kez sayılır. JSON olmayan bir gövde 400 `invalid_json` ile yanıtlanır. `POST /gif` ve `POST /reports` gövdelerini kendileri denetler (her uç noktaya bakın).
    - **Sorgu dizeleri.** Bir parametrenin ilk geçtiği yer geçerlidir, bilinmeyen parametreler yok sayılır ve `+` boşluk olarak çözülür: süre kontrolü kategorisi alan uç noktalar hem `3%2B2` hem `3+2` biçimini kabul eder.
    - **Metotlar.** `HEAD`, gövdesiz bir `GET` isteğidir. Var olan bir yola yapılan `OPTIONS` isteği bir `Allow` başlığıyla (ve aşağıda açıklanan CORS ön kontrol isteği başlıklarıyla) 204 yanıtını alır. Yolun desteklemediği başka her metot `Allow` ile birlikte 405 `method_not_allowed` yanıtını alır.
    - **İstek hedefi.** 4096 karakteri aşamaz (414 `uri_too_long`). Hiç ayrıştırılamayan ya da başlık bölümü çok büyük olan veya çok yavaş gelen bir istek boş bir 400, 431 ya da 408 yanıtı alır ve bağlantı kapatılır.
    - **CORS: varsayılan olarak kapalı bir izin listesi.** API, `Origin` göndermeyen oyuna hizmet eder. `CORS_ORIGINS` boşken (varsayılan) hiçbir `Access-Control-*` başlığı gönderilmez, dolayısıyla hiçbir web sayfası bir yanıtı okuyamaz. `CORS_ORIGINS`, sayfaları API'yi bir tarayıcıdan çağırabilecek web kökenlerini (origin) listeler; her biri tam olarak tarayıcıların `Origin` içinde gönderdiği biçimde yazılır: `https://host` ya da `https://host:port`, küçük harflerle, yol, sondaki eğik çizgi ve varsayılan port olmadan; `http://` yalnızca `localhost`, `127.0.0.1` ve `[::1]` için (yerel geliştirme); joker karakter yok. Resmî sunucu, web sitesi için `CORS_ORIGINS=https://scacelith.com,https://www.scacelith.com` ayarını kullanır. Bu durumda:
      - Sağlık kontrolü uç noktaları dışında, `/api/v1` altındaki bir yol için verilen her yanıt, başarılı da olsa hatalı da olsa (adres başına katmanın 429 yanıtı dahil), `Vary: Origin` taşır. İsteğin `Origin` değeri listedeki kökenlerden biriyle bayt bayt aynıysa yanıt ayrıca `Access-Control-Allow-Origin: <that origin>` başlığını ve, ön kontrol isteğine verilen yanıt dışında, `Access-Control-Expose-Headers: Retry-After, Content-Disposition` başlığını taşır; böylece sayfa hata kodlarını, `Retry-After` değerini ve PGN ile GIF indirmelerinin dosya adlarını okuyabilir.
      - Listedeki bir kökenden var olan bir yola gelen ön kontrol isteği (`Access-Control-Request-Method` içeren `OPTIONS`) gövdesiz bir 204 yanıtı alır; yanıt `Allow`, `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods` (`Allow` içindeki metotlar), `Access-Control-Allow-Headers: Authorization, Content-Type`, `Access-Control-Max-Age: 600` ve `Vary: Origin` başlıklarını taşır. Ön kontrol isteği token gerektirmez, hiçbir hesap kotasından ve hiçbir uç nokta sınırından düşülmez; her istek gibi adres başına katmandan bir jeton alır. Tarayıcı bir ön kontrol yanıtını 10 dakika boyunca yeniden kullanır, ama yalnızca sorgu dizesi dahil tam olarak aynı URL için. `GET /info` ya da `GET /leaderboard` gibi `Authorization` içermeyen ve JSON gövdesi olmayan bir istek ön kontrol gerektirmez.
      - Ön kontrol isteğine verilen başka her yanıt, CORS başlıklarını taşısa bile başarısız bir ön kontroldür: var olmayan bir yol için 404 ya da adres başına katmanın 429 yanıtı. Bu durumda tarayıcı isteği göndermez ve sayfa, yanıtı da `Retry-After` değerini de görmeden bir ağ hatası görür.
      - `Access-Control-Allow-Credentials` hiçbir zaman gönderilmez, `Access-Control-Allow-Origin: *` de gönderilmez: sayfa, oyunun yaptığı gibi oturum token'ını `Authorization: Bearer` içinde gönderir ve API hiçbir çerez ayarlamaz. HTML sayfaları, sağlık kontrolü uç noktaları ve okunamayan isteklere verilen boş yanıtlar hiçbir CORS başlığı taşımaz. WebSocket CORS kullanmaz: onu açabilecek sayfaları `WS_ALLOWED_ORIGINS` listeler.
      - Listede olmayan bir kökenin sayfası hiçbir şey okuyamaz ve siteler arası yazma istekleri hiçbir zaman gönderilmez: JSON gövdeleri ve `Authorization` bir ön kontrol isteği gerektirir ve bu isteğin yanıtında o köken için `Access-Control-Allow-Origin` bulunmaz. `Cross-Origin-Resource-Policy: same-origin` her yanıtta kalır: yalnızca `no-cors` isteklerine (bir yanıtı kendi sayfasına gömen başka bir site) uygulanır, listedeki bir sayfanın `cors` isteklerine asla uygulanmaz.

    ## Yanıtlar ve hatalar

    - Yanıtlar UTF-8 kodlamalı JSON'dur; PGN indirmesi (`application/x-chess-pgn`), animasyonlu GIF'ler (`image/gif`) ve HTML sayfaları hariç. Zamanlar, 1 Ocak 1970 UTC'den bu yana geçen milisaniyelerdir. Kimlikler (id) tam sayıdır. Oyun kimlikleri en fazla 16 basamaklıdır ama 2^53'ün altında kalır; bu yüzden bir JSON sayısı (double) onları tam olarak tutar.
    - API'nin her yanıtı `Cache-Control: no-store`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`, `X-Frame-Options: DENY`, `Cross-Origin-Resource-Policy: same-origin` ve `Content-Security-Policy: default-src 'none'; frame-ancestors 'none'` başlıklarını taşır (HTML sayfalarının kendi politikası vardır). `TLS_MODE=native` ile ayrıca `Strict-Transport-Security: max-age=31536000` taşır. Dinleyicinin API'den önce kendisinin yazdığı yanıtlar (sağlık kontrolü uç noktalarının `GET` ve `HEAD` yanıtları, adres başına katmanın 429 yanıtı ve API tamamen başarısız olduğunda verilen 500 `internal_error`) `Cross-Origin-Resource-Policy` dışında aynı başlıkları taşır ve `Content-Type` değerleri charset içermeyen `application/json` olur.
    - Bir yanıt, sunucunun onu hazır ettiği andan itibaren 60 saniye içinde okunmalıdır; bu yalnızca büyük yanıtlar için önemlidir (bir GIF, veri dışa aktarımı, uzun bir PGN). Sunucunun bir yanıtı hazırlarken geçirdiği süre ne bu süreye ne de hiç bayt alınıp gönderilmediğinde bağlantının kapatılmasına yol açan 30 saniyeye dahil edilir.

    Hataların tek bir biçimi vardır (`Error` şeması):

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

    `message` günlükler ve yedek metin için düşünülmüştür; istemci ne göstereceğini `error` değerine göre seçer. `retryAfter` (saniye) yalnızca zamanla sona eren retlerde bulunur ve bu durumda yanıt, aynı değerde bir `Retry-After` başlığı da taşır; tek istisna, geçmiş, oyun, oyuncu ve sıralama tablosu okumalarının 503 `busy` yanıtıdır: onda yalnızca alan bulunur. Bazı hatalar ek alanlar içerir: `field` (geçersiz girdi), `reason` (`weak_password`, `pow_required`), `pow` (`pow_required`), `until` (`banned`), `line` ve `column` (`invalid_pgn`).

    Herhangi bir uç noktanın verebileceği hatalar:

    | Durum | `error` | Ne zaman |
    |---|---|---|
    | 400 | `invalid_request` | Gövde uç noktanın şemasına uymuyor (`field` yerini belirtir), istek hedefi ya da `Content-Length` hatalı biçimli, bir yol parametresi geçerli bir URL kodlaması değil ya da gövde yarıda kesildi. |
    | 400 | `invalid_json` | Gövde JSON değil. |
    | 401 | `unauthorized` | Oturum gerektiren bir uç noktada `Authorization` başlığı yok. |
    | 401 | `invalid_token` | Token hatalı biçimli, süresi dolmuş, geçersiz kılınmış ya da silinmiş bir hesaba ait; oturumun isteğe bağlı olduğu uç noktalarda da. |
    | 404 | `not_found` | Böyle bir uç nokta yok. Bazı uç noktalar da kullanır: böyle bir oyun, oyuncu ya da oturum yok. |
    | 405 | `method_not_allowed` | Yol başka metotlar için var (bkz. `Allow`). |
    | 408 | `request_timeout` | Gövde 10 saniye içinde gelmedi. |
    | 413 | `payload_too_large` | Gövde `HTTP_BODY_LIMIT` değerini aşıyor (`POST /gif` için 135.168 bayt). |
    | 414 | `uri_too_long` | İstek hedefi 4096 karakteri aşıyor. |
    | 415 | `unsupported_media_type` | Gövde `application/json` değil ya da charset değeri UTF-8 değil. |
    | 429 | `rate_limited` | Bir hız sınırı (aşağıya bakın): `retryAfter` ve bir `Retry-After` başlığı. |
    | 500 | `internal_error` | Beklenmeyen bir hata. Sunucu bunu günlüğe kaydeder. |
    | 503 | `timeout` | Sunucu 30 saniye içinde yanıt vermedi (dışa aktarım için 60 saniye, varsayılan ayarlarla GIF'ler için 45 saniye). |
    | 503 | `server_busy` | Bir oturum sorgusu ya da bir hesap değişikliği veritabanını kilitli buldu (`retryAfter` 1) ya da parola hash kuyruğu dolu (aşağıya bakın). Ayrıca sunucu çok geride kaldığında, `retryAfter` 2 ile 5 arasında olmak üzere: bir `POST`, `PUT`, `PATCH` ya da `DELETE` için veritabanında bekleyen çok fazla yazma işlemi var; herhangi bir istek için, zaman aşımını geçtiği hâlde hâlâ çalışan önceki istek sayısı çok fazla ya da oturum token'ı 5 saniye içinde denetlenemedi. İstek işlenmedi ve hız sınırlarından hiçbir şey düşülmedi. |

    Okuma uç noktaları (oyun geçmişi, oyunlar, oyuncular, sıralama tablosu) ve dışa aktarım, veritabanı kilitli kaldığında `retryAfter: 1` ile 503 `busy` yanıtını verir.

    Parola denetleyen ya da parola hash'i hesaplayan uç noktalar ayrıca şu iki yanıttan birini de verebilir; ikisinde de `retryAfter` 5 ile 15 saniye arasında rastgele bir değerdir:

    - 503 `server_busy`: sunucunun parola hash kuyruğu (`PASSWORD_HASH_QUEUE_MAX`) dolu ya da bekleme süresi doldu.
    - 429 `rate_limited`: kuyruk yarı yarıya dolduktan sonra, bu istemcinin (bir IPv4 adresi ya da bir IPv6 /48 bloğu) zaten `PASSWORD_HASH_WAITERS_PER_SOURCE` kadar bekleyen hash'i var. Bu 429, isteğin hız sınırlarından aldığı jetonları geri verir.

    Hiçbir şey değiştirilmedi ve hiçbir başarısız deneme sayılmadı (`POST /auth/sso/google/link` hariç: orada bilet denemesi ve hesabın başarısız denemesi hash'ten önce sayılmıştır); bir sıfırlama bağlantısı da geçerliliğini korur.

    HTML sayfaları hatalarını aynı durum kodlarıyla, HTML sayfası olarak verir.

    ## Kimlik doğrulama

    Oturum gerektiren uç noktalar `Authorization` başlığında bir bearer token alır (`bearerAuth` şeması):

    ```
    Authorization: Bearer sct_L_8GDd7uzfQ3QQWtqrsWXDTsFWzRwIvJcwIGHhjWPS8
    ```

    - **Token alma.** Bir token (`sct_` ve ardından 43 base64url karakteri) şuralardan gelir: `POST /auth/login` (iki faktörlü kimlik doğrulama açıksa ardından `POST /auth/login/mfa`), `POST /auth/sso/google/finish` ve `POST /auth/sso/google/link` (Google ile giriş) ve `POST /auth/sso/complete`. Bunların her biri `{ token, expiresAt, user }` döndürür. Sunucu token'ın yalnızca SHA-256 özetini saklar.
    - **Zorunlu oturum.** Başlık yoksa yanıt, `WWW-Authenticate: Bearer realm="scacelith"` ile 401 `unauthorized` olur. Geçersiz bir token, `WWW-Authenticate: Bearer realm="scacelith", error="invalid_token"` ile 401 `invalid_token` yanıtını alır. İstemci, `invalid_token` alan bir token'ı unutmalı ve yeniden giriş yapmalıdır.
    - **İsteğe bağlı oturum.** `GET /games/{id}`, `GET /games/{id}/pgn`, `GET /players/{username}` ve `GET /players/{username}/games` uç noktalarında oturum isteğe bağlıdır. Başlık olmadan herkese açık görünümü döndürürler; başlık gönderilirse içindeki token geçerli olmalıdır.
    - **Meşgul depo.** Bir token denetlenirken veritabanı kilitli kalırsa, oturum gerektiren ya da kabul eden bir uç nokta `retryAfter: 1` (ve `Retry-After`) ile 503 `server_busy` yanıtını verir; denetim 5 saniyeden uzun sürerse 2 ile 5 arasında bir `retryAfter` ile 503 `server_busy` yanıtını verir. Her iki durumda da token hakkında karar verilmemiştir: token'ı saklayın ve isteği yeniden gönderin.
    - **Ömür.** Bir oturum, girişten sonraki `SESSION_MAX_DAYS` (90) gün (bu `expiresAt` değeridir) ile kullanılmadan geçen `SESSION_IDLE_DAYS` (30) günden hangisi önce dolarsa o zaman sona erer. Her kullanım boşta kalma sınırını ileri iter; sunucu yeni değeri arka planda, en fazla 5 dakikada bir yazar. Veritabanında bekleyen çok sayıda yazma işlemi varken bu yazmayı sonraki bir isteğe erteler, ama boşta kalma sınırından önceki son bir saat içinde ertelemez; böylece kullanılan bir oturumun süresi dolmaz. Bir hesap en fazla `MAX_SESSIONS_PER_USER` (10) oturum tutar: yeni bir giriş, bu sayıyı aşan en eski oturumları geçersiz kılar.
    - **Geçersiz kılma.** Çıkış yapmak (`POST /auth/logout`, `POST /auth/logout-all`, `DELETE /auth/sessions/{id}`) oturumları geçersiz kılar; parola değişikliği (diğer oturumları), parola sıfırlama ve hesabın silinmesi (tüm oturumları) ile bir yönetici de geçersiz kılar. API üzerinden yapılan geçersiz kılma hemen etkili olur ve geçersiz kılınan bir oturumla açılmış WebSocket kapatılır. Parola değişikliği ya da sıfırlaması ve hesabın silinmesi, oturumları değişikliğin kendisiyle aynı işlemde geçersiz kılar: bunlardan biri başarısız olursa hiçbir oturum geçersiz kılınmamıştır ve başka hiçbir şey de değişmemiştir. Sunucu oturum sorgularını 30 saniye önbellekte tutar; bu yüzden ayrı bir süreç olan yönetici komutuyla yapılan geçersiz kılma 30 saniye içinde etkili olur.
    - **Kapsam.** Bir token tek bir sunucuya aittir ve o sunucunun WebSocket'ini de açar. Onu asla başka bir sunucuya göndermeyin.

    ## Hız sınırları ve diğer kısıtlamalar

    Sınırlar iki kapsamdan birine uygulanır. **İstemci:** bir IPv4 adresi ya da bir IPv6 /64 bloğu (proxy modunda adres, bir `TRUSTED_PROXIES` adresinin gönderdiği `X-Forwarded-For` başlığından alınır); bazı sınırlar, her bir /64 ağına ek olarak her IPv6 /48 bloğunu da bir bütün olarak sayar. **Oyuncu:** adresi ne olursa olsun, giriş yapılmış hesap; oturumun isteğe bağlı olduğu bir uç noktada oyuncu başına sayılan bir sınır, token'sız bir istek için istemci başına sayılır.

    Her sınır, `limit` kadar istek tutan ve `limit / window` hızında sürekli dolan bir jeton kovasıdır; `retryAfter` bir sonraki jetona kadar kalan süredir. *Paylaşımlı* olarak işaretlenen sınırlar ayrıca aynı uzunlukta bir kayan pencere üzerinden de sayılır; böylece hiçbir pencere `limit` değerinden çok daha fazla istek içermez. Her sınır tüm sunucu için sayılır. Bir uç noktanın sınırlarından biri bir isteği reddettiğinde, diğer sınırlarının o istek için aldığı jetonlar geri verilir. Ret yanıtı `retryAfter` ve `Retry-After` ile 429 `rate_limited` olur.

    - **Adres başına katman.** Her istek (sağlık kontrolü uç noktaları ve WebSocket yükseltmeleri dahil, her yol ve her metot) önce istemcisinin kotasından bir jeton alır: dakikada `HTTP_RATE_PER_IP` (600), yarım dakikalık ani artış payıyla; bir IPv6 /48 bloğu için `HTTP_RATE_PER_PREFIX` (4 x `HTTP_RATE_PER_IP`). Ayrıca bir istemcinin aynı anda işlenmekte olan en fazla `IP_MAX_INFLIGHT` (32 x `WORKERS`) isteği olabilir (fazlası için `retryAfter` 1). Retlerden sonra istek göndermeyi sürdüren bir istemci engellenir: bir dakikada `ABUSE_BLOCK_REFUSALS_PER_MIN` (600) ret onu 1 dakika engeller, ardından 6 saat içindeki her yeni engellemede 4, 16 ve 60 dakika. `auth`, `auth_*` ve `reauth` sınırlarının bir reti 5 sayılır; oyuncu başına sayılan sınırlar hiç sayılmaz. `ABUSE_EXEMPT` içindeki adresler bu katmanı atlar, ama hesap kotasını ya da uç nokta sınırlarını atlamaz.
    - **Hesap kotası.** Geçerli bir oturum token'ı taşıyan her istek ayrıca hesabından da düşülür: adres ne olursa olsun, tüm uç noktalar için toplam dakikada `USER_RATE_PER_MIN` (120), yarım dakikalık ani artış payıyla.
    - **Uç nokta sınırları:**

    | Sınır | Varsayılan | Neye göre sayılır | Uç noktalar |
    |---|---|---|---|
    | `auth` | `AUTH_RATE_PER_IP` (20) / 10 dk, paylaşımlı | istemci ve IPv6 /48 başına `AUTH_RATE_PER_PREFIX` (5 x `AUTH_RATE_PER_IP`) | `POST /auth/register`, `/auth/login`, `/auth/login/mfa`, `/auth/verify-email/resend`, `/auth/password/forgot`, `/auth/password/reset`, `/auth/sso/google/link`, `/auth/sso/complete`; `POST /verify-email`, `/reset-password`, `/confirm-email-change` |
    | `auth_register` | `AUTH_REGISTER_PER_HOUR` (10) / saat, paylaşımlı | istemci; IPv6 /48 başına bunun 3 katı | `POST /auth/register` |
    | `auth_mail` | `AUTH_MAIL_PER_HOUR` (10) / saat, paylaşımlı | istemci; IPv6 /48 başına bunun 3 katı | `POST /auth/verify-email/resend` |
    | `auth_forgot` | `AUTH_FORGOT_PER_HOUR` (3) / saat, paylaşımlı | istemci; IPv6 /48 başına bunun 3 katı | `POST /auth/password/forgot` |
    | `auth_forgot_day` | `AUTH_FORGOT_PER_DAY` (10) / 24 saat, paylaşımlı | istemci; IPv6 /48 başına bunun 3 katı | `POST /auth/password/forgot` |
    | `auth_reset` | `AUTH_RESET_PER_HOUR` (10) / saat, paylaşımlı | istemci; IPv6 /48 başına bunun 3 katı | `POST /auth/password/reset`, `POST /reset-password` |
    | `reauth` | `auth` ile aynı sayılar, ayrı bir kova, paylaşımlı | istemci ve 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 dk, paylaşımlı | oyuncu | `reauth` ile aynı uç noktalar |
    | `account` | 60 / dk | oyuncu | `GET /account/me`, `PUT /account/preferences` |
    | `account_games` | 60 / dk | oyuncu | `GET /account/games` |
    | `account_export` | 5 / saat, paylaşımlı | oyuncu | `POST /account/export` (`reauth` öncesinde denetlenir; her deneme sayılır) |
    | `sessions` | 60 / dk | oyuncu | `POST /auth/logout`, `/auth/logout-all`, `GET /auth/sessions`, `DELETE /auth/sessions/{id}` |
    | `public_read` | 60 / dk | oyuncu (token'sız istemci) | `GET /players/{username}`, `/players/{username}/games`, `/games/{id}`, `/games/{id}/pgn` (dördü için tek kova) |
    | `gif` | 30 / dk | oyuncu | `GET /games/{id}/gif`, `POST /gif` (ikisi için tek kova) |
    | `gif_user_min`, `gif_user_hour` | `GIF_USER_RENDERS_PER_MIN` (4) / dk ve `GIF_USER_RENDERS_PER_HOUR` (30) / saat, paylaşımlı | oyuncu | aynı iki uç nokta, yalnızca GIF'in oluşturulması gerektiğinde (önbellekten değil) |
    | `gif_ip_min`, `gif_ip_hour` | `GIF_IP_RENDERS_PER_MIN` (12) / dk ve `GIF_IP_RENDERS_PER_HOUR` (120) / saat, paylaşımlı | istemci (tüm hesapları birlikte); IPv6 /48 başına bunun 3 katı | aynı uç noktalar, yukarıdaki gibi |
    | `reports` | 30 / saat | oyuncu | `POST /reports` |
    | `sso_start` | 30 / 10 dk, paylaşımlı | istemci ve IPv6 /48 başına 90 | `POST /auth/sso/google/start`, `POST /auth/sso/google/web/start` (birlikte) |
    | `sso_finish` | 30 / dk | istemci | `POST /auth/sso/google/finish` |
    | `page` | 60 / dk | istemci | `GET /verify-email`, `/reset-password`, `/confirm-email-change` |

    `GET /info` ve `GET /leaderboard` uç noktalarının kendine ait bir sınırı yoktur: yalnızca adres başına katman uygulanır. Birden çok sınırı olan bir uç nokta bunları açıklamasında verilen sırayla denetler.

    Sunucu bir isteği şu sırayla işler: adres başına katman, ardından sağlık kontrolü uç noktaları; uç nokta eşleştirmesi; sunucunun çok geride kalmadığına dair denetimler (503 `server_busy`): zaman aşımını geçtiği hâlde hâlâ çalışan çok fazla önceki istek ve bir `POST`, `PUT`, `PATCH` ya da `DELETE` için veritabanının yazma birikimi; 5 saniye içinde kimlik doğrulama (aşılırsa 503 `server_busy`); istek bir oturum taşıyorsa hesap kotası; uç noktanın sınırları; gövde; uç noktanın kendisi (GIF uç noktaları oluşturma sınırlarını yalnızca oluşturacakları bir GIF olduğunda alır). Dolayısıyla token'ı yüzünden reddedilen bir istek uç noktanın jetonlarından hiçbirini harcamaz, gövdesi geçersiz bir istek ise harcar.

    Uç noktaların kendilerinin yanıtladığı diğer kısıtlamalar:

    - **Bir giriş adı için başarısız girişler** (kullanıcı adı ya da e-posta): `AUTH_FAILURES_PER_ACCOUNT` (5) başarısız denemeden itibaren her deneme bir öncekinin iki katı kadar beklemelidir (2 sn, 4 sn ve bu şekilde 15 dakikaya kadar): `retryAfter` ile 429 `too_many_attempts`. Sayaç, başarısız deneme olmadan geçen bir saatten sonra sıfırlanır.
    - **Girişte başarısız ikinci faktörler:** hesabın 5. yanlış kodundan itibaren aynı kural. Bir giriş adımı en fazla 5 yanlış kod kabul eder.
    - **Bir hesabın ikinci faktör kodları:** girişte ve yeniden kimlik doğrulamalarda, hangi adresten olursa olsun, 15 dakikada en fazla `AUTH_MFA_PER_ACCOUNT` (10) kod (kimlik doğrulama uygulaması kodları ya da kurtarma kodları, doğru ya da yanlış); bunun ötesinde kod denetlenmeden önce 429 `too_many_attempts` verilir, böylece kurtarma kodu harcanmaz.
    - **Bir hesabın başarısız yeniden kimlik doğrulamaları** (yanlış parola ya da kod): aynı kural (429 `too_many_attempts`); yeniden kimlik doğrulama yapan tüm uç noktalar için ortaktır.
    - **E-postalar:** adres başına 5 dakikada bir onay ya da sıfırlama e-postası (yanıt aynı kalır); adres başına saatte bir "biri adresinizi kullanmaya çalıştı" uyarısı.
    - **Bildirimler:** oyuncu başına 24 saatte `REPORTS_PER_DAY` (5): 429 `report_limit`.

    ## İş kanıtı

    `POW_REGISTER_BITS` 0'dan büyükse (varsayılan 18; `GET /info` bunu `pow.register` olarak verir) `POST /auth/register` her zaman bir iş kanıtı gerektirir. `POST /auth/login` ve `POST /auth/sso/google/link` (ikisi için tek tür sınama) yalnızca sunucu bir başarısız giriş dalgası gördükten sonraki 5 dakika boyunca iş kanıtı gerektirir (`POW_LOGIN_TRIGGER_PER_MIN`, ardından `POW_LOGIN_BITS`); istemci bunu yanıttan öğrenir.

    1. Kanıtsız (ya da reddedilen bir kanıtla gönderilen) istek, `reason` ve bir `pow` sınaması `{ challenge, bits, expiresAt }` ile 428 `pow_required` yanıtını alır.
    2. Bir nonce bulun: `SHA-256(challenge + ":" + nonce)` değerinin `bits` adet sıfır bitle (önce ilk baytın en anlamlı biti) başlamasını sağlayan, en fazla 20 basamaklı bir ondalık dize. 18 bit ortalama yaklaşık 260.000 hash gerektirir.
    3. Aynı isteği, gövdesinde `"pow": { "challenge": "...", "nonce": "123456" }` ile yeniden gönderin.

    Bir sınama 2 dakika geçerlidir ve yalnızca bir kez, tek bir uç nokta ve tek bir istemci ağı (bir IPv4 adresi ya da IPv6 /64 bloğu) için kullanılabilir. İmzalıdır; bu yüzden sunucu, sınama geri gelene kadar onunla ilgili hiçbir şey saklamaz. `reason` bir kanıtın neden reddedildiğini söyler: `required`, `malformed`, `signature`, `endpoint`, `network`, `expired`, `bits`, `work` ya da `replayed`.

    ## Yeniden kimlik doğrulama

    Hesap değişiklikleri parolayı yeniden ister; iki faktörlü kimlik doğrulama açıksa bir ikinci faktör de ister:

    - yalnızca parola: `POST /account/password` ve `POST /account/mfa/totp/setup`;
    - parola ve bir kimlik doğrulama uygulaması kodu (kurtarma kodu reddedilir): `POST /account/mfa/recovery-codes`;
    - parola ve bir kimlik doğrulama uygulaması kodu ya da bir kurtarma kodu: `POST /account/mfa/totp/disable`, `POST /account/email`, `POST /account/export` ve `POST /account/delete`.

    Gövdelerde `code` 6 haneli bir kimlik doğrulama uygulaması kodunu, `recoveryCode` ise bir kurtarma kodunu tutar (`xxxx-xxxx-xx`; büyük/küçük harf, boşluklar ve tireler önemli değildir). Kurtarma kodlarının kabul edildiği yerlerde bir kurtarma kodu `code` içinde de gönderilebilir. Her kod bir kez çalışır: kullanılmış bir kimlik doğrulama uygulaması kodu bir sonraki 30 saniyelik adıma kadar reddedilir, bir kurtarma kodu ise kullanıldığında tükenir.

    | Durum | `error` | Ne zaman |
    |---|---|---|
    | 403 | `invalid_password` | Yanlış parola ya da istek işlenirken parola sıfırlandı veya değiştirildi (hiçbir şey değiştirilmedi). |
    | 403 | `mfa_code_required` | İki faktörlü kimlik doğrulama açık ve ne `code` ne de `recoveryCode` gönderildi. |
    | 403 | `invalid_code` | Yanlış ya da daha önce kullanılmış kod. |
    | 400 | `password_not_set` | Yalnızca Google ile giriş yapan bir hesabın henüz parolası yok ("Parolanızı mı unuttunuz?" ile bir parola belirlenir). |
    | 429 | `too_many_attempts` | Bu hesapta çok fazla başarısız deneme ya da çok fazla denenen kod. |
    | 503 / 429 | `server_busy` / `rate_limited` | Parola hash kuyruğu meşgul. |

    Başarısız parolalar ve kodlar hesabın başarısız deneme sayacına eklenir ve güvenlik olayı olarak kaydedilir.

    ## Metrik portu

    Sunucu, API portunun yanı sıra metrik portunda da düz HTTP ile yanıt verir (`METRICS_PORT`, 9464; `METRICS_BIND` adresine bağlanır, varsayılan 127.0.0.1; bu portu dışarıya açmayın). Bu port bu API'nin parçası değildir: `GET /healthz` `ok` yanıtını verir; `GET /readyz` başlatma tamamlandıktan sonra (her shard kendi işlem günlüğünü yeniden oynatmış ve dinleyiciler bağlanmıştır) kapatma başlayana kadar `ready`, aksi hâlde 503 `not ready` yanıtını verir; `GET /metrics` Prometheus metriklerini sunar ve `METRICS_TOKEN` ayarlanmışsa tam olarak o token ile `Authorization: Bearer <token>` gerektirir.
  contact:
    name: Scacelith
    url: https://github.com/DarkCenobyte/scacelith-chess-server
  license:
    name: GPL-3.0-or-later
    identifier: GPL-3.0-or-later
externalDocs:
  description: Scacelith sunucusunun kaynak kodu ve başvuru belgeleri (API.md, PROTOCOL.md, CONFIG.md).
  url: https://github.com/DarkCenobyte/scacelith-chess-server
servers:
  - url: https://caissa.scacelith.com/api/v1
    description: Resmî sunucu.
  - url: https://{host}:{port}/api/v1
    description: Herhangi bir Scacelith sunucusu, örneğin bir topluluk sunucusu.
    variables:
      host:
        default: caissa.scacelith.com
        description: Sunucunun genel ana bilgisayar adı (`SERVER_PUBLIC_HOST`).
      port:
        default: "443"
        description: Genel API portu (`PUBLIC_API_PORT`, yoksa `API_PORT`).
tags:
  - name: server-info
    x-displayName: Sunucu bilgileri
    description: Bir istemcinin giriş yapmadan ya da bağlanmadan önce bilmesi gerekenler.
  - name: health
    x-displayName: Sağlık kontrolü
    description: |-
      İzleme için sunucunun canlılık ve hazır olma durumu. Bu uç noktalar API portunda, kimlik doğrulamadan ve tüm uç nokta sınırlarından önce yanıt verir, ama her istek gibi adres başına katmandan bir jeton alır; bir izleme sunucusu `ABUSE_EXEMPT` içinde listelenebilir. Her uç nokta iki yolda da çalışır: sunucunun kökünde ve `/api/v1` altında.

      Metrik portunun, giriş bölümünde açıklanan kendi sağlık kontrolü uç noktaları vardır.
  - name: auth
    x-displayName: Kayıt ve giriş
    description: |-
      Hesap oluşturma, parolayla (ve ikinci faktörle) giriş, onay ve parola sıfırlama e-postaları.

      `POST /auth/login`, `POST /auth/login/mfa`, `POST /auth/sso/google/finish`, `POST /auth/sso/google/link` ve `POST /auth/sso/complete` bir oturum `{ token, expiresAt, user }` döndürür (ya da ilk birkaçı için ikinci bir adım).
  - name: google-sign-in
    x-displayName: Google ile giriş
    description: |-
      `GET /info` yanıtında `sso.google: true` olduğunda sunulur; aksi hâlde aşağıdaki her uç nokta 404 `sso_disabled` yanıtını verir. `sso.googleWeb` true olduğunda bir web sitesi de kendi istemcisiyle aynı şekilde giriş yapar (aşağıda, **Bir web sitesinde**). Oyun, RFC 8252'deki yüklü uygulama akışıyla ("Masaüstü uygulaması" türünde bir istemci) sistem tarayıcısı üzerinden giriş yapar: Google tarayıcıyı hiçbir zaman bu sunucuya değil, oyunun `127.0.0.1` üzerindeki bir dinleyicisine geri gönderir ve oyun hiçbir Google kimlik bilgisini görmez.

      1. İstemci `127.0.0.1:0` üzerinde dinler (portu sistem seçer) ve bir PKCE çifti oluşturur: `[A-Za-z0-9._~-]` karakterlerinden oluşan 43 ile 128 karakterlik bir `codeVerifier` ve 43 karakterli, dolgusuz `codeChallenge = BASE64URL(SHA-256(codeVerifier))`.
      2. Sınama ve portla gönderilen `POST /auth/sso/google/start` isteği Google URL'sini ve denemenin `state` değerini döndürür. İstemci URL'yi denetler (aşağıya bakın) ve tarayıcıda açar.
      3. Google tarayıcıyı `http://127.0.0.1:<port>/oauth2/google/<tag>?code=...&state=...` adresine gönderir. İstemci yalnızca başlatma yanıtındaki `state` değerini kabul eder ve bir `error=` yönlendirmesinden sonra hiçbir şey göndermez.
      4. Deneme kimliği, `codeVerifier`, `state` ve `code` (Google gönderdiyse `iss` de) ile `POST /auth/sso/google/finish`.
      5. Yanıt bir oturum, bir iki faktörlü kimlik doğrulama adımı (`POST /auth/login/mfa` ile devam edin), yeni bir oyuncu için `needsUsername` (`POST /auth/sso/complete` ile devam edin) ya da adresi parolalı bir hesap kullanıyorsa `needsPassword` (`POST /auth/sso/google/link` ile devam edin) olur.

      **Köken etiketi.** Yönlendirme URI'si, oyuncunun eklediği sunucuya ait bir etiket taşır; böylece başka bir sunucunun kendi oyuncuları için aldığı bir URL oyun tarafından reddedilir. Köken; küçük harfli ana bilgisayar adı (bir IPv6 değişmezi köşeli parantez içinde), `:` ve 443 dahil her zaman yazılan ondalık API portundan oluşur: sunucu `SERVER_PUBLIC_HOST` değerini ve genel API portunu, oyun ise bağlı olduğu adresi kullanır. Etiket, SHA-256(UTF-8 `"scacelith-sso-origin-v1\n"` + köken) değerinin base64url (dolgusuz) kodlamasının ilk 22 karakteridir. Yönlendirme URI'si `"http://127.0.0.1:" + port + "/oauth2/google/" + tag` biçimindedir: her zaman IPv4 değişmezi ve oyunun dinleyicisinin portu (1024 ile 65535 arası). Sunucu istemciden asla bir URI, ana bilgisayar adı ya da yol almaz.

      | Köken | Etiket |
      |---|---|
      | `play.scacelith.example:443` | `IhcScoV7eDOzTEcSnqPUPt` |
      | `localhost:8443` | `TFGx7zQ_8QlGZW5zpqznCr` |
      | `[::1]:8443` | `XToJm0DG5PjciEVmZa9Cho` |
      | `127.0.0.1:50443` | `3r653wM5ZjYsHcAJljmCwY` |

      Bu yüzden Google ile giriş yalnızca sunucuyu tam olarak `SERVER_PUBLIC_HOST` adı ve genel API portuyla eklemiş oyuncular için çalışır.

      **Oyunun tarayıcıyı açmadan önce denetledikleri.** `authUrl` tam olarak `https://accounts.google.com/o/oauth2/v2/auth?` ile başlar, 4096 karakterden kısa ve yazdırılabilir ASCII'dir; sorgusunda tam olarak bir `response_type=code`, oyunun kendi portundan ve köken etiketinden hesapladığı URI'ye eşit tam olarak bir `redirect_uri`, yanıttaki `state` değerine eşit tam olarak bir `state`, `code_challenge_method=S256` ve 43 karakterlik bir `code_challenge` bulunur. Aksi hâlde oyun dinleyicisini durdurur ve hiçbir şey açmaz. `finish` ve `link` isteklerini yalnızca `start` isteğini yanıtlayan sunucuya gönderir.

      **Bir web sitesinde.** Bir web sitesi (resmî site: <https://scacelith.com/>) aynı Google projesinin "Web uygulaması" türündeki bir istemcisiyle (`GOOGLE_WEB_CLIENT_ID`) giriş yapar. Google tarayıcıyı, işletmecinin yapılandırdığı, sitenin tek bir sayfasına (`GOOGLE_WEB_REDIRECT_URI`; istemcinin "Yetkilendirilmiş yönlendirme URI'leri" listesinde yer alır) geri gönderir ve o sayfa kodu `finish` uç noktasına gönderir; sunucu burada da istemciden yönlendirme URI'si almaz.

      1. Sayfa yukarıdaki gibi bir PKCE çifti oluşturur, doğrulayıcıyı sekmede (`sessionStorage`) saklar ve sınamayla birlikte `POST /auth/sso/google/web/start` isteğini gönderir.
      2. `authUrl` değerini oyunun yaptığı gibi denetler, tek farkla: `redirect_uri` kendi kökenindeki bir sayfa olmalıdır; ardından o adrese gider. Hiçbir Google betiği yüklenmez.
      3. Google tarayıcıyı `GOOGLE_WEB_REDIRECT_URI?code=...&state=...` adresine gönderir. Sayfa yalnızca başlatma yanıtındaki `state` değerini kabul eder ve bir `error=` yönlendirmesinden sonra hiçbir şey göndermez.
      4. `finish`, `link` ve `complete` oyundaki gibi devam eder. Deneme, onu bir web sitesinin başlattığını hatırlar: kod, web sitesinin istemcisi ve sayfasıyla takas edilir ve ID token'ı o istemciyi belirtmelidir (oyunun istemcisine ait bir kod ya da ID token'ı 502 `sso_failed` ile başarısız olur, tersi de geçerlidir).

      Bir Google hesabı, oyundan da web sitesinden de aynı Scacelith hesabına ulaşır: bağlama, istemci ne olursa olsun Google hesabını tanımlayan Google subject değerini (`sub`) tutar. Web sitesinin kökeni `CORS_ORIGINS` içinde olmalıdır (aksi hâlde `check-config` uyarır).

      **Google ile girişin ulaştığı hesap:** o Google hesabına zaten bağlı olan hesap; yoksa Google'ın doğruladığı adrese sahip etkin bir hesap (parolası varsa `finish` uç noktası `needsPassword` döndürür ve bağlama ancak parolası, ardından açıksa ikinci faktörü doğrulandığında kaydedilir; parolası yoksa 409 `sso_account_exists`); o da yoksa yeni bir hesap (`needsUsername`). Bir Google hesabı hiçbir zaman yalnızca adresi üzerinden mevcut bir hesaba bağlanmaz. Google ile giriş bir hesap oluşturduğunda ve mevcut bir hesaba eklendiğinde hesabın adresine bir e-posta gönderilir.
  - name: sessions
    x-displayName: Oturumlar
    description: Hesabın giriş yapılmış cihazları ve çıkış yapma.
  - name: account
    x-displayName: Hesap
    description: Hesap görünümü, tercihleri ve parolası.
  - name: two-step-verification
    x-displayName: İki faktörlü kimlik doğrulama
    description: 'Kimlik doğrulama uygulamaları (TOTP, RFC 6238): SHA-1, 6 hane, 30 saniyelik adımlar ve her iki yönde bir adımlık tolerans; ayrıca tek kullanımlık kurtarma kodları.'
  - name: email-change
    x-displayName: E-posta adresi değişikliği
    description: Hesabın adresinin değiştirilmesi; değişiklik yeni adrese gönderilen bir bağlantıyla onaylanır.
  - name: data-export
    x-displayName: Veri dışa aktarımı
    description: Sunucunun hesapla ilgili tuttuğu her şey, tek bir JSON dosyası olarak.
  - name: account-deletion
    x-displayName: Hesap silme
    description: Hesabın kalıcı olarak silinmesi.
  - name: game-history
    x-displayName: Oyun geçmişi
    description: Giriş yapmış oyuncunun kendi oyunları, filtrelenmiş ve sayfalara bölünmüş olarak.
  - name: games
    x-displayName: Oyunlar ve PGN
    description: |-
      Hamleleri ve saatleriyle oyun kayıtları ve bunların PGN dosyaları.

      **Oyun kodları.** `status`: 1 `WhiteWins`, 2 `BlackWins`, 3 `Draw`, 4 `Aborted` (yalnızca bitmiş oyunlar saklanır). `result`: `1-0`, `0-1`, `1/2-1/2` ya da `*` (iptal edildi). `reason`: adı (`termination`) ve PGN dosyasının hamle metnini bitiren sözcüklerle birlikte (dosyada İngilizce; aşağıda çevirileriyle); 7, 14 ve 21 kodları beraberliktir (süresi biten, terk eden ya da oyundan ayrılan oyuncunun karşısında mat edemeyen bir rakip vardı: FIDE 6.9 ve 5.1.2; kural `DESIGN.md` 6.3'te verilir) ve sunucu hiçbir oyunu 4 ve 13 kodlarıyla bitirmez (bunlar ortak neden listesine aittir):

      | Kod | `termination` | PGN'deki sözcükler |
      |---|---|---|
      | 1 | `Checkmate` | Checkmate (Şah mat) |
      | 2 | `Resignation` | Resignation (Terk) |
      | 3 | `Timeout` | Loss on time (Süreden kayıp) |
      | 4 | `IllegalMoves` | Second illegal move (forfeit) (İkinci kural dışı hamle, hükmen yenilgi) |
      | 5 | `Stalemate` | Stalemate (Pat) |
      | 6 | `InsufficientMaterial` | Dead position (insufficient material) (Ölü pozisyon, yetersiz materyal) |
      | 7 | `TimeoutVsInsufficient` | Flag fall, but the opponent cannot checkmate (Süre doldu, ama rakip mat edemez) |
      | 8 | `FivefoldRepetition` | Fivefold repetition (Beş kez tekrar) |
      | 9 | `SeventyFiveMoves` | 75-move rule (75 hamle kuralı) |
      | 10 | `ThreefoldClaim` | Threefold repetition (claimed) (Üç kez tekrar, talep edildi) |
      | 11 | `FiftyMoveClaim` | 50-move rule (claimed) (50 hamle kuralı, talep edildi) |
      | 12 | `Agreement` | Draw by agreement (Anlaşmayla beraberlik) |
      | 13 | `IllegalMovesVsInsufficient` | Second illegal move, but the opponent cannot checkmate (İkinci kural dışı hamle, ama rakip mat edemez) |
      | 14 | `ResignationVsInsufficient` | Resignation, but the opponent cannot checkmate (Terk, ama rakip mat edemez) |
      | 20 | `Abandonment` | Abandoned (disconnected for too long) (Oyundan ayrıldı, bağlantı çok uzun süre koptu) |
      | 21 | `AbandonmentVsInsufficient` | Abandoned, but the opponent cannot checkmate (Oyundan ayrıldı, ama rakip mat edemez) |
      | 22 | `Aborted` | Game aborted (Oyun iptal edildi) |
      | 23 | `NoShow` | Aborted: first move not played in time (İptal: ilk hamle zamanında yapılmadı) |
      | 24 | `Forfeit` | Forfeit (fair play violation) (Hükmen yenilgi, adil oyun ihlali) |
      | 25 | `ServerAborted` | Aborted by the server (Sunucu tarafından iptal edildi) |
      | 26 | `BothDisconnected` | Aborted: both players disconnected (İptal: iki oyuncunun da bağlantısı koptu) |

      14 kodu protokol alt sürümü 1 ile eklendi: bu sürümden önce yazılmış bir istemci, bilinmeyen bir kodu `result` değeriyle birlikte sıradan bir oyun bitişi olarak göstermelidir.
  - name: gifs
    x-displayName: Animasyonlu GIF'ler
    description: |-
      Saklamak ya da paylaşmak için animasyonlu GIF olarak bir oyun: yukarıdan görülen tahta, başlangıçtan son pozisyona kadar her pozisyon için bir çerçeve, tahtanın üstünde oyuncuların adları ve puanları, altında son hamle ve son çerçevede sonuç ile oyunun nasıl bittiği.

      **Maliyet, önbellek ve kotalar.** Bir GIF, oyunları yürüten iş parçacıklarında asla değil, kendine ait bir oluşturma iş parçacığında ve en düşük CPU önceliğiyle oluşturulur: `GIF_THREADS` iş parçacığı (varsayılan `WORKERS`); ilk GIF ile başlatılır ve GIF oluşturulmadan geçen bir dakikadan sonra durdurulur. En fazla `GIF_QUEUE_MAX` (4 x `WORKERS`) GIF bir iş parçacığı için bekler, her biri en fazla `GIF_QUEUE_TIMEOUT_MS` (10 sn); bir oluşturma işlemi en fazla `GIF_RENDER_TIMEOUT_MS` (30 sn) sürebilir. Sunucu oluşturduğu GIF'leri `GIF_CACHE_MB` MB'lık (32 x `WORKERS`) bir önbellekte tutar, en uzun süredir kullanılmayan ilk çıkar; önbellekten gelen ya da başka bir istek için zaten oluşturulmakta olan bir GIF oluşturma maliyeti doğurmaz. Önbellek anahtarı, adlar dahil resmi değiştiren her şeyi kapsar.

      Her istek `gif` sınırına sayılır (iki uç nokta için birlikte, oyuncu başına dakikada 30). Oluşturulması gereken bir GIF ayrıca oluşturma sınırlarına da sayılır: oyuncu başına dakikada `GIF_USER_RENDERS_PER_MIN` (4) ve saatte `GIF_USER_RENDERS_PER_HOUR` (30); istemci başına, tüm hesapları birlikte, dakikada `GIF_IP_RENDERS_PER_MIN` (12) ve saatte `GIF_IP_RENDERS_PER_HOUR` (120); IPv6 /48 başına bunun 3 katı. İstemci indirdiği dosyayı yeniden istemek yerine saklamalı, bir 429 ya da 503 yanıtından sonra `retryAfter` kadar beklemelidir.

      Boyutlar: `small` (32 px kareler: 284 x 350 piksel), `medium` (48 px: 424 x 515), `large` (72 px: 628 x 762); koordinatlar olmadan 268 x 342, 400 x 503 ve 600 x 748. Başlangıç pozisyonu en az 1 sn ekranda kalır (gecikme daha uzunsa gecikme süresince), son pozisyon 3 sn kalır ve GIF döngü hâlinde oynar; ilk çerçeveden sonra resmin yalnızca değişen kısmı saklanır. 40 hamlelik bir oyun yaklaşık 135, 205 ve 325 KiB (small, medium, large), 150 hamlelik bir oyun 0,5; 0,8 ve 1,2 MiB tutar.
  - name: players
    x-displayName: Oyuncular
    description: 'Herkese açık profiller ve son oyunlar. Yalnızca herkese açık veriler: e-posta adresi, oturum, yaptırım ya da dürüstlük düzeyi asla yer almaz. Silinmiş bir hesabın profili yoktur.'
  - name: leaderboard
    x-displayName: Sıralama tablosu
    description: Her resmî kategorinin en iyi oyuncuları.
  - name: reports
    x-displayName: Bildirimler
    description: Yakın zamanda oynanan bir oyundaki rakibi moderatörlere bildirme.
  - name: pages
    x-displayName: HTML sayfaları
    description: |-
      E-postalardaki bağlantılardan açılan, tarayıcıya yönelik sayfalar. Bağlantıları `/api/v1` dışında, `https://<SERVER_PUBLIC_HOST>` adresini gösterir (port 443 değilse `:<PUBLIC_API_PORT>` ile).

      - Sayfalar hiçbir JavaScript çalıştırmaz ve hiçbir harici kaynak yüklemez. `Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'` ile sunulurlar.
      - Bir `GET` yalnızca bir düğme ya da form gösterir; böylece bağlantıyı açan bir e-posta tarama aracı onu tüketmez. Değişiklik `POST` ile gerçekleşir: `application/x-www-form-urlencoded` olarak gönderilen bir form (JSON gövdesi de kabul edilir); bağlantının token'ı gizli bir alandadır. İki kez verilen bir alan reddedilir.
      - Hatalar (hız sınırları, geçersiz alanlar) da HTML sayfalarıdır; başlıkları "Request refused" (istek reddedildi) ya da 500'den itibaren "Server error" (sunucu hatası) olur. Yalnızca sayfa eşleştirilmeden önce saptanan hatalar (çok uzun ya da hatalı biçimli bir istek hedefi, adres başına katman) JSON ile yanıtlanır.
x-tagGroups:
  - name: server
    x-displayName: Sunucu
    tags:
      - server-info
      - health
  - name: accounts
    x-displayName: Hesaplar
    tags:
      - auth
      - google-sign-in
      - sessions
      - account
      - two-step-verification
      - email-change
      - data-export
      - account-deletion
  - name: games
    x-displayName: Oyunlar
    tags:
      - game-history
      - games
      - gifs
  - name: community
    x-displayName: Oyuncular ve bildirimler
    tags:
      - players
      - leaderboard
      - reports
  - name: browser-pages
    x-displayName: Tarayıcı sayfaları
    tags:
      - pages
paths:
  /info:
    get:
      operationId: getServerInfo
      tags:
        - server-info
      summary: Sunucunun adını, sürümlerini, portlarını ve kayıt kurallarını al
      description: |-
        Bir istemcinin giriş yapmadan ya da bağlanmadan önce ihtiyaç duyduğu bilgiler: sunucunun adı ve kimliği, WebSocket protokol sürümleri ve WebSocket'in nerede olduğu, kayıt kuralları, resmî süre kontrolleri ve bir istemcinin bir formu göndermeden önce denetleyebileceği sınırlar.

        **Sınırlar:** yalnızca adres başına katman.
      security: []
      responses:
        '200':
          description: Sunucunun tanımı.
          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`: adres başına katman.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /auth/register:
    post:
      operationId: registerAccount
      tags:
        - auth
      summary: Hesap oluştur
      description: |-
        Bir hesap oluşturur: e-posta adresi, adrese gönderilen bağlantıyla onaylandığında ya da e-posta onayı yoksa hemen.

        **E-posta onayıyla** (`REQUIRE_EMAIL_VERIFICATION`, varsayılan): 202 `verification_sent`. Henüz bir hesap yoktur: kayıt 24 saat bekler ve bu sürede kullanıcı adını ayırır. Bu 24 saat boyunca geçerli bir bağlantı adrese gönderilir, adres başına 5 dakikada en fazla bir tane (`POST /auth/verify-email/resend` yenisini gönderir); bağlantı kullanıldığında (`/verify-email` sayfasındaki düğme) hesap, adresi onaylanmış olarak oluşturulur ve oyuncu artık giriş yapabilir. O zamana kadar bu kullanıcı adıyla yapılan bir giriş, bilinmeyen bir hesapta olduğu gibi `invalid_credentials` yanıtını alır ve herkese açık profil yoktur. Aynı adresle yapılan yeni bir kayıt, bekleyen kaydın yerini alır. Adresi zaten başka bir hesap kullanıyorsa da yanıt aynıdır: bu durumda bağlantı gönderilmez, bunun yerine adresin sahibine bir uyarı gider (saatte en fazla bir) ve kullanıcı adı aynı şekilde ayrılır; böylece hiçbir şey adresin bir hesabı olup olmadığını belli etmez. Bağlantısı kullanılmayan bir kayıt 24 saat sonra silinir ve kullanıcı adı yeniden serbest kalır.

        **E-posta onayı olmadan** (`REQUIRE_EMAIL_VERIFICATION=false`): 201 `ready`; hesap hemen oluşturulur ve giriş yapabilir.

        **Kurallar.** `username`: `USERNAME_MIN` ile `USERNAME_MAX` arası karakter (3 ile 20), harfler, rakamlar, `_` ve `-`; bir harf ya da rakamla başlar (`GET /info` bunları `limits` olarak verir); ayrılmış adlar (`admin`, `moderator`, `deleted`...) ve bazı önekler reddedilir; büyük/küçük harf ayrımı gözetilmeden benzersizdir. `email`: baştaki ve sondaki boşluklar kırpılır ve küçük harfle saklanır; noktalı bir alan adı içeren düz ASCII. `password`: en az `PASSWORD_MIN_LENGTH` (10) karakter ve en fazla 256 bayt UTF-8; kullanıcı adını ya da e-postanın yerel kısmını içermemeli ve yaygın bir parola olmamalıdır.

        **Hatalar**, bu sırayla denetlenir: 403 `registration_closed`; 400 `invalid_username`; 400 `invalid_email`; 400 `weak_password`; 409 `username_taken`; 428 `pow_required`; parola hash kuyruğu hataları; 409 `email_taken` (yalnızca e-posta onayı olmadan).

        **Sınırlar:** `auth`, ardından `auth_register` (istemci başına saatte 10 kayıt). **İş kanıtı:** `POW_REGISTER_BITS` 0'dan büyükse her zaman.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
            examples:
              first:
                summary: İlk deneme, iş kanıtı olmadan
                value:
                  username: alice
                  email: alice@example.org
                  password: correct horse battery
              withPow:
                summary: İş kanıtıyla yeniden gönderilen istek
                value:
                  username: alice
                  email: alice@example.org
                  password: correct horse battery
                  pow:
                    challenge: eyJ2IjoxLCJlIjoicmVnaXN0ZXIiLCJiIjoxOCwieCI6MTc5MDg4Mjk5MTIwMH0.6Ku_D7kw3S69OYxj-9KxsXszRCwz6ECnOQ-B6-v2erM
                    nonce: '123456'
      responses:
        '201':
          description: Hesap oluşturuldu ve giriş yapabilir (bu sunucuda e-posta onayı yok).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    const: ready
              example:
                status: ready
        '202':
          description: Kayıt, onay bağlantısının kullanılmasını bekliyor (ya da adresin zaten bir hesabı var; yanıt bunu belli etmez).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerificationSentStatus'
              example:
                status: verification_sent
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_request` (`field` ile), `invalid_json`, `invalid_username`, `invalid_email` ya da `reason` ile `weak_password`: `too_short`, `too_long`, `contains_username`, `contains_email` veya `too_common`.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`registration_closed`: bu sunucuda kayıt kapalı (`GET /info` yanıtında `registration: closed`).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`username_taken`: kullanıcı adı bir hesaba ait ya da başka bir adresle yapılmış bekleyen bir kayıt onu ayırmış. `email_taken`: adresi başka bir hesap kullanıyor (yalnızca e-posta onayı olmadan).'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '428':
          $ref: '#/components/responses/PowRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: `auth` ya da `auth_register` sınırı veya parola hash kuyruğu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (parola hash kuyruğu ya da veritabanı kilitli kaldı), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /auth/login:
    post:
      operationId: logIn
      tags:
        - auth
      summary: Parolayla giriş yap
      description: |-
        Bir kullanıcı adı ya da e-posta adresi ve bir parolayla giriş yapar. İki olası yanıt vardır, ikisi de 200 durum koduyla: bir oturum ya da, iki faktörlü kimlik doğrulama açıksa, 5 dakika içinde `POST /auth/login/mfa` ile tamamlanacak ikinci bir adım.

        Bilinmeyen hesap, yanlış parola ve parolası olmayan hesap aynı süre sonunda aynı yanıtı alır: 401 `invalid_credentials`. Hesap denetimleri (`banned`, `email_unverified`) ancak doğru bir paroladan sonra yapılır. Bir giriş adı için `AUTH_FAILURES_PER_ACCOUNT` (5) başarısız denemeden itibaren her deneme beklemek zorundadır (429 `too_many_attempts`).

        **Sınır:** `auth`. **İş kanıtı:** yalnızca bir başarısız giriş dalgası sırasında (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: Bir oturum ya da iki faktörlü kimlik doğrulamalı bir girişin ikinci adımı.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginAnswer'
              examples:
                session:
                  summary: Giriş yapıldı
                  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: İki faktörlü kimlik doğrulama açık
                  value:
                    mfaRequired: true
                    mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                    expiresIn: 300
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_credentials`: bilinmeyen hesap, yanlış parola ya da parolası olmayan bir hesap.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: 'Yalnızca doğru bir paroladan sonra: `until` ile `banned` (epoch ms; kalıcı askıya alma için `null`) ya da `email_unverified` (adresi henüz onaylanmamış eski bir hesap).'
        '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` (bu giriş adının başarısız deneme beklemesi) ya da `rate_limited` (`auth` sınırı, parola hash kuyruğu).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (parola hash kuyruğu ya da veritabanı kilitli kaldı), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /auth/login/mfa:
    post:
      operationId: logInWithSecondFactor
      tags:
        - auth
      summary: Girişi ikinci faktörle tamamla
      description: |-
        `POST /auth/login` ya da bir Google ile giriş (`POST /auth/sso/google/finish` veya `POST /auth/sso/google/link`) `mfaRequired` döndürdükten sonra, iki faktörlü kimlik doğrulamalı bir girişin ikinci adımı. `code` (6 haneli bir kimlik doğrulama uygulaması kodu ya da bir kurtarma kodu) veya `recoveryCode` gönderin. Burada kullanılan bir kurtarma kodu tükenir.

        Bir adım en fazla 5 yanlış kod kabul eder; parola sıfırlandığında ya da değiştirildiğinde de adım sona erer. Bir Google bağlamasının parola adımından sonra bağlama yalnızca kod burada doğrulandığında kaydedilir.

        **Sınırlar:** `auth` ve hangi adresten olursa olsun hesap için 15 dakikada en fazla `AUTH_MFA_PER_ACCOUNT` (10) kod; bunun ötesinde kod denetlenmez, böylece kurtarma kodu harcanmaz.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MfaLoginRequest'
            examples:
              authenticator:
                summary: Bir kimlik doğrulama uygulaması kodu
                value:
                  mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                  code: '123456'
              recovery:
                summary: Bir kurtarma kodu
                value:
                  mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                  recoveryCode: j7v5-3ezx-zn
      responses:
        '200':
          description: Giriş yapıldı.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionAnswer'
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_request`: ne `code` ne de `recoveryCode` gönderildi ya da gövde şemaya uymuyor; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_mfa_token`: adımın süresi doldu, adım kullanıldı ya da 5 yanlış koddan sonra sona erdi veya ilk adımdan bu yana parola sıfırlandı ya da değiştirildi (yeniden giriş yapın). `invalid_code`: yanlış ya da daha önce kullanılmış bir kod.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`banned` (`until` ile), `email_unverified`.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_already_linked`: yalnızca bir Google bağlamasının parola adımından sonra; Google hesabı bu arada başka bir hesaba bağlandı.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: yalnızca bir Google bağlamasının parola adımından sonra; hesap bu arada değişti; girişi baştan başlatın.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts`: hesabın başarısız deneme beklemesi ya da hesabın son 15 dakikadaki `AUTH_MFA_PER_ACCOUNT` kod hakkı tükendi. `rate_limited`: `auth` sınırı.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`retryAfter: 1` ile `server_busy`: veritabanı kilitli kaldı (bir Google bağlama adımından sonra hiçbir şey bağlanmadı: girişi baştan başlatın). `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /auth/logout:
    post:
      operationId: logOut
      tags:
        - sessions
      summary: Bu oturumu kapat
      description: |-
        Kullanılan token'ın oturumunu geçersiz kılar. Bu oturumla açılan WebSocket kapatılır. Gövde yok (ya da `{}`).

        **Sınır:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Çıkış yapıldı.
          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`: `{}` dışında bir gövde; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: `sessions` sınırı ya da hesap kotası.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (veritabanı kilitli kaldı), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /auth/logout-all:
    post:
      operationId: logOutEverywhere
      tags:
        - sessions
      summary: Tüm oturumları kapat
      description: |-
        Bu oturum dahil, hesabın tüm oturumlarını geçersiz kılar. Bu oturumlarla açılan WebSocket'ler kapatılır. Gövde yok (ya da `{}`).

        **Sınır:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Tüm oturumlar kapatıldı.
          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`: `{}` dışında bir gövde; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: `sessions` sınırı ya da hesap kotası.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (veritabanı kilitli kaldı), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /auth/verify-email/resend:
    post:
      operationId: resendVerificationEmail
      tags:
        - auth
      summary: Onay bağlantısını yeniden gönder
      description: |-
        E-posta onay bağlantısını yeniden gönderir. Adres ne olursa olsun yanıt 202 `accepted` olur; böylece adresi bir hesabın ya da bir kaydın kullanıp kullanmadığını hiçbir zaman belli etmez.

        Bir istek, adres başına 5 dakikada en fazla bir kez etki eder. O adresle bekleyen bir kaydın 24 saati, adresi başka bir hesap kullansa da kullanmasa da yeniden başlar; böylece kullanıcı adı her iki durumda da aynı süre ayrılmış kalır. Bağlantı yalnızca, adresin hesabı yoksa o kayıt için (24 saat geçerli yeni bir bağlantı öncekinin yerini alır) ya da o adrese sahip etkin, onaylanmamış bir hesap için gönderilir.

        **Sınırlar:** `auth`, ardından `auth_mail` (istemci başına saatte 10).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmailRequest'
            example:
              email: alice@example.org
      responses:
        '202':
          description: Kabul edildi (adres ne olursa olsun).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AcceptedStatus'
              example:
                status: accepted
        '400':
          $ref: '#/components/responses/BadRequest'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: `auth` ya da `auth_mail` sınırı (adres ne olursa olsun).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`retryAfter: 1` ile `server_busy`: hesap aranırken ya da onaylanmamış bir hesabın bağlantısı kaydedilirken veritabanı kilitli kaldı (bekleyen bir kaydın yenilenmesi en iyi çaba esasına göre yapılır ve isteği hiçbir zaman başarısız kılmaz); adresin 5 dakikası yine de başlar, bu yüzden bu süre içindeki yeni bir istek hiçbir şey yapmaz. `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /auth/password/forgot:
    post:
      operationId: requestPasswordReset
      tags:
        - auth
      summary: Parola sıfırlama bağlantısı gönder
      description: |-
        Bir saat geçerli olan ve `/reset-password` sayfasını açan bir parola sıfırlama bağlantısı gönderir. Adres ne olursa olsun yanıt 202 `accepted` olur. Bağlantı yalnızca etkin bir hesaba gider, adres başına 5 dakikada en fazla bir kez. Yalnızca Google ile giriş yapan bir hesap ilk parolasını bu yolla belirler.

        Parola kurtarma, API'nin en katı sınırlarına sahiptir ve hepsi tüm sunucu için sayılır: istemci (bir IPv4 adresi ya da bir IPv6 /64 bloğu) başına saatte 3 istek (`AUTH_FORGOT_PER_HOUR`) ve 24 saatte 10 istek (`AUTH_FORGOT_PER_DAY`), IPv6 /48 başına bu sayıların 3 katı; bunlara `auth` sınırı (10 dakikada 20) ve adres başına 5 dakikada bir e-posta kuralı da eklenir. Ne 202 ne de 429 yanıtı, adresi bir hesabın kullanıp kullanmadığını belli eder. Yeni parolanın belirlenmesinin kendi sınırı vardır (`auth_reset`).

        **Sınırlar:** `auth`, `auth_forgot`, ardından `auth_forgot_day`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmailRequest'
            example:
              email: alice@example.org
      responses:
        '202':
          description: Kabul edildi (adres ne olursa olsun).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AcceptedStatus'
              example:
                status: accepted
        '400':
          $ref: '#/components/responses/BadRequest'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: `auth`, `auth_forgot` ya da `auth_forgot_day` sınırı (adres ne olursa olsun).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`retryAfter: 1` ile `server_busy`: hesap aranırken ya da bağlantı kaydedilirken veritabanı kilitli kaldı; adresin 5 dakikası yine de başlar, bu yüzden bu süre içindeki yeni bir istek bağlantı göndermez. `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /auth/password/reset:
    post:
      operationId: resetPassword
      tags:
        - auth
      summary: Sıfırlama bağlantısının token'ıyla yeni parola belirle
      description: |-
        Bir sıfırlama bağlantısının token'ıyla yeni bir parola belirler (`/reset-password` sayfası da aynısını yapar). Yeni parola kayıt kurallarına uyar.

        Sıfırlamanın şu etkileri vardır ve hepsi tek bir işlemde gerçekleşir (ya hepsi ya hiçbiri): tüm oturumları geçersiz kılar ve bekleyen bir e-posta değişikliğini iptal eder; hesabın diğer sıfırlama bağlantıları çalışmaz hâle gelir; adres onaylanmış sayılır (bağlantı bunu kanıtladı); hesabın sahibine bir e-posta gider. İki faktörlü kimlik doğrulamaya dokunulmaz. Bir parola hash kuyruğu hatasından, bir 503 ya da bir 500 yanıtından sonra hiçbir şey değişmemiştir: eski parola ve oturumlar kalır, bağlantı da geçerliliğini korur.

        **Sınırlar:** `auth`, ardından `auth_reset` (istemci başına saatte 10, IPv6 /48 başına 30; sayfayla ortak: her deneme bir parolanın hash'ini hesaplar).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasswordResetRequest'
            example:
              token: A0ATfmXCDKFC8UkSxLJCD85K7au5nMIrZ2P3K-1VWQg
              newPassword: a much better passphrase
      responses:
        '200':
          description: Parola değiştirildi ve tüm oturumlar kapatıldı.
          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`: bağlantı geçersiz, kullanılmış ya da süresi dolmuş veya hesabın artık sahip olmadığı bir adrese gönderilmiş. `weak_password` (`reason` ile). `invalid_request`, `invalid_json`.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: `auth` ya da `auth_reset` sınırı veya parola hash kuyruğu.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: parola hash kuyruğu ya da veritabanı kilitli kaldı (`retryAfter: 1`, hiçbir şey değişmedi). `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /auth/sso/google/start:
    post:
      operationId: startGoogleSignIn
      tags:
        - google-sign-in
      summary: Google ile girişi başlat
      description: |-
        PKCE sınaması ve oyunun `127.0.0.1` üzerindeki dinleyicisinin portu için bir Google ile giriş denemesi başlatır. Yanıt, sistem tarayıcısında açılacak Google URL'sini ve denemenin `state` değerini verir; deneme 10 dakika geçerlidir.

        `authUrl` şunları içerir: `client_id`, `redirect_uri` (`redirectPort` ve sunucunun köken etiketinden oluşturulur), `response_type=code`, `scope=openid email profile`, `state`, `nonce`, `code_challenge_method=S256` ile birlikte `code_challenge` (sunucunun Google için kullandığı kendi doğrulayıcısının S256 değeri) ve `prompt=select_account`. Oyun bu URL'yi açmadan önce denetler (bkz. bölüm açıklaması).

        **Sınır:** `sso_start`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SsoStartRequest'
            example:
              codeChallenge: 6e7diXEYxG7OTYw7STfNOltEeLxAilPthC_txzaE0xA
              redirectPort: 51234
      responses:
        '200':
          description: Deneme.
          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`: bu sunucu Google ile giriş sunmuyor.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: `sso_start` sınırı.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (veritabanı kilitli kaldı), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /auth/sso/google/web/start:
    post:
      operationId: startGoogleWebSignIn
      tags:
        - google-sign-in
      summary: Bir web sitesinde Google ile girişi başlat
      description: |-
        Bir web sitesi için, sayfasının PKCE sınamasıyla bir Google ile giriş denemesi başlatır. Yanıt oyundakiyle aynıdır: gidilecek Google URL'si ve denemenin `state` değeri; deneme 10 dakika geçerlidir. `GET /info` yanıtında `sso.googleWeb: true` olduğunda sunulur.

        `authUrl`, web sitesinin `client_id` değerini ve `redirect_uri=GOOGLE_WEB_REDIRECT_URI` parametresini, diğer parametreler oyundakiyle aynı olmak üzere içerir. Sayfa bu URL'ye gitmeden önce onu denetler (bkz. bölüm açıklamasındaki **Bir web sitesinde**).

        **Sınır:** `sso_start`; oyunun başlatma isteğiyle ortak.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SsoWebStartRequest'
            example:
              codeChallenge: 6e7diXEYxG7OTYw7STfNOltEeLxAilPthC_txzaE0xA
      responses:
        '200':
          description: Deneme.
          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`: eksik ya da hatalı biçimli bir `codeChallenge` veya başka herhangi bir alan (`redirectPort` dahil).'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: bu sunucu Google ile giriş sunmuyor ya da web sitelerine sunmuyor.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: `sso_start` sınırı (oyunun başlatma isteğiyle ortak).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (veritabanı kilitli kaldı), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /auth/sso/google/finish:
    post:
      operationId: finishGoogleSignIn
      tags:
        - google-sign-in
      summary: Google'ın yanıtını sunucuya ilet
      description: |-
        Google'ın oyunun dinleyicisine ya da web sitesinin sayfasına gönderdiği `code` ve `state` değerlerini, deneme kimliği ve PKCE doğrulayıcısıyla birlikte sunucuya iletir. Bir deneme kimliği doğrulayıcı olmadan işe yaramaz; kod da sunucunun kendi PKCE doğrulayıcısı ve istemci gizli anahtarı (client secret) olmadan işe yaramaz.

        Sunucu önce denemeyi (bilinmeyen, kullanılmış ya da süresi dolmuş: 410), ardından doğrulayıcıyı (yanlış bir doğrulayıcı denemeyi kullanılabilir bırakır) denetler; sonra denemeyi bir kez kullanır, `state` ve `iss` değerlerini denetler, kodu Google ile takas eder ve ID token'ını doğrular. Yanıt (200) şunlardan biridir:

        - `{ token, expiresAt, user }`: bağlı hesaba giriş yapıldı;
        - `{ mfaRequired, mfaToken, expiresIn }`: `POST /auth/login/mfa` ile devam edin;
        - `{ needsUsername, ssoTicket, suggestedUsername }`: yeni bir hesap; 10 dakika içinde `POST /auth/sso/complete` ile devam edin. `suggestedUsername` Google adından ya da adresten türetilir ve uygun bir ad bulunamazsa `""` olur;
        - `{ needsPassword, linkTicket, username, expiresIn }`: o adrese sahip hesabın bir parolası var; `POST /auth/sso/google/link` ile devam edin. Henüz hiçbir şey bağlanmadı.

        **Sınır:** `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: Bir oturum, ikinci bir adım ya da ilk kez yapılan bir Google ile girişin sonraki adımı.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SsoFinishAnswer'
              examples:
                session:
                  summary: Bağlı hesaba giriş yapıldı
                  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: İki faktörlü kimlik doğrulama açık
                  value:
                    mfaRequired: true
                    mfaToken: mfa_m4wXAVhYCMWG0PX7MLcIpBeccBFg0oxPvoRujzGWjO8
                    expiresIn: 300
                needsUsername:
                  summary: Yeni bir oyuncu
                  value:
                    needsUsername: true
                    ssoTicket: sso_gYIKKhwnz1AqWfFxOC3R-ZvCACw06q74fEMtGEOJQWU
                    suggestedUsername: alice
                needsPassword:
                  summary: Adresi parolalı bir hesap kullanıyor
                  value:
                    needsPassword: true
                    linkTicket: sso_ZfEI6iZjePTHbnbevupaD9O7GjBCzcL-GJu2XGOdFGc
                    username: alice
                    expiresIn: 600
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_verifier`: doğrulayıcı denemenin sınamasıyla eşleşmiyor (deneme kullanılabilir kalır). `sso_email_unverified`: Google adresi doğrulamamış. `registration_closed`. `account_disabled`. Yalnızca bağlı bir hesap için: `banned` (`until` ile), `email_unverified`.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: bu sunucu Google ile giriş sunmuyor.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_account_exists`: adresi, parolası olmayan etkin bir hesap kullanıyor (hesabın adı verilmez).'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: bilinmeyen, kullanılmış ya da süresi dolmuş bir deneme.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: `sso_finish` sınırı.'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/BadGateway'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (veritabanı kilitli kaldı), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /auth/sso/google/link:
    post:
      operationId: linkGoogleAccount
      tags:
        - google-sign-in
      summary: Google'ı parolasıyla mevcut bir hesaba bağla
      description: |-
        `finish` uç noktası `needsPassword` döndürdükten sonra: oyunda yazılan hesap parolasıyla Google'ı mevcut hesaba bağlar. Yanıt (200) bir oturumdur (bağlama kaydedilir ve oyuncu giriş yapmış olur) ya da iki faktörlü kimlik doğrulama açıksa `{ mfaRequired, mfaToken, expiresIn }`: `POST /auth/login/mfa` ile devam edin; bağlama yalnızca orada bir kod doğrulandığında kaydedilir.

        Yanlış bir parola bileti kullanılabilir bırakır, toplam 5 denemeye kadar. Deneme hakkı, parola denetlenmeden hemen önce düşülür: 429 `too_many_attempts` ya da 428 `pow_required` hiç hak düşürmez; parola hash kuyruğunun bir reti (503 `server_busy`, 429 `rate_limited`) ise bir hak düşürmüştür ve hesabın başarısız denemesi olarak sayılır. Başarısız deneme beklemesi, `POST /auth/login` ile aynı sayaçtır.

        **Sınır:** `auth` (IPv6 /48 sayımıyla birlikte). **İş kanıtı:** `POST /auth/login` ile aynı.
      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: Bir oturum ya da girişin ikinci adımı.
          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`: yanlış parola; bilet toplam 5 deneme boyunca geçerli kalır.'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`banned` (`until` ile), yalnızca doğru parola girildikten sonra.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`sso_disabled`: bu sunucuda Google ile giriş sunulmuyor.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`sso_already_linked`: Google hesabı bu arada başka bir hesaba bağlandı.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: bilinmeyen, kullanılmış ya da süresi dolmuş bir bilet, 5. yanlış parola, `finish` adımından bu yana durumu veya adresi değişmiş bir hesap ya da bağlama kaydedilirken durumu, adresi, parolası veya iki faktörlü kimlik doğrulaması değişmiş bir hesap. Girişi yeniden başlatın.'
        '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` (hesabın başarısız deneme beklemesi) veya `rate_limited` (`auth` sınırı, parola hash kuyruğu).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: parola hash kuyruğu ya da veritabanı kilitli kaldı (`retryAfter: 1`, hiçbir şey bağlanmadı). `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /auth/sso/complete:
    post:
      operationId: completeGoogleSignUp
      tags:
        - google-sign-in
      summary: İlk kez Google ile girişte hesabı oluştur
      description: |-
        `finish`, `needsUsername` yanıtını verdikten sonra: hesabı seçilen kullanıcı adıyla oluşturur (kayıt kuralları geçerlidir) ve bu hesapla giriş yapar. Hesabın parolası yoktur: `hasPassword` `false` olur ve "Parolanızı mı unuttunuz?" (`POST /auth/password/forgot`) hesaba bir parola verir.

        **Sınır:** `auth` (IPv6 /48 bloğu sayımıyla birlikte).
      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: Hesap oluşturuldu ve giriş yapıldı.
          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`: bu sunucuda Google ile giriş sunulmuyor.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`username_taken` (kullanıcı adı bir hesapta kullanılıyor ya da başka bir adresin bekleyen kaydı onu ayırmış), `sso_already_linked`, `email_taken`.'
        '410':
          $ref: '#/components/responses/Gone'
          description: '`sso_expired`: bilinmeyen, kullanılmış ya da süresi dolmuş bir bilet.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: `auth` sınırı.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (veritabanı kilitli kaldı), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /auth/sessions:
    get:
      operationId: listSessions
      tags:
        - sessions
      summary: Giriş yapılmış cihazları listele
      description: |-
        Hesabın etkin oturumları (giriş yapılmış cihazlar), en son kullanılan önce. `current`, isteği yapan oturumu işaretler. `lastSeenAt` en fazla 5 dakikada bir güncellenir (sunucu çok geride kaldığı sürece daha geç); `expiresAt` mutlak bitiş zamanıdır ve boşta kalma sınırı oturumu daha erken sonlandırabilir.

        **Sınır:** `sessions`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Etkin oturumlar.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionList'
              example:
                sessions:
                  - id: 3
                    createdAt: 1790882839708
                    lastSeenAt: 1790882839708
                    expiresAt: 1798658839708
                    clientLabel: Laptop
                    current: false
                  - id: 1
                    createdAt: 1790882839708
                    lastSeenAt: 1790882839708
                    expiresAt: 1798658839708
                    clientLabel: Scacelith 1.4 (Windows)
                    current: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: `sessions` sınırı veya hesap kotası.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (veritabanı kilitli kaldı), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /auth/sessions/{id}:
    delete:
      operationId: revokeSession
      tags:
        - sessions
      summary: Bir cihazda çıkış yap
      description: |-
        Hesabın bir oturumunu kapatır; geçerli oturum da kapatılabilir. Bu oturumla açılan WebSocket kapatılır. Gövde yok (veya `{}`).

        **Sınır:** `sessions`.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/SessionId'
      responses:
        '200':
          description: Oturum kapatıldı.
          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`: `{}` dışında bir gövde ya da geçerli URL kodlaması olmayan bir `id`; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: bu hesapta bu kimliğe sahip etkin bir oturum yok.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: `sessions` sınırı veya hesap kotası.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (veritabanı kilitli kaldı), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /account/me:
    get:
      operationId: getAccount
      tags:
        - account
      summary: Hesabı, puanlarını ve etkin yaptırımlarını getir
      description: |-
        Hesap, oyuncusunun gördüğü hâliyle: hesap görünümü (her giriş yanıtındaki `user` de budur), oyuncunun puanlı oyun oynadığı her kategori için bir puan kaydı, etkin yaptırımlar ve askıya alma. Hile karşıtı sistemin dürüstlük düzeyi hiçbir zaman gösterilmez.

        **Sınır:** `account`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Hesap.
          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` veya `invalid_token` (hesap silinmişse de).'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: `account` sınırı veya hesap kotası.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (veritabanı kilitli kaldı), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /account/preferences:
    put:
      operationId: updatePreferences
      tags:
        - account
      summary: Doğrudan meydan okumaları kabul et veya reddet
      description: |-
        Diğer oyuncuların bu oyuncuya adıyla meydan okuyup okuyamayacağını belirler. `none` ile doğrudan meydan okumalar reddedilir: meydan okuyan oyuncuya, oyuncunun müsait olmadığı bildirilir. Yeniden kimlik doğrulama gerekmez.

        **Sınır:** `account`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Preferences'
            example:
              acceptChallenges: none
      responses:
        '200':
          description: Şu anda geçerli olan tercihler.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - preferences
                properties:
                  preferences:
                    $ref: '#/components/schemas/Preferences'
              example:
                preferences:
                  acceptChallenges: none
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: `account` sınırı veya hesap kotası.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (veritabanı kilitli kaldı), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /account/password:
    post:
      operationId: changePassword
      tags:
        - account
      summary: Parolayı değiştir
      description: |-
        Parolayı değiştirir. Mevcut parola gerekir, ancak iki faktörlü kimlik doğrulama açık olsa bile ikinci faktör gerekmez. Yeni parola kayıt kurallarına uymalıdır (mevcut paroladan sonra denetlenir).

        Değişikliğin etkileri şunlardır ve tek bir işlemde gerçekleşir (hepsi ya da hiçbiri): diğer tüm oturumları geçersiz kılar (bu oturumda giriş açık kalır), bekleyen bir e-posta değişikliğini iptal eder, hesabın parola sıfırlama bağlantılarını çalışmaz hâle getirir ve hesap sahibine bir e-posta gönderir.

        **Yeniden kimlik doğrulama:** yalnızca parola. **Sınırlar:** `reauth`, ardından `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: Parola değiştirildi.
          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` (yalnızca Google ile giriş yapılan bir hesap), `weak_password` (`reason` ile), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password`: yanlış mevcut parola ya da istek denetlenirken gerçekleşen bir parola sıfırlaması ya da değişikliği.'
        '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` (hesabın başarısız yeniden kimlik doğrulamaları) veya `rate_limited` (`reauth` ya da `reauth_user` sınırı, hesap kotası, parola hash kuyruğu).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: parola hash kuyruğu ya da veritabanı kilitli kaldı (`retryAfter: 1`, hiçbir şey değişmedi). `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /account/mfa/totp/setup:
    post:
      operationId: startTotpSetup
      tags:
        - two-step-verification
      summary: İki faktörlü kimlik doğrulamayı açmaya başla
      description: |-
        Bekleyen yeni bir kimlik doğrulama gizli anahtarı kaydeder (daha önce bekleyen bir anahtar varsa onun yerini alır) ve bunu kimlik doğrulama uygulaması için döndürür (metin olarak ve QR kodu olarak gösterilecek bir `otpauth://` URI'si olarak). İki faktörlü kimlik doğrulama henüz açık değildir: `POST /account/mfa/totp/enable` bu gizli anahtarın bir koduyla onu açar.

        **Yeniden kimlik doğrulama:** yalnızca parola. **Sınırlar:** `reauth`, ardından `reauth_user`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasswordOnlyRequest'
            example:
              password: correct horse battery
      responses:
        '200':
          description: Bekleyen gizli anahtar.
          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` (yalnızca Google ile giriş yapılan bir hesap), `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` (paroladan önce denetlenir).'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (hesabın başarısız yeniden kimlik doğrulamaları) veya `rate_limited` (`reauth` ya da `reauth_user` sınırı, hesap kotası, parola hash kuyruğu).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (parola hash kuyruğu ya da veritabanı kilitli kaldı), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /account/mfa/totp/enable:
    post:
      operationId: enableTotp
      tags:
        - two-step-verification
      summary: İki faktörlü kimlik doğrulamayı açmayı tamamla ve kurtarma kodlarını al
      description: |-
        Bekleyen gizli anahtarın bir koduyla (tam 6 hane) iki faktörlü kimlik doğrulamayı açar. Burada parola istenmez; kurulumda verilmişti. Yanıt, yalnızca bu kez gösterilen 10 kurtarma kodunu içerir.

        **Sınırlar:** `reauth`, ardından `reauth_user`; yanlış bir kod, hesabın başarısız yeniden kimlik doğrulamalarına sayılır.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TotpEnableRequest'
            example:
              code: '123456'
      responses:
        '200':
          description: İki faktörlü kimlik doğrulama açık.
          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` tam 6 hane değil ya da gövde şemaya uymuyor; `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_code`: yanlış kod (cihazın saatini kontrol edin).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`mfa_already_enabled`; `mfa_setup_required`: bekleyen gizli anahtar yok (önce `POST /account/mfa/totp/setup` çağrısını yapın).'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (hesabın başarısız yeniden kimlik doğrulamaları) veya `rate_limited` (`reauth` ya da `reauth_user` sınırı, hesap kotası).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (veritabanı kilitli kaldı), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /account/mfa/totp/disable:
    post:
      operationId: disableTotp
      tags:
        - two-step-verification
      summary: İki faktörlü kimlik doğrulamayı kapat
      description: |-
        İki faktörlü kimlik doğrulamayı kapatır. Gizli anahtar ve kurtarma kodları silinir ve hesap sahibine bir e-posta gönderilir.

        **Yeniden kimlik doğrulama:** parola ve bir kimlik doğrulama uygulaması kodu ya da bir kurtarma kodu (`code` veya `recoveryCode` alanlarından biri zorunludur). **Sınırlar:** `reauth`, ardından `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: İki faktörlü kimlik doğrulama kapalı.
          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` (yalnızca Google ile giriş yapılan bir hesap), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`mfa_code_required` (ne `code` ne de `recoveryCode` var; paroladan önce denetlenir), `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` (başarısız yeniden kimlik doğrulamalar ya da bu hesapta denenen çok fazla kod) veya `rate_limited` (`reauth` ya da `reauth_user` sınırı, hesap kotası, parola hash kuyruğu).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (parola hash kuyruğu ya da veritabanı kilitli kaldı), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /account/mfa/recovery-codes:
    post:
      operationId: regenerateRecoveryCodes
      tags:
        - two-step-verification
      summary: Kurtarma kodlarını değiştir
      description: |-
        Kurtarma kodlarını 10 yeni kodla değiştirir; eski kodlar artık çalışmaz.

        **Yeniden kimlik doğrulama:** parola ve `code` içinde bir kimlik doğrulama uygulaması kodu (kurtarma kodu 403 `invalid_code` ile reddedilir). **Sınırlar:** `reauth`, ardından `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: Yalnızca bu kez gösterilen yeni kurtarma kodları.
          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` (yalnızca Google ile giriş yapılan bir hesap), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password`, `mfa_code_required`, `invalid_code` (kurtarma kodu için de).'
        '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` (başarısız yeniden kimlik doğrulamalar ya da bu hesapta denenen çok fazla kod) veya `rate_limited` (`reauth` ya da `reauth_user` sınırı, hesap kotası, parola hash kuyruğu).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (parola hash kuyruğu ya da veritabanı kilitli kaldı), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /account/email:
    post:
      operationId: changeEmail
      tags:
        - email-change
      summary: E-posta adresini değiştir
      description: |-
        Hesabın e-posta adresini değiştirir. `newEmail` baştaki ve sondaki boşluklardan arındırılır ve küçük harfe çevrilir, ardından bir kayıt adresi gibi denetlenir.

        **E-posta onayıyla** (varsayılan): 202 `verification_sent`. Yeni adrese 24 saat geçerli bir bağlantı gönderilir; bağlantı `/confirm-email-change` sayfasını açar ve adres yalnızca oyuncu o sayfadaki düğmeye bastığında değişir. O zamana kadar `GET /account/me` yeni adresi `pendingEmail` olarak gösterir. Yeni bir istek bekleyen isteğin yerini alır; bir parola değişikliği veya sıfırlaması onu iptal eder. Kim isterse istesin, belirli bir yeni adrese 5 dakikada en fazla bir bağlantı gönderilir (zaten bekleyen değişiklik için yapılan bir istek, daha önce gönderilmiş ve geçerliliğini koruyan bağlantıyı korur); yanıt aynıdır. Mevcut adrese, maskelenmiş bir adrese (`a***@example.org`) değişiklik istendiğini bildiren bir uyarı gider. Yeni adresi başka bir hesap zaten kullanıyorsa yanıt ve `pendingEmail` aynıdır: bu durumda bağlantı gönderilmez, dolayısıyla bu değişiklik hiçbir zaman tamamlanmaz ve bunun yerine o adresin sahibi bir uyarı alır (saatte en fazla bir tane).

        Bağlantı onaylandığında adres değişir ve onaylanmış sayılır, cihazlarda giriş açık kalır, daha önce gönderilen bağlantılar (onay, parola sıfırlama, diğer değişiklikler) çalışmaz hâle gelir ve eski adrese, yeni adres maskelenmiş olarak bilgi verilir.

        **E-posta onayı olmadan** (`REQUIRE_EMAIL_VERIFICATION=false`): adres hemen değişir (200 `email_changed`) ve eski adrese bilgi verilir. Adresi başka bir hesap kullanıyorsa yanıt 409 `email_taken` olur ve o adresin sahibi uyarıyı alır.

        **Yeniden kimlik doğrulama:** parola ve, iki faktörlü kimlik doğrulama açıksa, bir kimlik doğrulama uygulaması kodu ya da bir kurtarma kodu. `invalid_email` ve `same_email` paroladan önce denetlenir, bu yüzden başarısız deneme olarak sayılmaz. **Sınırlar:** `reauth`, ardından `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: Adres hemen değişti (bu sunucuda e-posta onayı yok).
          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: Kaydedildiği hâliyle yeni adres.
              example:
                status: email_changed
                email: alice.new@example.org
        '202':
          description: Yeni adrese bir onay bağlantısı gönderildi (ya da adres başka bir hesaba ait; yanıt bunu belli etmez).
          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` (hesabın mevcut adresi), `password_not_set` (yalnızca Google ile giriş yapılan bir hesap), `invalid_request`, `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`invalid_password` (bir parola değişikliği veya sıfırlaması isteğin önüne geçtiğinde de: bağlantı gönderilmez), `mfa_code_required`, `invalid_code`.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '409':
          $ref: '#/components/responses/Conflict'
          description: '`email_taken`: yalnızca e-posta onayı olmadan.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`too_many_attempts` (başarısız yeniden kimlik doğrulamalar ya da bu hesapta denenen çok fazla kod) veya `rate_limited` (`reauth` ya da `reauth_user` sınırı, hesap kotası, parola hash kuyruğu).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`: parola hash kuyruğu ya da veritabanı kilitli kaldı (`retryAfter: 1`: hiçbir şey değişmedi ve aynı istek yeniden gönderilebilir). `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /account/export:
    post:
      operationId: exportAccountData
      tags:
        - data-export
      summary: Hesabın verilerini indir
      description: |-
        Sunucunun hesap hakkında sakladığı her şey, kaydedilecek tek bir JSON dosyası olarak (`format` `scacelith-account-export`, `version` 1). Dışa aktarma bir güvenlik olayı kaydeder (`account_exported`). Belgedeki `notes` dizisi, nelerin dışarıda bırakıldığını oyuncuya sade bir İngilizceyle anlatır.

        **Dışa aktarmada asla bulunmayanlar:** parola hash'i, iki faktörlü kimlik doğrulamanın gizli anahtarı ve kurtarma kodları; herhangi bir oturum veya bağlantı token'ı ya da bunların hash'i; hile karşıtı sistemin verileri (dürüstlük düzeyi ve skoru, anormallikler, oyunların analizi, bir bildirimin ağırlığı); diğer oyuncuların bu oyuncu hakkında yaptığı bildirimler; moderatörlerin kimlikleri; diğer oyuncuların özel verileri (rakipler herkese açık adları ve puanlarıyla görünür, başka bir oyuncunun yaptırım alıp almadığını hiçbir şey belli etmez ve başka bir kişiye ait olabilecek hiçbir IP adresi dahil edilmez).

        **Yeniden kimlik doğrulama:** parola ve, iki faktörlü kimlik doğrulama açıksa, bir kimlik doğrulama uygulaması kodu ya da bir kurtarma kodu. **Sınırlar:** `account_export` (oyuncu başına saatte 5, tüm sunucu için; başarısızlar dahil her deneme sayılır; ilk olarak denetlenir), ardından `reauth` ve `reauth_user`. **Süre sınırı:** 60 sn.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReauthCredentials'
            example:
              password: correct horse battery
              code: '123456'
      responses:
        '200':
          description: Dışa aktarma belgesi, ek olarak.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            Content-Disposition:
              description: '`attachment; filename="scacelith-account-<username>.json"`. Kullanıcı adındaki harf, rakam, `_`, `.` ve `-` dışındaki karakterler `_` olur.'
              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` (yalnızca Google ile giriş yapılan bir hesap), `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` (başarısız yeniden kimlik doğrulamalar ya da bu hesapta denenen çok fazla kod) veya `rate_limited` (`account_export`, `reauth` ya da `reauth_user` sınırı, hesap kotası, parola hash kuyruğu).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (veritabanı kilitli kaldı, `Retry-After: 1` başlığıyla), `server_busy` (parola hash kuyruğu), `timeout` (60 sn). Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /account/delete:
    post:
      operationId: deleteAccount
      tags:
        - account-deletion
      summary: Hesabı sil
      description: |-
        Hesabı siler; bu işlem geri alınamaz.

        - Tüm oturumlar hemen geçersiz kılınır: token bundan sonra 401 `invalid_token` alır.
        - Kullanıcı adı, hesapta ve her oyun kaydında `deleted#<id>` olur.
        - Şunlar silinir: e-posta adresi, parola hash'i, iki faktörlü kimlik doğrulamanın gizli anahtarı ve kurtarma kodları, oturumlar ve bağlantı token'ları, Google bağlaması, hile karşıtı sistemin dürüstlük kaydı ve güvenlik olaylarıyla birlikte saklanan IP adresleri.
        - Puanlar ve oyunlar korunur. Oyunlar anonim ad altında okunabilir kalır ve `GET /players/{username}` eski ad için 404 yanıtını verir.

        **Yeniden kimlik doğrulama:** parola ve, iki faktörlü kimlik doğrulama açıksa, bir kimlik doğrulama uygulaması kodu ya da bir kurtarma kodu. **Sınırlar:** `reauth`, ardından `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: Hesap silindi.
          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` (yalnızca Google ile giriş yapılan bir hesap), `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` (başarısız yeniden kimlik doğrulamalar ya da bu hesapta denenen çok fazla kod) veya `rate_limited` (`reauth` ya da `reauth_user` sınırı, hesap kotası, parola hash kuyruğu).'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (parola hash kuyruğu ya da veritabanı kilitli kaldı), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /account/games:
    get:
      operationId: listAccountGames
      tags:
        - game-history
      summary: Oyuncunun oyunlarını filtreleyerek ve sayfalayarak listele
      description: |-
        Giriş yapmış oyuncunun oyunları: en yeni önce, filtrelenmiş ve sayfalanmış olarak, filtreye uyan oyun sayısıyla birlikte. Tüm sorgu parametreleri isteğe bağlıdır ve boş bir değer, parametre hiç verilmemiş gibi sayılır.

        Sayfalama: önceki sayfanın `next` değerini `before` olarak gönderin; son sayfada `next` `null` olur. `total`, tüm sayfalar genelinde filtreye uyan oyunları sayar.

        **Sınır:** `account_games`.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/BeforeQuery'
        - $ref: '#/components/parameters/LimitQuery'
        - name: category
          in: query
          required: false
          description: Resmî bir kategori kimliği (`3+2` veya `3%2B2`) ya da başka bir süre kontrolüyle oynanan tüm oyunlar için `custom`.
          schema:
            type: string
            pattern: '^\s*([0-9]+[+ ][0-9]+|custom)\s*$'
          example: '3+2'
        - name: rated
          in: query
          required: false
          description: Yalnızca puanlı (`true`) veya puansız (`false`) oyunlar.
          schema:
            type: string
            enum:
              - 'true'
              - 'false'
        - name: result
          in: query
          required: false
          description: Oyuncunun açısından yalnızca kazanılan, kaybedilen veya berabere biten oyunlar. İptal edilen oyunlar yalnızca bu filtre olmadan görünür.
          schema:
            type: string
            enum:
              - win
              - loss
              - draw
      responses:
        '200':
          description: Geçmişin bir sayfası.
          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` bir oyun kimliği değil), `invalid_limit` veya `invalid_filter` (`category`, `rated` ya da `result`); her biri, parametreyi belirten `field` ile.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: `account_games` sınırı veya hesap kotası.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (veritabanı kilitli kaldı; gövdede `retryAfter: 1`, `Retry-After` başlığı yok), `server_busy` (oturum sorgulaması veritabanını kilitli buldu), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /games/{id}:
    get:
      operationId: getGame
      tags:
        - games
      summary: Bir oyun kaydını hamleleri ve saatleriyle getir
      description: |-
        Hamleleri ve saatleriyle bir oyun kaydı. Token olmadan ya da oyunu oynamamış bir oyuncunun token'ıyla yanıt, herkese açık yanıttır. Token'ın sahibi olan oyuncu oyunu oynamışsa yanıta `you` ve `reportable` eklenir.

        **Sınır:** `public_read` (token varsa oyuncu başına, yoksa istemci başına).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
      responses:
        '200':
          description: Oyun kaydı.
          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`: en fazla 16 haneli pozitif bir tam sayı değil (2^53''ün altında); `invalid_request`: geçerli URL kodlaması değil.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: bir token gönderildi ve geçerli değil.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: böyle bir oyun yok.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: `public_read` sınırı veya hesap kotası.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (veritabanı kilitli kaldı; gövdede `retryAfter: 1`, `Retry-After` başlığı yok), `server_busy` (oturum sorgulaması veritabanını kilitli buldu), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /games/{id}/pgn:
    get:
      operationId: getGamePgn
      tags:
        - games
      summary: Bir oyunu PGN dosyası olarak indir
      description: |-
        Aynı oyun, PGN dosyası olarak: `\n` satır sonlarıyla tek bir oyun ve 80 sütunun altındaki satırlarda hamle metni. Yanıt, token olsa da olmasa da aynıdır.

        Etiketler, bu sırayla: `Event` (`<SERVER_NAME> rated <category>` veya `<SERVER_NAME> casual <category>`), `Site` (`SERVER_PUBLIC_HOST`), `Date` (UTC başlangıç tarihi), `Round`, `White`, `Black`, `Result` (iptal edilen bir oyun için `*`), `UTCDate` ve `UTCTime` (başlangıç), `WhiteElo` ve `BlackElo` (başlangıçtaki puanlar ya da `-`), `WhiteRatingDiff` ve `BlackRatingDiff` (her puanlı oyunda `+10` ve `-10` gibi değişimler; puan kuralları puanı olduğu yerde bıraktığında `+0`; puansız, özel süre kontrollü ya da iptal edilen bir oyunda ikisi de bulunmaz), `TimeControl` (saniye cinsinden), `Termination` (PGN standardındaki bir değer: `normal`; `time forfeit`, süre dolması, berabere bittiğinde de; `abandoned`; `rules infraction`, ikinci bir kural dışı hamle ya da adil oyun ihlali nedeniyle hükmen yenilgi; `unterminated`, iptal edilen bir oyun), `PlyCount`, `ScacelithGameId` (onluk tabanda kimlik).

        Her hamle `{[%clk h:mm:ss.f] [%emt h:mm:ss.f]}` taşır: hamleyi yapan oyuncunun hamleden sonraki saati ve hamle için düşülen süre, saniyenin onda biri cinsinden (küsurat atılarak). Kayıtta bulunmayan bir değer yazılmaz. Son hamleden sonra bitiş nedeni sözcüklerle, ardından sonuç gelir.

        **Sınır:** `public_read` (token varsa oyuncu başına, yoksa istemci başına).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
      responses:
        '200':
          description: PGN dosyası.
          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: 'UTF-8 kodlamasında PGN metni (`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`: en fazla 16 haneli pozitif bir tam sayı değil (2^53''ün altında); `invalid_request`: geçerli URL kodlaması değil.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: bir token gönderildi ve geçerli değil.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: böyle bir oyun yok.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: `public_read` sınırı veya hesap kotası.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`internal_error`: diğer durumların yanı sıra, kayıtlı hamleler kayıtlı bitişe kadar yeniden oynatılamıyor (sunucu bunu günlüğe kaydeder).'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (veritabanı kilitli kaldı; gövdede `retryAfter: 1`, `Retry-After` başlığı yok), `server_busy` (oturum sorgulaması veritabanını kilitli buldu), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /games/{id}/gif:
    get:
      operationId: getGameGif
      tags:
        - gifs
      summary: Bu sunucudaki bir oyunu animasyonlu GIF olarak indir
      description: |-
        Oyun, animasyonlu GIF olarak. Adlar ve puanlar oyun kaydındakilerdir (başlangıçtaki puanlar, silinmiş bir hesap `deleted#<id>` olarak); sonuç ve bitiş şekli de oyun kaydından gelir (`Resignation`, `Loss on time`...).

        Denetimler şu sırayla yapılır: `gif_disabled`, resim seçenekleri (`size`, `orientation`, `delay`, `coords`), oyun kimliği, oyun, oyunun uzunluğu.

        **Oturum gerekli:** kotalar hesap başına sayılır. **Sınırlar:** her istekte `gif`; `gif_user_min`, `gif_user_hour`, `gif_ip_min` ve `gif_ip_hour` yalnızca GIF'in oluşturulması gerektiğinde (bkz. bölüm açıklaması). **Süre sınırı:** `GIF_QUEUE_TIMEOUT_MS` + `GIF_RENDER_TIMEOUT_MS` + 5 sn (varsayılan olarak 45 sn).
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/GameId'
        - name: size
          in: query
          required: false
          description: 'Resim boyutu: `small` (32 px kareler), `medium` (48 px) veya `large` (72 px).'
          schema:
            type: string
            enum:
              - small
              - medium
              - large
            default: medium
        - name: orientation
          in: query
          required: false
          description: Tahtanın altında yer alan taraf.
          schema:
            type: string
            enum:
              - white
              - black
            default: white
        - name: delay
          in: query
          required: false
          description: Hamle başına milisaniye (onluk tabanda 1 ila 6 hane).
          schema:
            type: integer
            minimum: 100
            maximum: 3000
            default: 500
        - name: coords
          in: query
          required: false
          description: Dikey harflerinin ve yatay numaralarının tahtanın çevresine çizilip (`1`) çizilmeyeceği (`0`).
          schema:
            type: string
            enum:
              - '1'
              - '0'
            default: '1'
      responses:
        '200':
          $ref: '#/components/responses/GifFile'
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_option`, `field` ile birlikte (`size`, `orientation`, `delay` veya `coords`): izin verilenlerin dışında bir değer. `invalid_game_id`. `invalid_request`: geçerli URL kodlaması değil.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: böyle bir oyun yok. `gif_disabled`: sunucu GIF''leri kapattı (`GIF_ENABLED=false`; `gif` jetonu iade edilir).'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '422':
          $ref: '#/components/responses/UnprocessableContent'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: `gif` sınırı, bir oluşturma sınırı veya hesap kotası.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`render_failed`: GIF oluşturulamadı (sunucu nedenini günlüğe kaydeder). `internal_error`.'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`, `retryAfter` (3 ila 10 sn) ve `Retry-After` ile: oluşturma kuyruğu dolu ya da GIF boş bir iş parçacığı için `GIF_QUEUE_TIMEOUT_MS` (10 sn) bekledi; isteğin aldığı tüm sınır jetonları iade edilir. `busy`: veritabanı kilitli kaldı (`retryAfter: 1` yalnızca gövdede). `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /gif:
    post:
      operationId: renderPgnGif
      tags:
        - gifs
      summary: PGN olarak gönderilen herhangi bir oyunun animasyonlu GIF'ini oluştur
      description: |-
        PGN metni olarak gönderilen herhangi bir oyun için `GET /games/{id}/gif` ile aynı resim: Scacelith oyununun kaydettiği bir oyun, başka bir sitenin dışa aktarması, elle yazılmış bir oyun. Metnin yalnızca ilk oyunu kullanılır.

        - PGN okuyucusu, oyunun kendi okuyucusunun kabul ettiği her şeyi kabul eder: bu sunucunun yazdığı her PGN ve diğer sitelerin olağan dışa aktarmaları (yorumlar, varyantlar, NAG'ler ve saat açıklamaları atlanır; hamle numaraları ve SAN esnek biçimde okunur; `SetUp` `"0"` olmadıkça bir `FEN` etiketi başlangıç pozisyonunu verir).
        - Adlar ve puanlar `White`, `Black`, `WhiteElo` ve `BlackElo` etiketlerinden gelir. Aksanlı harfler aksanlarını kaybeder, yazdırılabilir ASCII dışındaki diğer karakterler `?` olur ve uzun adlar kısaltılır (48 karakter). Sonuç `Result` etiketinden, o yoksa hamle metninin sonundan gelir. `Termination` etiketi `normal` değilse gösterilir: aksi hâlde ne olduğunu son pozisyon kendisi anlatır (mat, pat).
        - Bu uç nokta gövdesini kendisi denetler: bilinmeyen bir alan ya da eksik veya dize olmayan bir `pgn`, `field` alanını içeren 400 `invalid_request` ile yanıtlanır; yanlış bir seçenek 400 `invalid_option` ile yanıtlanır (`delayMs` bir JSON sayısı, `coords` bir boolean olmalıdır; `null` reddedilir).

        **Oturum gerekli:** kotalar hesap başına sayılır. **Sınırlar:** `GET /games/{id}/gif` ile aynı. **Gövde sınırı:** `HTTP_BODY_LIMIT` ne olursa olsun 135.168 bayt (JSON dizesi olarak PGN, kaçış karakterleri dahil). **Süre sınırı:** varsayılan olarak 45 sn.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GifRequest'
            examples:
              short:
                summary: Elle yazılmış kısa bir oyun, koordinatlar olmadan
                value:
                  pgn: 1. f3 e5 2. g4 Qh4# 0-1
                  coords: false
              options:
                summary: Tüm seçenekleri içeren bir PGN dosyası
                value:
                  pgn: |
                    [White "alice"]
                    [Black "bob"]
                    [WhiteElo "1500"]
                    [BlackElo "1520"]
                    [Result "1-0"]

                    1. e4 e5 2. Bc4 Nc6 3. Qh5 Nf6 4. Qxf7# 1-0
                  size: small
                  orientation: black
                  delayMs: 800
                  coords: true
      responses:
        '200':
          $ref: '#/components/responses/GifFile'
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_request`, `field` ile (bilinmeyen bir alan ya da eksik veya dize olmayan `pgn`; gövde bir nesne değilse `field` olmadan); `invalid_json`; `invalid_option`, `field` ile (`size`, `orientation`, `delayMs` veya `coords`); `invalid_pgn`, `line` ve `column` (1''den başlayarak, sütunlar karakter cinsinden) ve okuyucunun `message` alanıyla: kural dışı ya da belirsiz bir hamle, bozuk bir etiket, bilinmeyen bir satranç türü, 65.536 bayttan fazlası.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`gif_disabled`: sunucu GIF''leri kapattı (`GIF_ENABLED=false`; `gif` jetonu iade edilir).'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
          description: '`payload_too_large`: 135.168 baytı aşan bir gövde.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '422':
          $ref: '#/components/responses/UnprocessableContent'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: `gif` sınırı, bir oluşturma sınırı veya hesap kotası.'
        '500':
          $ref: '#/components/responses/InternalError'
          description: '`render_failed`: GIF oluşturulamadı (sunucu nedenini günlüğe kaydeder). `internal_error`.'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy`, `retryAfter` (3 ila 10 sn) ve `Retry-After` ile: oluşturma kuyruğu dolu ya da GIF boş bir iş parçacığı için çok uzun bekledi; isteğin aldığı tüm sınır jetonları iade edilir. `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /players/{username}:
    get:
      operationId: getPlayer
      tags:
        - players
      summary: Bir oyuncunun herkese açık profilini getir
      description: |-
        Bir oyuncunun herkese açık profili: resmî kategorilerdeki puanlar (sunucunun sırasıyla) ve oyun sayıları. `games.total` puansız ve iptal edilenler dahil kayıtlı tüm oyunları sayar; `games.rated`, `wins`, `draws` ve `losses` puan kayıtları üzerinden toplanır, yani yalnızca puanlı oyunları kapsar. Yanıt, token olsa da olmasa da aynıdır.

        **Sınır:** `public_read` (token varsa oyuncu başına, yoksa istemci başına).
      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`: `[A-Za-z0-9_.-]` kümesinden 2 ila 24 karakter değil.'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: bir token gönderildi ve geçerli değil.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: böyle bir oyuncu yok ya da hesap silinmiş.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: `public_read` sınırı veya hesap kotası.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (veritabanı kilitli kaldı; gövdede `retryAfter: 1`, `Retry-After` başlığı yok), `server_busy` (oturum sorgulaması veritabanını kilitli buldu), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /players/{username}/games:
    get:
      operationId: listPlayerGames
      tags:
        - players
      summary: Bir oyuncunun son oyunlarını listele
      description: |-
        Oyuncunun son oyunları, en yeni önce; geçmiş gibi sayfalanır, ancak filtre ve toplam olmadan. `color` bu oyuncunun tarafıdır. Sayfa her dolduğunda `next` son oyunun kimliğidir, bu nedenle sonraki sayfa boş olabilir. Önce oyuncu aranır: sorgu ne derse desin, bilinmeyen bir oyuncu için yanıt 404 olur.

        **Sınır:** `public_read` (token varsa oyuncu başına, yoksa istemci başına).
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Username'
        - $ref: '#/components/parameters/BeforeQuery'
        - $ref: '#/components/parameters/LimitQuery'
      responses:
        '200':
          description: Oyuncunun oyunlarının bir sayfası.
          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` bir oyun kimliği değil), `invalid_limit` (bu ikisi `field` olmadan).'
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: '`invalid_token`: bir token gönderildi ve geçerli değil.'
        '404':
          $ref: '#/components/responses/NotFound'
          description: '`not_found`: böyle bir oyuncu yok ya da hesap silinmiş.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: `public_read` sınırı veya hesap kotası.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (veritabanı kilitli kaldı; gövdede `retryAfter: 1`, `Retry-After` başlığı yok), `server_busy` (oturum sorgulaması veritabanını kilitli buldu), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /leaderboard:
    get:
      operationId: getLeaderboard
      tags:
        - leaderboard
      summary: Bir kategorinin en iyi oyuncularını getir
      description: |-
        Resmî bir kategoride en az `minGames` (`PROVISIONAL_GAMES`) sayılan oyunu olan en iyi 100 puan kaydı; silinmiş hesaplar ve hileciliği kesinleşmiş oyuncular hariç. Sunucu her tabloyu en fazla 10 saniyede bir yeniden hesaplar; `updatedAt` ne zaman hesaplandığını söyler. Bir kategori için aynı anda tek bir hesaplama çalışır: bu sırada diğer istekler önceki listeyi alır ya da henüz liste yoksa hesaplamayı bekler.

        **Sınırlar:** yalnızca adres başına katman.
      security: []
      parameters:
        - name: category
          in: query
          required: true
          description: Resmî bir kategori kimliği (`3+2` veya `3%2B2`).
          schema:
            type: string
            pattern: '^\s*[0-9]+[+ ][0-9]+\s*$'
          example: '3+2'
        - name: limit
          in: query
          required: false
          description: 'Kaç oyuncu: 1 ila 100. En fazla 3 haneli daha büyük bir sayı 100 sayılır; boş bir değer, parametre hiç verilmemiş gibi sayılır.'
          schema:
            type: integer
            minimum: 1
            maximum: 999
            default: 100
      responses:
        '200':
          description: Tablo.
          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` (eksik ya da resmî bir kategori değil), `invalid_limit`.'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`rate_limited`: adres başına katman.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`busy` (veritabanı kilitli kaldı; gövdede `retryAfter: 1`, `Retry-After` başlığı yok), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /reports:
    post:
      operationId: reportPlayer
      tags:
        - reports
      summary: Yakın tarihli bir oyundaki rakibi bildir
      description: |-
        Oyuncunun son 7 gün içinde biten kendi oyunlarından birindeki rakibi bildirir. Bir bildirim tek başına hiçbir zaman bir puanı, bir yaptırımı ya da bir dürüstlük düzeyini değiştirmez. Moderatörlerin gördüğü inceleme önceliğini yükseltir ve `abuse` dışında oyunun satranç motoru analizini ister.

        Aynı oyun için aynı rakibin yeniden bildirilmesi aynı yanıtı alır ve hiçbir şeyi değiştirmez; yanıt, bildirilen hesap hakkında hiçbir zaman bir şey söylemez. `GET /games/{id}`, oyunun oyuncularına bir bildirimin kabul edilip edilmeyeceğini önceden söyler (`reportable`).

        Bu uç nokta gövdesini kendisi, `gameId`, `reported`, `category`, `comment` sırasıyla denetler: bir hata, `field` olmadan 400 `invalid_request` ile yanıtlanır. Bilmediği alanlar yok sayılır.

        **Sınırlar:** `reports` (oyuncu başına), ardından oyuncu başına 24 saatte `REPORTS_PER_DAY` (5) bildirim (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: Bildirim alındı (ya da zaten yapılmıştı).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    const: received
              example:
                status: received
        '400':
          $ref: '#/components/responses/BadRequest'
          description: '`invalid_request` (`field` olmadan), `invalid_json`.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: '`report_not_allowed`: son 7 gün içinde biten bir oyunda bildirimi yapanın rakibi değil.'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          description: '`report_limit` (`retryAfter: 3600`, `Retry-After` başlığıyla): son 24 saatte `REPORTS_PER_DAY` bildirim. `rate_limited`: `reports` sınırı veya hesap kotası.'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: '`server_busy` (oturum sorgulaması veritabanını kilitli buldu), `timeout`. Ayrıca, sunucu çok geride kaldığında (bkz. giriş bölümü) `retryAfter` 2 ile 5 arasında olmak üzere `server_busy`: istek işlenmedi.'
  /verify-email:
    servers:
      - url: https://caissa.scacelith.com
        description: Resmî sunucu (sayfalar kökte, `/api/v1` dışında bulunur).
      - url: https://{host}:{port}
        description: Herhangi bir Scacelith sunucusu (sayfalar kökte, `/api/v1` dışında bulunur).
        variables:
          host:
            default: caissa.scacelith.com
            description: Sunucunun herkese açık ana bilgisayar adı (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Herkese açık API portu (`PUBLIC_API_PORT`, yoksa `API_PORT`).
    get:
      operationId: showVerifyEmailPage
      tags:
        - pages
      summary: E-posta onay sayfasını göster
      description: |-
        Bir e-posta onay bağlantısının sayfası (kayıt ya da yeniden gönderilen bir onay e-postası). Yalnızca bir "Confirm my e-mail address" (e-posta adresimi onayla) düğmesi gösterir, böylece bağlantıyı açan bir e-posta tarama yazılımı bağlantıyı kullanılmış hâle getirmez; düğme formu `POST /verify-email` adresine gönderir.

        **Sınır:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Onay düğmesiyle birlikte sayfa.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Bağlantı geçersiz ya da süresi dolmuş (bir HTML sayfası).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: '`page` sınırı (bir HTML sayfası) ya da adres başına katman (JSON `rate_limited`).'
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitVerifyEmailPage
      tags:
        - pages
      summary: E-posta adresini onayla
      description: |-
        Onay sayfasının formu. Adresi onaylar; yeni bir kayıtta hesabı o anda oluşturur ve oyuncu giriş yapabilir.

        **Sınır:** `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: Adres onaylandı (ve yeni bir kaydın hesabı oluşturuldu).
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Bağlantı geçersiz, kullanılmış ya da süresi dolmuş veya form geçersiz (bir HTML sayfası).
        '408':
          $ref: '#/components/responses/HtmlRequestTimeout'
        '409':
          $ref: '#/components/responses/HtmlConflict'
          description: Kullanıcı adını veya adresini bu arada başka bir hesabın aldığı yeni bir kaydın bağlantısı; hesap oluşturulmaz (bir HTML sayfası).
        '413':
          $ref: '#/components/responses/HtmlPayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/HtmlUnsupportedMediaType'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: '`auth` sınırı (bir HTML sayfası) ya da adres başına katman (JSON `rate_limited`).'
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Veritabanı kilitli kaldı (`Retry-After: 1`) ya da sunucu çok geride kaldı (`Retry-After` 2 ila 5, bkz. giriş bölümü): hiçbir şey değişmedi ve bağlantı hâlâ çalışır. İşleyici zaman aşımına uğradığında da.'
  /reset-password:
    servers:
      - url: https://caissa.scacelith.com
        description: Resmî sunucu (sayfalar kökte, `/api/v1` dışında bulunur).
      - url: https://{host}:{port}
        description: Herhangi bir Scacelith sunucusu (sayfalar kökte, `/api/v1` dışında bulunur).
        variables:
          host:
            default: caissa.scacelith.com
            description: Sunucunun herkese açık ana bilgisayar adı (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Herkese açık API portu (`PUBLIC_API_PORT`, yoksa `API_PORT`).
    get:
      operationId: showResetPasswordPage
      tags:
        - pages
      summary: Parola sıfırlama formunu göster
      description: |-
        Bir parola sıfırlama bağlantısının sayfası: yeni parola formu (parola iki kez). Form `POST /reset-password` adresine gönderilir.

        **Sınır:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Yeni parola formu.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Bağlantı geçersiz (hesabın artık sahip olmadığı bir adrese gönderilmişse de).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: '`page` sınırı (bir HTML sayfası) ya da adres başına katman (JSON `rate_limited`).'
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitResetPasswordPage
      tags:
        - pages
      summary: Sıfırlama formundan yeni parola belirle
      description: |-
        Sıfırlama sayfasının formu; `POST /auth/password/reset` ne yapıyorsa onu tek bir işlemde yapar: tüm cihazlarda çıkış yapılır, bekleyen bir e-posta değişikliği iptal edilir, diğer sıfırlama bağlantıları çalışmaz hâle gelir, adres onaylanmış sayılır ve hesap sahibine bir e-posta gönderilir.

        Önce bağlantı, ardından iki parolanın aynı olup olmadığı, sonra parola kuralları denetlenir. Sunucu meşgulken form `Retry-After` ile geri gelir ve bağlantı geçerli kalır.

        **Sınırlar:** `auth`, ardından `auth_reset` (`POST /auth/password/reset` ile paylaşılır).
      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: Parola değiştirildi ve tüm cihazlarda çıkış yapıldı.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Hatayla birlikte yeniden form (parolalar farklı, zayıf bir parola), bağlantı geçersiz ya da form geçersiz (bir HTML sayfası).
        '408':
          $ref: '#/components/responses/HtmlRequestTimeout'
        '413':
          $ref: '#/components/responses/HtmlPayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/HtmlUnsupportedMediaType'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: "`auth` ya da `auth_reset` sınırı (bir HTML sayfası), bu istemcinin bekleyen çok fazla parola hash'i olduğunda `Retry-After` ile yeniden form (bağlantı geçerli kalır) ya da adres başına katman (JSON `rate_limited`)."
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Sunucu meşgulken `Retry-After` ile yeniden form (parola hash kuyruğu ya da veritabanı kilitli kaldı); bağlantı geçerli kalır. Sunucu çok geride kaldığında (bkz. giriş bölümü) bunun yerine `Retry-After` (2 ila 5) ile bir "Server error" (sunucu hatası) sayfası: hiçbir şey işlenmedi ve bağlantı geçerli kalır. İşleyici zaman aşımına uğradığında da.'
  /confirm-email-change:
    servers:
      - url: https://caissa.scacelith.com
        description: Resmî sunucu (sayfalar kökte, `/api/v1` dışında bulunur).
      - url: https://{host}:{port}
        description: Herhangi bir Scacelith sunucusu (sayfalar kökte, `/api/v1` dışında bulunur).
        variables:
          host:
            default: caissa.scacelith.com
            description: Sunucunun herkese açık ana bilgisayar adı (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Herkese açık API portu (`PUBLIC_API_PORT`, yoksa `API_PORT`).
    get:
      operationId: showConfirmEmailChangePage
      tags:
        - pages
      summary: E-posta değişikliği onay sayfasını göster
      description: |-
        Bir e-posta değişikliği bağlantısının sayfası: yeni adresi ve hesabın adını, formu `POST /confirm-email-change` adresine gönderen bir "Use this e-mail address" (bu e-posta adresini kullan) düğmesiyle birlikte gösterir.

        **Sınır:** `page`.
      security: []
      parameters:
        - $ref: '#/components/parameters/LinkTokenQuery'
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
          description: Yeni adresi ve onay düğmesini içeren sayfa.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Bağlantı geçersiz ya da süresi dolmuş (istekten bu yana hesabın adresi değişmişse de).
        '414':
          $ref: '#/components/responses/UriTooLong'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: '`page` sınırı (bir HTML sayfası) ya da adres başına katman (JSON `rate_limited`).'
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
    post:
      operationId: submitConfirmEmailChangePage
      tags:
        - pages
      summary: Yeni e-posta adresini onayla
      description: |-
        E-posta değişikliği sayfasının formu. Adres değişir ve onaylanmış sayılır; cihazlarda giriş açık kalır; daha önce gönderilen bağlantılar (onay, parola sıfırlama, diğer değişiklikler) çalışmaz hâle gelir; eski adrese, yeni adres maskelenmiş olarak bilgi verilir.

        **Sınır:** `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: Adres değiştirildi.
        '400':
          $ref: '#/components/responses/HtmlBadRequest'
          description: Bağlantı geçersiz, kullanılmış ya da süresi dolmuş veya form geçersiz (bir HTML sayfası).
        '408':
          $ref: '#/components/responses/HtmlRequestTimeout'
        '409':
          $ref: '#/components/responses/HtmlConflict'
          description: Adresi bu arada başka bir hesap aldı (bir HTML sayfası).
        '413':
          $ref: '#/components/responses/HtmlPayloadTooLarge'
        '414':
          $ref: '#/components/responses/UriTooLong'
        '415':
          $ref: '#/components/responses/HtmlUnsupportedMediaType'
        '429':
          $ref: '#/components/responses/HtmlTooManyRequests'
          description: '`auth` sınırı (bir HTML sayfası) ya da adres başına katman (JSON `rate_limited`).'
        '500':
          $ref: '#/components/responses/HtmlInternalError'
        '503':
          $ref: '#/components/responses/HtmlServiceUnavailable'
          description: 'Veritabanı kilitli kaldı (`Retry-After: 1`) ya da sunucu çok geride kaldı (`Retry-After` 2 ila 5, bkz. giriş bölümü): hiçbir şey değişmedi ve bağlantı hâlâ çalışır. İşleyici zaman aşımına uğradığında da.'
  /healthz:
    servers:
      - url: https://caissa.scacelith.com
        description: Resmî sunucu, kökte.
      - url: https://caissa.scacelith.com/api/v1
        description: Resmî sunucu, `/api/v1` altında.
      - url: https://{host}:{port}
        description: Herhangi bir Scacelith sunucusu, kökte.
        variables:
          host:
            default: caissa.scacelith.com
            description: Sunucunun herkese açık ana bilgisayar adı (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Herkese açık API portu (`PUBLIC_API_PORT`, yoksa `API_PORT`).
      - url: https://{host}:{port}/api/v1
        description: Herhangi bir Scacelith sunucusu, `/api/v1` altında.
        variables:
          host:
            default: caissa.scacelith.com
            description: Sunucunun herkese açık ana bilgisayar adı (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Herkese açık API portu (`PUBLIC_API_PORT`, yoksa `API_PORT`).
    get:
      operationId: getLiveness
      tags:
        - health
      summary: Sürecin çalıştığını denetle
      description: |-
        Süreç çalıştığı sürece 200 `ok` yanıtını verir. `HEAD` de çalışır. Diğer tüm metotlar (`OPTIONS` dahil) `Allow: GET, HEAD` başlığıyla 405 `method_not_allowed` ile yanıtlanır.

        **Sınırlar:** yalnızca adres başına katman.
      security: []
      responses:
        '200':
          description: Süreç çalışıyor.
          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`: adres başına katman.'
  /readyz:
    servers:
      - url: https://caissa.scacelith.com
        description: Resmî sunucu, kökte.
      - url: https://caissa.scacelith.com/api/v1
        description: Resmî sunucu, `/api/v1` altında.
      - url: https://{host}:{port}
        description: Herhangi bir Scacelith sunucusu, kökte.
        variables:
          host:
            default: caissa.scacelith.com
            description: Sunucunun herkese açık ana bilgisayar adı (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Herkese açık API portu (`PUBLIC_API_PORT`, yoksa `API_PORT`).
      - url: https://{host}:{port}/api/v1
        description: Herhangi bir Scacelith sunucusu, `/api/v1` altında.
        variables:
          host:
            default: caissa.scacelith.com
            description: Sunucunun herkese açık ana bilgisayar adı (`SERVER_PUBLIC_HOST`).
          port:
            default: "443"
            description: Herkese açık API portu (`PUBLIC_API_PORT`, yoksa `API_PORT`).
    get:
      operationId: getReadiness
      tags:
        - health
      summary: Sunucunun oyuncu kabul ettiğini denetle
      description: |-
        Sunucu oyuncu kabul ettiğinde 200 `ready`, başlarken ya da dururken 503 `not_ready` yanıtını verir. `HEAD` de çalışır. Diğer tüm metotlar (`OPTIONS` dahil) `Allow: GET, HEAD` başlığıyla 405 `method_not_allowed` ile yanıtlanır.

        **Sınırlar:** yalnızca adres başına katman.
      security: []
      responses:
        '200':
          description: Sunucu oyuncu kabul ediyor.
          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`: adres başına katman.'
        '503':
          description: Sunucu başlıyor ya da duruyor.
          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: |-
        Bir giriş yanıtından alınan oturum token'ı (`sct_` ve ardından 43 base64url karakteri), `Authorization` başlığında: `Authorization: Bearer <token>`. Önek tam olarak `Bearer` (büyük/küçük harfe duyarlı) ve bir boşluktur; bu öneki taşımayan bir değer ya da 1 ila 512 yazdırılabilir ASCII karakterinden oluşmayan bir token, diğer tüm geçersiz token'lar gibi 401 `invalid_token` ile yanıtlanır. Boş bir başlık, başlık yokmuş gibi sayılır.
  parameters:
    GameId:
      name: id
      in: path
      required: true
      description: "Oyun kimliği: baştaki sıfırlar olmadan, en fazla 16 haneli, 2^53'ün altında, onluk tabanda pozitif bir tam sayı."
      schema:
        type: integer
        format: int64
        minimum: 1
        maximum: 9007199254740991
      example: 4100000000001
    SessionId:
      name: id
      in: path
      required: true
      description: Oturum kimliği, `GET /auth/sessions` tarafından verildiği gibi, baştaki sıfırlar olmadan yazılır (`03`, 3 numaralı oturum değildir).
      schema:
        type: integer
        format: int64
        minimum: 1
      example: 3
    Username:
      name: username
      in: path
      required: true
      description: Oyuncunun kullanıcı adı, büyük/küçük harf ayrımı gözetilmeden. Sunucu, `[A-Za-z0-9_.-]` kümesinden 2 ila 24 karakterlik her adı kabul eder (şimdiye kadar izin verdiği en geniş kural).
      schema:
        type: string
        pattern: '^[A-Za-z0-9_.-]{2,24}$'
      example: alice
    BeforeQuery:
      name: before
      in: query
      required: false
      description: Bir oyun kimliği; yalnızca daha eski oyunlar listelenir. Önceki sayfanın `next` değerini gönderin. Boş bir değer, parametre hiç verilmemiş gibi sayılır.
      schema:
        type: integer
        format: int64
        minimum: 1
        maximum: 9007199254740991
      example: 4100000000002
    LimitQuery:
      name: limit
      in: query
      required: false
      description: 'Sayfa boyutu: 1 ila 50. En fazla 3 haneli daha büyük bir sayı 50 sayılır; boş bir değer, parametre hiç verilmemiş gibi sayılır.'
      schema:
        type: integer
        minimum: 1
        maximum: 999
        default: 20
    LinkTokenQuery:
      name: token
      in: query
      required: false
      description: E-posta bağlantısının token'ı (43 base64url karakteri). Eksik ya da yanlış bir token, "link invalid or expired" (bağlantı geçersiz ya da süresi dolmuş) sayfasını gösterir.
      schema:
        type: string
        pattern: '^[A-Za-z0-9_-]{43}$'
      example: asfioO4P0r_Zn5DxWzcQuF6Drgxu7wXGjCyS-UFbTaw
  headers:
    CacheControl:
      description: Sunucunun her yanıtı önbelleğe almayı yasaklar.
      schema:
        type: string
        const: no-store
    RetryAfter:
      description: Yeniden denemeden önce beklenecek saniye; gövdedeki `retryAfter` ile aynı değer.
      schema:
        type: integer
        minimum: 1
      example: 30
    WwwAuthenticate:
      description: Bearer sınaması; geçersiz bir token için `error="invalid_token"` ile.
      schema:
        type: string
        enum:
          - Bearer realm="scacelith"
          - Bearer realm="scacelith", error="invalid_token"
    ConnectionClose:
      description: Sunucu bu yanıttan sonra bağlantıyı kapatır.
      schema:
        type: string
        const: close
  responses:
    BadRequest:
      description: '`invalid_request` (hata gövdedeki bir alandaysa `field` ile) veya `invalid_json`.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalidRequest:
              summary: Bir alan şemaya uymuyor
              value:
                error: invalid_request
                message: '"password" is required'
                field: password
            invalidJson:
              summary: Gövde JSON değil
              value:
                error: invalid_json
                message: The body is not valid JSON.
            weakPassword:
              summary: Zayıf bir parola
              value:
                error: weak_password
                message: The password must have at least 10 characters.
                reason: too_short
            invalidPgn:
              summary: Okunamayan bir PGN
              value:
                error: invalid_pgn
                message: illegal move 'Ke3'
                line: 1
                column: 13
    Unauthorized:
      description: '`unauthorized` (`Authorization` başlığı yok) veya `invalid_token` (token hatalı biçimli, süresi dolmuş, geçersiz kılınmış ya da silinmiş bir hesaba ait).'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        WWW-Authenticate:
          $ref: '#/components/headers/WwwAuthenticate'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            unauthorized:
              summary: Oturum yok
              value:
                error: unauthorized
                message: Log in first.
            invalidToken:
              summary: Geçersiz bir token
              value:
                error: invalid_token
                message: The session is invalid or has expired; log in again.
    Forbidden:
      description: İstek reddedildi.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalidPassword:
              summary: Yeniden kimlik doğrulamada yanlış parola
              value:
                error: invalid_password
                message: Wrong password.
            banned:
              summary: Askıya alınmış bir hesap
              value:
                error: banned
                message: This account is banned.
                until: 1791487639708
    NotFound:
      description: '`not_found` ya da bu sunucunun kapattığı bir özellik.'
      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`: gövde 10 sn içinde ulaşmadı. Sunucu bağlantıyı kapatır.'
      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: İstek, sunucunun durumuyla çakışıyor.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: username_taken
            message: This username is already taken.
    Gone:
      description: '`sso_expired`: Google ile giriş denemesi ya da bileti bilinmiyor, kullanılmış ya da süresi dolmuş; girişi yeniden başlatın.'
      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`: gövde `HTTP_BODY_LIMIT` değerini aşıyor (varsayılan olarak 16.384 bayt). Sunucu bağlantıyı kapatır.'
      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`: istek hedefi 4096 karakteri aşıyor.'
      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`: gövde `application/json` değil ya da karakter kümesi UTF-8 değil.'
      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`: oyunda `GIF_MAX_PLIES` (600) yarım hamleden fazlası var.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: game_too_long
            message: The game is too long for a GIF (742 plies, at most 600).
    PowRequired:
      description: '`pow_required`: `pow` sınamasını çözün ve aynı isteği `pow: { challenge, nonce }` ile yeniden gönderin. `reason` nedenini söyler: `required`, `malformed`, `signature`, `endpoint`, `network`, `expired`, `bits`, `work` veya `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` (bir hız sınırı, hesap kotası ya da parola hash kuyruğu) veya `too_many_attempts` (hesaba uygulanan bir yavaşlatma); `retryAfter` ve bir `Retry-After` başlığıyla.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            rateLimited:
              summary: Bir hız sınırı
              value:
                error: rate_limited
                message: Too many requests; try again later.
                retryAfter: 30
            tooManyAttempts:
              summary: Bu hesapta çok fazla başarısız deneme
              value:
                error: too_many_attempts
                message: Too many attempts; wait before trying again.
                retryAfter: 4
    InternalError:
      description: '`internal_error`: beklenmeyen bir hata. Sunucu bunu günlüğe kaydeder.'
      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`: yanlış bir `state` ya da `iss` veya Google kodu reddetti ya da doğrulanamayan bir ID token''ı gönderdi (diğer istemcinin, yani oyunun ya da web sitesinin bir kodu veya ID token''ı dahil). Google''ın metnini hiçbir zaman içermez.'
      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` veya `timeout`: `retryAfter` verilmişse o süre geçtikten sonra yeniden deneyin.'
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            serverBusy:
              summary: Sunucu meşgul
              value:
                error: server_busy
                message: The server is busy; try again in a few seconds.
                retryAfter: 9
            busy:
              summary: Veritabanı kilitli kaldı (bir okuma)
              value:
                error: busy
                message: Try again shortly.
                retryAfter: 1
            timeout:
              summary: Sunucunun yanıtı çok uzun sürdü
              value:
                error: timeout
                message: The server took too long to answer; try again.
    GifFile:
      description: Animasyonlu GIF, ek olarak.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Content-Disposition:
          description: 'Bu sunucudaki bir oyun için `attachment; filename="scacelith-<id>.gif"`, `POST /gif` ile gönderilen bir PGN için `attachment; filename="scacelith-game.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: Dosyanın bayt cinsinden boyutu.
          schema:
            type: integer
            minimum: 1
      content:
        image/gif:
          schema:
            type: string
            format: binary
    HtmlPage:
      description: Bir HTML sayfası.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlBadRequest:
      description: Reddin nedenini açıklayan bir HTML sayfası.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlRequestTimeout:
      description: Form 10 sn içinde ulaşmadı (bir HTML sayfası). Sunucu bağlantıyı kapatır.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Connection:
          $ref: '#/components/headers/ConnectionClose'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlConflict:
      description: Çakışmayı açıklayan bir HTML sayfası.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlPayloadTooLarge:
      description: Form `HTTP_BODY_LIMIT` değerini aşıyor (bir HTML sayfası). Sunucu bağlantıyı kapatır.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Connection:
          $ref: '#/components/headers/ConnectionClose'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlUnsupportedMediaType:
      description: Gövde ne bir form (`application/x-www-form-urlencoded`) ne de JSON; ya da karakter kümesi UTF-8 değil (bir HTML sayfası).
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlTooManyRequests:
      description: Bir hız sınırı (bir HTML sayfası), `Retry-After` ile.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlInternalError:
      description: Beklenmeyen bir hata ("Server error" (sunucu hatası) başlıklı bir HTML sayfası). Sunucu bunu günlüğe kaydeder.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
    HtmlServiceUnavailable:
      description: Sunucu meşgul (veritabanı kilitli kaldı ya da sunucu çok geride kaldı; `Retry-After` ile) ya da yanıtı çok uzun sürdü ("Server error" (sunucu hatası) başlıklı bir HTML sayfası).
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        text/html:
          schema:
            $ref: '#/components/schemas/HtmlDocument'
  schemas:
    Error:
      type: object
      description: JSON API'nin tüm hata yanıtlarının tek biçimi.
      required:
        - error
        - message
      properties:
        error:
          type: string
          pattern: '^[a-z][a-z0-9_]*$'
          description: '`snake_case` biçimindeki hata kodu. İstemci ne göstereceğini buna göre seçer.'
          examples:
            - rate_limited
        message:
          type: string
          description: Günlükler için ve yedek olarak tasarlanmış İngilizce bir cümle.
        retryAfter:
          type: integer
          minimum: 1
          description: Yeniden denemeden önce beklenecek saniye; yalnızca zamanla sona eren retlerde bulunur. Bu durumda yanıt, aynı değere sahip bir `Retry-After` başlığı da taşır; geçmiş, oyun, oyuncu ve sıralama tablosu okumalarının 503 `busy` yanıtı bunun dışındadır.
        field:
          type: string
          description: Hatalı alan ya da sorgu parametresi (`invalid_request`, `invalid_option` ve `GET /account/games` uç noktasının `invalid_cursor`, `invalid_limit` ve `invalid_filter` hataları). İç içe bir alan noktalı yazılır (`pow.nonce`).
        reason:
          type: string
          description: 'Neden: bir `weak_password` hatasının ihlal ettiği kural ya da bir iş kanıtının neden istendiği veya reddedildiği (`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: 'Yalnızca `banned`: askıya almanın bitişi (epoch ms), kalıcı askıya almada `null`.'
        line:
          type: integer
          minimum: 1
          description: 'Yalnızca `invalid_pgn`: hatanın satırı, 1''den başlayarak.'
        column:
          type: integer
          minimum: 1
          description: 'Yalnızca `invalid_pgn`: hatanın sütunu, 1''den başlayarak, karakter cinsinden.'
    PowChallenge:
      type: object
      description: Bir iş kanıtı sınaması (428 `pow_required` yanıtındaki `pow`).
      required:
        - challenge
        - bits
        - expiresAt
      properties:
        challenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{16,400}\.[A-Za-z0-9_-]{43}$'
          description: İmzalı sınama; olduğu gibi geri gönderilmelidir.
        bits:
          type: integer
          minimum: 1
          maximum: 26
          description: '`SHA-256(challenge + ":" + nonce)` değerinin başında bulunması gereken sıfır bit sayısı.'
        expiresAt:
          type: integer
          format: int64
          description: 'Sınamanın süresinin dolduğu an (epoch ms): verilmesinden 2 dakika sonra.'
    PowAnswer:
      type: object
      description: Bir iş kanıtı sınamasının yanıtı.
      additionalProperties: false
      required:
        - challenge
        - nonce
      properties:
        challenge:
          type: string
          minLength: 16
          maxLength: 512
          description: 428 yanıtındaki `challenge`, verildiği gibi.
        nonce:
          type: string
          pattern: '^[0-9]{1,20}$'
          description: '`SHA-256(challenge + ":" + nonce)` değeri `bits` adet sıfır bitle başlayacak şekilde, en fazla 20 haneli, onluk tabanda bir sayı dizesi.'
    ServerInfo:
      type: object
      description: Bir istemcinin giriş yapmadan ya da bağlanmadan önce ihtiyaç duyduğu bilgiler.
      required:
        - name
        - serverId
        - motd
        - protocol
        - wsPort
        - wsPath
        - registration
        - emailVerification
        - sso
        - mfa
        - pow
        - categories
        - limits
      properties:
        name:
          type: string
          description: Sunucunun adı (`SERVER_NAME`).
        serverId:
          type:
            - string
            - 'null'
          format: uuid
          description: Veritabanının ilk başlatılışında aldığı bir UUID. Yeniden başlatmalarda aynı kalır. Veritabanı onu veremediğinde `null` olur.
        motd:
          type: string
          description: Günün mesajı (`SERVER_MOTD`); boş olabilir.
        protocol:
          type: object
          description: WebSocket protokolü (`PROTOCOL.md`).
          required:
            - min
            - max
            - schema
            - subprotocol
          properties:
            min:
              type: integer
              minimum: 1
              description: Sunucunun desteklediği en eski protokol sürümü.
            max:
              type: integer
              minimum: 1
              description: Sunucunun desteklediği en yeni protokol sürümü.
            schema:
              type: integer
              format: int64
              minimum: 0
              maximum: 4294967295
              description: 'Protokol şemasının parmak izi (kanonik biçiminin SHA-256 değerinin ilk 4 baytı, işaretsiz bir tam sayı olarak: protokol alt sürümü 2 için `0xc649140f`, 3326678031); bilgi amaçlıdır, hiçbir zaman karşılaştırılmaz.'
            subprotocol:
              type: string
              description: WebSocket alt protokol tanımlayıcısı (`Sec-WebSocket-Protocol`).
        wsPort:
          type: integer
          minimum: 0
          maximum: 65535
          description: Oyuncuların kullandığı WebSocket portu (`PUBLIC_WS_PORT`, yoksa `WS_PORT`, o da yoksa `API_PORT`).
        wsPath:
          type: string
          const: /ws
          description: WebSocket'in yolu.
        registration:
          type: string
          enum:
            - open
            - closed
          description: Yeni hesap oluşturulup oluşturulamayacağı.
        emailVerification:
          type: boolean
          description: Yeni hesapların adreslerini onaylayıp onaylamayacağı (`REQUIRE_EMAIL_VERIFICATION`).
        sso:
          type: object
          required:
            - google
            - googleWeb
          properties:
            google:
              type: boolean
              description: Oyuna Google ile giriş sunulup sunulmadığı.
            googleWeb:
              type: boolean
              description: Google ile girişin bir web sitesine de sunulup sunulmadığı (`POST /auth/sso/google/web/start`).
        mfa:
          type: boolean
          const: true
          description: İki faktörlü kimlik doğrulama her zaman kullanılabilir.
        pow:
          type: object
          required:
            - register
          properties:
            register:
              type: integer
              minimum: 0
              maximum: 26
              description: Kaydın gerektirdiği iş kanıtı bit sayısı (gerekmiyorsa 0).
        categories:
          type: array
          description: Resmî (puanlı) süre kontrolleri (`RATED_CATEGORIES`), sunucunun sırasıyla. Diğer tüm süre kontrolleri `custom` olur.
          items:
            $ref: '#/components/schemas/Category'
        limits:
          type: object
          description: Bir istemcinin form göndermeden önce denetleyebileceği kurallar.
          required:
            - usernameMin
            - usernameMax
            - usernamePattern
            - passwordMinLength
            - passwordMaxBytes
            - customTimeControls
            - reportsPerDay
            - wsMaxMessageBytes
          properties:
            usernameMin:
              type: integer
              description: En kısa kullanıcı adı uzunluğu (`USERNAME_MIN`).
            usernameMax:
              type: integer
              description: En uzun kullanıcı adı uzunluğu (`USERNAME_MAX`).
            usernamePattern:
              type: string
              description: Yeni bir kullanıcı adının uyması gereken düzenli ifade.
            passwordMinLength:
              type: integer
              description: En kısa parola uzunluğu, karakter cinsinden (`PASSWORD_MIN_LENGTH`).
            passwordMaxBytes:
              type: integer
              description: En uzun parola uzunluğu, UTF-8 bayt cinsinden.
            customTimeControls:
              type: boolean
              description: Meydan okumaların ve özel oyunların özel süre kontrolleri kullanıp kullanamayacağı.
            reportsPerDay:
              type: integer
              description: Bir oyuncunun 24 saatte en fazla kaç bildirim yapabileceği (`REPORTS_PER_DAY`).
            wsMaxMessageBytes:
              type: integer
              description: Bir istemcinin gönderebileceği en büyük WebSocket mesajının bayt cinsinden boyutu.
      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: Resmî bir süre kontrolü.
      required:
        - id
        - baseSec
        - incSec
      properties:
        id:
          type: string
          pattern: '^[0-9]+\+[0-9]+$'
          description: 'Kategori kimliği: dakika ve saniye cinsinden hamle başına ek süre (`3+2`).'
        baseSec:
          type: number
          minimum: 0
          description: Temel süre, saniye cinsinden.
        incSec:
          type: number
          minimum: 0
          description: Hamle başına ek süre, saniye cinsinden.
    AccountView:
      type: object
      description: Hesap, oyuncusunun gördüğü hâliyle (giriş yanıtlarındaki ve `GET /account/me` yanıtındaki `user`).
      required:
        - id
        - username
        - email
        - emailVerified
        - mfaEnabled
        - googleLinked
        - hasPassword
        - acceptChallenges
        - createdAt
        - lastLoginAt
        - pendingEmail
      properties:
        id:
          type: integer
          format: int64
          description: Hesap kimliği.
        username:
          type: string
          description: Kullanıcı adı.
        email:
          type: string
          format: email
          description: E-posta adresi.
        emailVerified:
          type: boolean
          description: Adresin onaylanıp onaylanmadığı.
        mfaEnabled:
          type: boolean
          description: İki faktörlü kimlik doğrulamanın açık olup olmadığı.
        googleLinked:
          type: boolean
          description: Bir Google hesabının bağlı olup olmadığı.
        hasPassword:
          type: boolean
          description: 'Google üzerinden oluşturulmuş ve henüz parola belirlememiş bir hesap için `false`.'
        acceptChallenges:
          type: string
          enum:
            - all
            - none
          description: Ada göre doğrudan meydan okumaların kabul edilip edilmediği (bkz. `PUT /account/preferences`).
        createdAt:
          type: integer
          format: int64
          description: Hesabın oluşturulduğu an (epoch ms).
        lastLoginAt:
          type:
            - integer
            - 'null'
          format: int64
          description: Son giriş (epoch ms) ya da `null`.
        pendingEmail:
          type:
            - string
            - 'null'
          format: email
          description: Bağlantısının kullanılmasını bekleyen bir e-posta değişikliğinin yeni adresi ya da `null`.
    SessionAnswer:
      type: object
      description: Yeni bir oturum.
      required:
        - token
        - expiresAt
        - user
      properties:
        token:
          type: string
          pattern: '^sct_[A-Za-z0-9_-]{43}$'
          description: Oturum token'ı; `Authorization` başlığı ve WebSocket için.
        expiresAt:
          type: integer
          format: int64
          description: Oturumun mutlak bitişi (epoch ms); boşta kalma sınırı oturumu daha erken sonlandırabilir.
        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: İki faktörlü kimlik doğrulamalı bir girişin ikinci adımı; `POST /auth/login/mfa` ile devam edin.
      required:
        - mfaRequired
        - mfaToken
        - expiresIn
      properties:
        mfaRequired:
          type: boolean
          const: true
        mfaToken:
          type: string
          pattern: '^mfa_[A-Za-z0-9_-]{43}$'
          description: Adımın token'ı; `POST /auth/login/mfa` için.
        expiresIn:
          type: integer
          const: 300
          description: Adımı tamamlamak için kalan saniye.
    LoginAnswer:
      description: Bir oturum ya da iki faktörlü kimlik doğrulamalı bir girişin ikinci adımı.
      oneOf:
        - $ref: '#/components/schemas/SessionAnswer'
        - $ref: '#/components/schemas/MfaChallenge'
    SsoNeedsUsername:
      type: object
      description: İlk kez Google ile giriş; `POST /auth/sso/complete` ile devam edin.
      required:
        - needsUsername
        - ssoTicket
        - suggestedUsername
      properties:
        needsUsername:
          type: boolean
          const: true
        ssoTicket:
          type: string
          pattern: '^sso_[A-Za-z0-9_-]{43}$'
          description: '`POST /auth/sso/complete` için bilet; 10 dakika geçerlidir.'
        suggestedUsername:
          type: string
          description: Google adından ya da adresten türetilmiş bir kullanıcı adı veya hiçbiri uymazsa `""`.
    SsoNeedsPassword:
      type: object
      description: Adresi parolası olan bir hesap kullanıyor; `POST /auth/sso/google/link` ile devam edin. Henüz hiçbir şey bağlanmadı.
      required:
        - needsPassword
        - linkTicket
        - username
        - expiresIn
      properties:
        needsPassword:
          type: boolean
          const: true
        linkTicket:
          type: string
          pattern: '^sso_[A-Za-z0-9_-]{43}$'
          description: '`POST /auth/sso/google/link` için bilet.'
        username:
          type: string
          description: O hesabın kullanıcı adı (yalnızca adresin kendisine ait olduğunu Google'a kanıtlamış biri görür).
        expiresIn:
          type: integer
          const: 600
          description: Biletin geçerli kaldığı saniye.
    SsoFinishAnswer:
      description: '`POST /auth/sso/google/finish` yanıtı.'
      oneOf:
        - $ref: '#/components/schemas/SessionAnswer'
        - $ref: '#/components/schemas/MfaChallenge'
        - $ref: '#/components/schemas/SsoNeedsUsername'
        - $ref: '#/components/schemas/SsoNeedsPassword'
    SsoStartAnswer:
      type: object
      description: Bir Google ile giriş denemesi.
      required:
        - attemptId
        - authUrl
        - state
        - expiresIn
      properties:
        attemptId:
          type: string
          pattern: '^sso_[A-Za-z0-9_-]{43}$'
          description: Deneme; `POST /auth/sso/google/finish` için.
        authUrl:
          type: string
          format: uri
          description: Denetlendikten sonra sistem tarayıcısında açılacak Google URL'si.
        state:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: Google'ın geri göndermesi gereken `state`.
        expiresIn:
          type: integer
          const: 600
          description: Denemenin geçerli kaldığı saniye.
    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: Kullanıcı adı; ardından sunucunun kuralları uygulanır (bkz. açıklama).
        email:
          type: string
          minLength: 1
          maxLength: 254
          description: E-posta adresi. Baştaki ve sondaki boşluklar kırpılır ve küçük harfle kaydedilir.
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Parola; ardından parola kuralları uygulanır.
        pow:
          $ref: '#/components/schemas/PowAnswer'
    LoginRequest:
      type: object
      additionalProperties: false
      required:
        - login
        - password
      properties:
        login:
          type: string
          minLength: 1
          maxLength: 254
          description: Kullanıcı adı ya da e-posta adresi (`@` içeren her metin).
        password:
          type: string
          minLength: 1
          maxLength: 1024
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
        pow:
          $ref: '#/components/schemas/PowAnswer'
    ClientLabel:
      type: string
      maxLength: 64
      description: İsteğe bağlı. Giriş yapılmış cihazlar listesinde gösterilir (örneğin `Scacelith 1.4 (Windows)`).
    MfaLoginRequest:
      type: object
      additionalProperties: false
      required:
        - mfaToken
      properties:
        mfaToken:
          type: string
          minLength: 1
          maxLength: 64
          description: Giriş yanıtındaki (ya da bir Google ile giriş yanıtındaki) `mfaToken`.
        code:
          type: string
          maxLength: 32
          description: İsteğe bağlı. 6 haneli bir kimlik doğrulama uygulaması kodu ya da bir kurtarma kodu.
        recoveryCode:
          type: string
          maxLength: 32
          description: İsteğe bağlı. Bir kurtarma kodu. `code` ve `recoveryCode` alanlarından biri gereklidir.
    EmailRequest:
      type: object
      additionalProperties: false
      required:
        - email
      properties:
        email:
          type: string
          minLength: 1
          maxLength: 254
          description: E-posta adresi.
    PasswordResetRequest:
      type: object
      additionalProperties: false
      required:
        - token
        - newPassword
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: Sıfırlama bağlantısının `token` parametresi.
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Yeni parola; kayıttaki parola kuralları geçerlidir.
    SsoStartRequest:
      type: object
      additionalProperties: false
      required:
        - codeChallenge
        - redirectPort
      properties:
        codeChallenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: İstemcinin `codeVerifier` değerinin S256 PKCE sınaması (`BASE64URL(SHA-256(codeVerifier))`).
        redirectPort:
          type: integer
          minimum: 1024
          maximum: 65535
          description: İstemcinin `127.0.0.1` üzerindeki dinleyicisinin portu.
    SsoWebStartRequest:
      type: object
      additionalProperties: false
      required:
        - codeChallenge
      properties:
        codeChallenge:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: Sayfanın `codeVerifier` değerinin S256 PKCE sınaması (`BASE64URL(SHA-256(codeVerifier))`).
    SsoFinishRequest:
      type: object
      additionalProperties: false
      required:
        - attemptId
        - codeVerifier
        - state
        - code
      properties:
        attemptId:
          type: string
          minLength: 1
          maxLength: 64
          description: '`POST /auth/sso/google/start` yanıtındaki `attemptId`.'
        codeVerifier:
          type: string
          pattern: '^[A-Za-z0-9._~-]{43,128}$'
          description: PKCE doğrulayıcısı; SHA-256 değeri `start` adımına verilen sınamayla eşleşmelidir.
        state:
          type: string
          pattern: '^[A-Za-z0-9_-]{43}$'
          description: Google'ın geri gönderdiği hâliyle `state`.
        code:
          type: string
          minLength: 1
          maxLength: 2048
          pattern: '^[\x21-\x7E]+$'
          description: Google'ın geri gönderdiği hâliyle `code` (boşluksuz yazdırılabilir ASCII).
        iss:
          type: string
          minLength: 1
          maxLength: 256
          description: İsteğe bağlı. Google gönderdiyse, geri gönderdiği hâliyle `iss`.
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
    SsoLinkRequest:
      type: object
      additionalProperties: false
      required:
        - linkTicket
        - password
      properties:
        linkTicket:
          type: string
          minLength: 1
          maxLength: 64
          description: '`finish` yanıtındaki `linkTicket`; 10 dakika geçerlidir.'
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Hesabın parolası.
        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: '`finish` yanıtındaki `ssoTicket`; 10 dakika geçerlidir.'
        username:
          type: string
          minLength: 1
          maxLength: 64
          description: Kullanıcı adı; kayıt kuralları geçerlidir.
        clientLabel:
          $ref: '#/components/schemas/ClientLabel'
    SessionList:
      type: object
      required:
        - sessions
      properties:
        sessions:
          type: array
          description: Etkin oturumlar, en son kullanılan önce.
          items:
            $ref: '#/components/schemas/SessionEntry'
    SessionEntry:
      type: object
      description: Etkin bir oturum (giriş yapılmış bir cihaz).
      required:
        - id
        - createdAt
        - lastSeenAt
        - expiresAt
        - clientLabel
        - current
      properties:
        id:
          type: integer
          format: int64
          description: Oturum kimliği; `DELETE /auth/sessions/{id}` için.
        createdAt:
          type: integer
          format: int64
          description: Giriş anı (epoch ms).
        lastSeenAt:
          type: integer
          format: int64
          description: Son kullanım (epoch ms); en fazla 5 dakikada bir güncellenir (sunucu çok geride kaldığı sürece daha geç).
        expiresAt:
          type: integer
          format: int64
          description: Mutlak bitiş (epoch ms); boşta kalma sınırı oturumu daha erken sonlandırabilir.
        clientLabel:
          type:
            - string
            - 'null'
          description: Girişteki `clientLabel` ya da hiç gönderilmediyse `null`.
        current:
          type: boolean
          description: Bunun, isteği yapan oturum olup olmadığı.
    AccountMe:
      type: object
      required:
        - user
        - ratings
        - sanctions
        - ban
      properties:
        user:
          $ref: '#/components/schemas/AccountView'
        ratings:
          type: array
          description: Oyuncunun puanlı oyun oynadığı her kategori için bir kayıt.
          items:
            $ref: '#/components/schemas/RatingSummary'
        sanctions:
          type: array
          description: Etkin yaptırımlar.
          items:
            $ref: '#/components/schemas/ActiveSanction'
        ban:
          description: 'Askıya alınmışken `{ until }`, aksi hâlde `null`.'
          oneOf:
            - $ref: '#/components/schemas/Ban'
            - type: 'null'
    RatingSummary:
      type: object
      description: Bir kategorinin puan kaydı.
      required:
        - category
        - rating
        - games
        - wins
        - draws
        - losses
        - peak
        - provisional
      properties:
        category:
          type: string
          description: Kategori kimliği.
        rating:
          type: integer
          description: Puan.
        games:
          type: integer
          minimum: 0
          description: Kategoride oynanan oyunlar (sayılsın ya da sayılmasın).
        wins:
          type: integer
          minimum: 0
        draws:
          type: integer
          minimum: 0
        losses:
          type: integer
          minimum: 0
        peak:
          type: integer
          description: Ulaşılan en yüksek puan.
        provisional:
          type: boolean
          description: 'Puan henüz belirlenmemişken ya da sayılan oyun sayısı `PROVISIONAL_GAMES` değerinden azken `true` (oyun bunu `1510?` olarak gösterir).'
    ActiveSanction:
      type: object
      required:
        - kind
        - reason
        - startsAt
        - endsAt
      properties:
        kind:
          type: string
          enum:
            - ban
            - mm_block
            - warning
          description: Yaptırımın türü.
        reason:
          type:
            - string
            - 'null'
          description: Varsa belirtilen neden.
        startsAt:
          type: integer
          format: int64
          description: Başlangıç (epoch ms).
        endsAt:
          type:
            - integer
            - 'null'
          format: int64
          description: Bitiş (epoch ms); yaptırım kalıcıysa `null`.
    Ban:
      type: object
      required:
        - until
      properties:
        until:
          type:
            - integer
            - 'null'
          format: int64
          description: Askıya almanın bitişi (epoch ms); kalıcıysa `null`.
    Preferences:
      type: object
      additionalProperties: false
      required:
        - acceptChallenges
      properties:
        acceptChallenges:
          type: string
          enum:
            - all
            - none
          description: 'Ada göre doğrudan meydan okumaları kabul etmek için `all`, reddetmek için `none`.'
    PasswordChangeRequest:
      type: object
      additionalProperties: false
      required:
        - currentPassword
        - newPassword
      properties:
        currentPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Mevcut parola.
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Yeni parola; kayıttaki parola kuralları geçerlidir.
    PasswordOnlyRequest:
      type: object
      additionalProperties: false
      required:
        - password
      properties:
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Hesabın parolası.
    TotpEnableRequest:
      type: object
      additionalProperties: false
      required:
        - code
      properties:
        code:
          type: string
          pattern: '^[0-9]{6}$'
          description: Bekleyen gizli anahtarın bir kodu, tam 6 hane.
    ReauthCredentials:
      type: object
      additionalProperties: false
      required:
        - password
      properties:
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Hesabın parolası.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: İsteğe bağlı; iki faktörlü kimlik doğrulama açıksa gereklidir (ya da `recoveryCode`). Bir kimlik doğrulama uygulaması kodu ya da bir kurtarma kodu.
        recoveryCode:
          type: string
          minLength: 1
          maxLength: 32
          description: İsteğe bağlı. Bir kurtarma kodu (`xxxx-xxxx-xx`; büyük/küçük harf, boşluklar ve tireler önemli değildir).
    RecoveryCodesRequest:
      type: object
      additionalProperties: false
      required:
        - password
        - code
      properties:
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Hesabın parolası.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: Bir kimlik doğrulama uygulaması kodu (kurtarma kodu reddedilir).
    EmailChangeRequest:
      type: object
      additionalProperties: false
      required:
        - newEmail
        - password
      properties:
        newEmail:
          type: string
          minLength: 1
          maxLength: 254
          description: Yeni adres; baştaki ve sondaki boşluklar kırpılır ve küçük harfe çevrilir, ardından bir kayıt adresi gibi denetlenir.
        password:
          type: string
          minLength: 1
          maxLength: 1024
          description: Hesabın parolası.
        code:
          type: string
          minLength: 1
          maxLength: 32
          description: İsteğe bağlı; iki faktörlü kimlik doğrulama açıksa gereklidir (ya da `recoveryCode`). Bir kimlik doğrulama uygulaması kodu ya da bir kurtarma kodu.
        recoveryCode:
          type: string
          minLength: 1
          maxLength: 32
          description: İsteğe bağlı. Bir kurtarma kodu.
    TotpSetup:
      type: object
      description: Kimlik doğrulama uygulaması için bekleyen gizli anahtar.
      required:
        - secret
        - uri
        - algorithm
        - digits
        - period
      properties:
        secret:
          type: string
          pattern: '^[A-Z2-7]+$'
          description: Uygulamaya yazılacak, base32 biçiminde gizli anahtar.
        uri:
          type: string
          format: uri
          description: QR kodu olarak gösterilecek `otpauth://totp/...` URI'si.
        algorithm:
          type: string
          const: SHA1
        digits:
          type: integer
          const: 6
        period:
          type: integer
          const: 30
          description: Adım başına saniye.
    RecoveryCodeList:
      type: array
      description: Yalnızca bu kez gösterilen 10 kurtarma kodu. Her biri bir kez çalışır.
      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: Bir oyunun bir tarafı.
      required:
        - name
        - rating
        - ratingAfter
        - ratingDiff
      properties:
        name:
          type: string
          description: Oyun kaydındaki ad (silinmiş bir hesap için `deleted#<id>`).
        rating:
          type:
            - integer
            - 'null'
          description: Başlangıçtaki puan; bilinmiyorsa `null`.
        ratingAfter:
          type:
            - integer
            - 'null'
          description: Oyundan sonraki puan. Puanlı bir oyunda her zaman bulunur; yalnızca puanlara sayılmayan bir oyun (puansız, özel süre kontrollü, iptal edilen) için `null`.
        ratingDiff:
          type:
            - integer
            - 'null'
          description: Puan değişimi; puan kuralları puanı olduğu yerde bıraktığında `0` (FIDE'nin sıfır skor kuralı, puanı henüz belirlenmemiş bir rakip, puanı henüz belirlenmemiş bir oyuncunun ilk oyunları); `ratingAfter` ile aynı durumlarda `null`.
    GameSummary:
      type: object
      description: Kaydedilmiş bir oyunun özeti.
      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: Oyun kimliği.
        category:
          type: string
          description: Resmî kategori kimliği (`3+2`) ya da `custom`.
        rated:
          type: boolean
          description: Oyunun puanlara sayılıp sayılmadığı.
        timeControl:
          type: string
          pattern: '^[0-9]+\+[0-9]+$'
          description: Saniye cinsinden süre kontrolü (`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` (bkz. oyun kodları).'
        reason:
          type: integer
          minimum: 0
          maximum: 255
          description: Bitiş nedeni kodu (bkz. oyun kodları).
        result:
          type: string
          enum:
            - 1-0
            - 0-1
            - 1/2-1/2
            - '*'
          description: Sonuç; iptal edilen bir oyun için `*`.
        termination:
          $ref: '#/components/schemas/Termination'
        plies:
          type: integer
          minimum: 0
          description: Yarım hamle sayısı.
        startedAt:
          type: integer
          format: int64
          description: Başlangıç (epoch ms).
        endedAt:
          type: integer
          format: int64
          description: Bitiş (epoch ms).
    Termination:
      type: string
      description: Bitiş nedeninin adı (bkz. oyun kodları); protokolün bilmediği bir kod için `Unknown`.
      enum:
        - Checkmate
        - Resignation
        - Timeout
        - IllegalMoves
        - Stalemate
        - InsufficientMaterial
        - TimeoutVsInsufficient
        - FivefoldRepetition
        - SeventyFiveMoves
        - ThreefoldClaim
        - FiftyMoveClaim
        - Agreement
        - IllegalMovesVsInsufficient
        - ResignationVsInsufficient
        - Abandonment
        - AbandonmentVsInsufficient
        - Aborted
        - NoShow
        - Forfeit
        - ServerAborted
        - BothDisconnected
        - Unknown
    PlayerGameSummary:
      description: Bir oyuncunun açısından oyun özeti.
      allOf:
        - $ref: '#/components/schemas/GameSummary'
        - type: object
          required:
            - color
          properties:
            color:
              type: string
              enum:
                - white
                - black
              description: Oyuncunun tarafı.
    HistoryGameSummary:
      description: Giriş yapmış oyuncunun geçmişindeki bir oyun.
      allOf:
        - $ref: '#/components/schemas/PlayerGameSummary'
        - type: object
          required:
            - baseMs
            - incMs
            - outcome
          properties:
            baseMs:
              type: integer
              format: int64
              minimum: 0
              description: Milisaniye cinsinden temel süre.
            incMs:
              type: integer
              format: int64
              minimum: 0
              description: Milisaniye cinsinden hamle başına ek süre.
            outcome:
              type: string
              enum:
                - win
                - loss
                - draw
                - aborted
              description: Oyuncunun açısından sonuç.
    HistoryPage:
      type: object
      required:
        - games
        - next
        - total
      properties:
        games:
          type: array
          description: Sayfanın oyunları, en yeni önce.
          items:
            $ref: '#/components/schemas/HistoryGameSummary'
        next:
          type:
            - integer
            - 'null'
          format: int64
          description: Sonraki sayfa için `before` olarak gönderilecek kimlik ya da son sayfada `null`.
        total:
          type: integer
          minimum: 0
          description: Tüm sayfalar genelinde filtreye uyan oyun sayısı.
    MoveRecord:
      type: object
      description: Bir oyunun bir yarım hamlesi.
      required:
        - uci
        - spentMs
        - clockMs
      properties:
        uci:
          type: string
          pattern: '^[a-h][1-8][a-h][1-8][nbrq]?$'
          description: UCI notasyonunda hamle; terfi için sonuna `n`, `b`, `r` veya `q` eklenir.
        spentMs:
          type:
            - integer
            - 'null'
          minimum: 0
          description: Hamle için düşülen süre (ms); kayıtta yoksa `null`.
        clockMs:
          type:
            - integer
            - 'null'
          minimum: 0
          description: Hamleyi yapan oyuncunun hamleden sonraki saati (ms); kayıtta yoksa `null`.
    PgnTagsView:
      type: object
      description: Görüntüleme için başlıca PGN etiketleri. Burada `Termination` bitiş nedeninin adıdır; PGN dosyası PGN standardının değerlerini kullanır.
      required:
        - Event
        - Site
        - Date
        - Round
        - White
        - Black
        - Result
        - WhiteElo
        - BlackElo
        - TimeControl
        - Termination
        - PlyCount
      properties:
        Event:
          type: string
          description: '`<SERVER_NAME> rated <category>` veya `<SERVER_NAME> casual <category>`.'
        Site:
          type: string
          description: '`SERVER_PUBLIC_HOST`.'
        Date:
          type: string
          pattern: '^[0-9]{4}\.[0-9]{2}\.[0-9]{2}$'
          description: UTC başlangıç tarihi.
        Round:
          type: string
          const: '-'
        White:
          type: string
        Black:
          type: string
        Result:
          type: string
          enum:
            - 1-0
            - 0-1
            - 1/2-1/2
            - '*'
        WhiteElo:
          description: Başlangıçtaki puan ya da `"-"`.
          oneOf:
            - type: integer
            - type: string
              const: '-'
        BlackElo:
          description: Başlangıçtaki puan ya da `"-"`.
          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: Hamleleri ve saatleriyle bir oyun kaydı. Bir oyun özetinin alanlarını içerir; `color` ve `outcome` hariç.
      allOf:
        - $ref: '#/components/schemas/GameSummary'
        - type: object
          required:
            - baseMs
            - incMs
            - statusName
            - rematchOf
            - moves
            - pgn
          properties:
            baseMs:
              type: integer
              format: int64
              minimum: 0
              description: Milisaniye cinsinden temel süre.
            incMs:
              type: integer
              format: int64
              minimum: 0
              description: Milisaniye cinsinden hamle başına ek süre.
            statusName:
              type: string
              enum:
                - WhiteWins
                - BlackWins
                - Draw
                - Aborted
              description: '`status` değerinin adı.'
            rematchOf:
              type:
                - integer
                - 'null'
              format: int64
              description: Bu oyunun rövanşı olduğu oyunun kimliği ya da `null`.
            moves:
              type: array
              description: Her yarım hamle için bir öğe.
              items:
                $ref: '#/components/schemas/MoveRecord'
            pgn:
              $ref: '#/components/schemas/PgnTagsView'
            you:
              type: string
              enum:
                - white
                - black
              description: "Yalnızca token'ın sahibi olan oyuncu bu oyunu oynamışsa: onun tarafı."
            reportable:
              type: boolean
              description: "Yalnızca token'ın sahibi olan oyuncu bu oyunu oynamışsa: `POST /reports` bu oyun için rakibe ilişkin bir bildirimi şu anda kabul edecekse `true` (oyun son 7 gün içinde bitmiş, günlük kota tükenmemiş ve rakip bu oyun için henüz bildirilmemiş)."
    PlayerProfile:
      type: object
      required:
        - username
        - createdAt
        - ratings
        - games
      properties:
        username:
          type: string
          description: Oyuncunun yazdığı hâliyle kullanıcı adı.
        createdAt:
          type: integer
          format: int64
          description: Hesabın oluşturulduğu an (epoch ms).
        ratings:
          type: array
          description: Yalnızca resmî kategoriler, sunucunun sırasıyla.
          items:
            $ref: '#/components/schemas/RatingSummary'
        games:
          type: object
          required:
            - total
            - rated
            - wins
            - draws
            - losses
          properties:
            total:
              type: integer
              minimum: 0
              description: Puansız ve iptal edilenler dahil, kaydedilmiş tüm oyunlar.
            rated:
              type: integer
              minimum: 0
              description: Puan kayıtları üzerinden toplanan puanlı oyunlar.
            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: Oyuncunun yazdığı hâliyle kullanıcı adı.
        games:
          type: array
          description: Sayfanın oyunları, en yeni önce.
          items:
            $ref: '#/components/schemas/PlayerGameSummary'
        next:
          type:
            - integer
            - 'null'
          format: int64
          description: Sayfa her dolduğunda son oyunun kimliği (bu durumda sonraki sayfa boş olabilir), aksi hâlde `null`.
    Leaderboard:
      type: object
      required:
        - category
        - minGames
        - updatedAt
        - players
      properties:
        category:
          type: string
          description: Kategori kimliği.
        minGames:
          type: integer
          minimum: 0
          description: Bir kaydın listelenmesi için gereken sayılan oyun sayısı (`PROVISIONAL_GAMES`).
        updatedAt:
          type: integer
          format: int64
          description: Tablonun hesaplandığı an (epoch ms).
        players:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/LeaderboardEntry'
    LeaderboardEntry:
      type: object
      required:
        - rank
        - username
        - rating
        - games
        - wins
        - draws
        - losses
        - peak
      properties:
        rank:
          type: integer
          minimum: 1
        username:
          type: string
        rating:
          type: integer
        games:
          type: integer
          minimum: 0
        wins:
          type: integer
          minimum: 0
        draws:
          type: integer
          minimum: 0
        losses:
          type: integer
          minimum: 0
        peak:
          type: integer
    GifRequest:
      type: object
      additionalProperties: false
      required:
        - pgn
      properties:
        pgn:
          type: string
          description: PGN metni; en fazla 65.536 bayt UTF-8. Yalnızca ilk oyun kullanılır.
        size:
          type: string
          enum:
            - small
            - medium
            - large
          default: medium
          description: İsteğe bağlı. Resim boyutu.
        orientation:
          type: string
          enum:
            - white
            - black
          default: white
          description: İsteğe bağlı. Tahtanın altında yer alan taraf.
        delayMs:
          type: integer
          minimum: 100
          maximum: 3000
          default: 500
          description: İsteğe bağlı. Hamle başına milisaniye; tam sayı değerli bir JSON sayısı.
        coords:
          type: boolean
          default: true
          description: İsteğe bağlı. Dikey harflerinin ve yatay numaralarının tahtanın çevresine çizilip çizilmeyeceği.
    ReportRequest:
      type: object
      required:
        - gameId
        - reported
        - category
      properties:
        gameId:
          description: Oyun; bir tam sayı ya da 1 ila 16 haneli bir dize olarak.
          oneOf:
            - type: integer
              format: int64
              minimum: 1
              maximum: 9007199254740991
            - type: string
              pattern: '^[0-9]{1,16}$'
        reported:
          type: string
          minLength: 1
          maxLength: 24
          description: Rakibin kullanıcı adı; oyun kaydındaki ya da şu anki hâliyle, büyük/küçük harf ayrımı gözetilmeden. Boş olamaz.
        category:
          type: string
          enum:
            - cheating
            - abuse
            - other
          description: Bildirimin konusu.
        comment:
          type:
            - string
            - 'null'
          description: İsteğe bağlı. Denetim karakterleri (sekme ve satır besleme dışında) kaldırılıp metnin başındaki ve sonundaki boşluklar kırpıldıktan sonra en fazla 500 karakter.
    AccountExport:
      type: object
      description: Sunucunun hesap hakkında sakladığı her şey (zamanlar epoch ms cinsinden).
      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: Dışa aktarmanın yapıldığı an.
        server:
          type: object
          required:
            - name
            - host
          properties:
            name:
              type: string
              description: '`SERVER_NAME`.'
            host:
              type: string
              description: '`SERVER_PUBLIC_HOST`.'
        notes:
          type: array
          description: Dosyanın neleri içerdiği ve neleri dışarıda bıraktığı; oyuncu için sade bir İngilizceyle.
          items:
            type: string
        account:
          description: '`GET /account/me` uç noktasının hesap görünümü, `googleEmail` ile birlikte.'
          allOf:
            - $ref: '#/components/schemas/AccountView'
            - type: object
              required:
                - googleEmail
              properties:
                googleEmail:
                  type:
                    - string
                    - 'null'
                  description: Bağlı Google hesabının adresi ya da `null`.
        ratings:
          type: array
          description: Tam puan kayıtları.
          items:
            $ref: '#/components/schemas/ExportRating'
        ratingRefunds:
          type: array
          description: Bir rakibin hile yaptığı belirlendikten sonra iade edilen puanlar; UTC günü ve kategori başına toplanmış, en yeni önce. Ne oyunlar ne de hileciler belirtilir.
          items:
            $ref: '#/components/schemas/RatingRefund'
        sessions:
          type: array
          description: Kaydedilmiş tüm oturumlar, en yeni önce, hiçbir token olmadan. Saklama temizliği, süresi dolmuş bir oturumu siler; kapatılmış bir oturumu ise çıkıştan bir gün sonra siler.
          items:
            $ref: '#/components/schemas/ExportSession'
        securityEvents:
          type: array
          description: Güvenlik olayları, en yeni önce; `RETENTION_SECURITY_DAYS` gün saklanır.
          items:
            $ref: '#/components/schemas/SecurityEvent'
        sanctions:
          type: array
          description: Kaldırılanlar dahil tüm yaptırımlar. Moderatörün adı hiçbir zaman dahil edilmez.
          items:
            $ref: '#/components/schemas/ExportSanction'
        conduct:
          type: array
          description: Oyuncunun ayrıldığı, iptal edilen ve zamanında ilk hamlesini yapmadığı oyunlar için kaydedilen davranış olayları; 30 gün saklanır.
          items:
            $ref: '#/components/schemas/ConductEvent'
        reportsFiled:
          type: array
          description: Oyuncunun yaptığı bildirimler.
          items:
            $ref: '#/components/schemas/FiledReport'
        games:
          type: object
          required:
            - total
            - list
          properties:
            total:
              type: integer
              minimum: 0
              description: Oyun sayısı.
            list:
              type: array
              description: Tüm oyunlar, en yeni önce, `GET /account/games` özetleri olarak. Hamleler `GET /games/{id}`, PGN ise `GET /games/{id}/pgn` adresindedir.
              items:
                $ref: '#/components/schemas/HistoryGameSummary'
    ExportRating:
      description: Tam bir puan kaydı.
      allOf:
        - $ref: '#/components/schemas/RatingSummary'
        - type: object
          required:
            - rated
            - countedGames
            - updatedAt
          properties:
            rated:
              type: boolean
              description: Oyuncunun, puanının henüz belirlenmediği aşamayı geride bırakıp bırakmadığı.
            countedGames:
              type: integer
              minimum: 0
              description: Puana katılan oyunlar.
            updatedAt:
              type: integer
              format: int64
              description: Kaydın son değiştiği an.
    RatingRefund:
      type: object
      required:
        - day
        - category
        - points
      properties:
        day:
          type: integer
          format: int64
          description: Günün 00:00 UTC anı (epoch ms).
        category:
          type: string
        points:
          type: integer
          description: O gün o kategoride iade edilen puanlar.
    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: Oturumun kapatıldığı an ya da `null`.
        clientLabel:
          type:
            - string
            - 'null'
        ip:
          type:
            - string
            - 'null'
          description: Girişin yapıldığı adres; `RETENTION_IP_DAYS` gün sonra silinir.
    SecurityEvent:
      type: object
      description: |-
        Bir güvenlik olayı. `ip` yalnızca giriş yapılmışken, hesabın parolasıyla (ve ikinci faktörle) ya da adresine e-postayla gönderilen bir bağlantıyla yapılan işlemler için verilir: `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` ve `account_exported`; diğer tüm türlerde `ip: null` olur ve `ip`, `RETENTION_IP_DAYS` gün sonra silinir.

        `detail` yalnızca şu alanları tutar: `login`: `method`; `sso_login`, `sso_account_created`: `provider`; `sso_linked`: `provider` ve `method` (`password` ya da `password+totp`); `login_failed`: `failures`; `login_lockout`: `retryAfterMs`; `mfa_failed`: `attempts`; `recovery_code_used`: `remaining`; `reauth_failed`: `factor`; `session_revoked` ve `sessions_revoked_all`: `reason`; `email_change_refused`: `reason`; `sanction_auto`: `kind`, `gameId`, `until`. Bir `moderator_action` olayı yalnızca `{ action }` tutar; bu da yalnızca `ban`, `unban`, `reset_mfa`, `verify_email` ve `revoke_sessions` eylemleri içindir (diğer moderatör eylemleri dışarıda bırakılır). Diğer tüm türlerde `detail: null` olur. `rating_refund` olayları dışarıda bırakılır: puanları `ratingRefunds` içindedir.
      required:
        - kind
        - at
        - ip
        - detail
      properties:
        kind:
          type: string
          description: Olayın türü (`login`, `password_changed`...).
        at:
          type: integer
          format: int64
          description: Olayın gerçekleştiği an.
        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: Bildirilen oyuncunun şu anki herkese açık adı.
        category:
          type: string
          enum:
            - cheating
            - abuse
            - other
        comment:
          type:
            - string
            - 'null'
        createdAt:
          type: integer
          format: int64
        status:
          type: string
          enum:
            - open
            - closed
          description: Bildirimin hâlâ açık olup olmadığı (bildirilen oyuncunun yaptırım alıp almadığı söylenmez).
    LinkTokenForm:
      type: object
      additionalProperties: false
      required:
        - token
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: E-posta bağlantısının token'ı (sayfadaki formun gizli alanı).
    ResetPasswordForm:
      type: object
      additionalProperties: false
      required:
        - token
        - newPassword
        - confirmPassword
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
          description: Sıfırlama bağlantısının token'ı (formun gizli alanı).
        newPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Yeni parola; kayıttaki parola kuralları geçerlidir.
        confirmPassword:
          type: string
          minLength: 1
          maxLength: 1024
          description: Yeni parola, ikinci kez; aynı olmalıdır.
    HtmlDocument:
      type: string
      contentMediaType: text/html
      description: JavaScript ya da harici kaynak içermeyen, UTF-8 kodlamasında bir HTML sayfası (`text/html; charset=utf-8`).
