{
  "openapi": "3.1.0",
  "info": {
    "title": "AWNEX ID (auth.awnex.pl)",
    "version": "1.0.0",
    "description": "OAuth2/OIDC provider tożsamości ekosystemu Awnex. Pełna dokumentacja opisowa (dla ludzi i botów): https://auth.awnex.pl/doc — surowy Markdown: https://auth.awnex.pl/llms.txt"
  },
  "servers": [{ "url": "https://auth.awnex.pl" }],
  "tags": [
    { "name": "discovery", "description": "OIDC discovery / JWKS" },
    { "name": "oauth", "description": "Tryb A — Authorization Code + PKCE" },
    { "name": "identity", "description": "Tryb B — service-to-service identity sync" },
    { "name": "account", "description": "Konto: rejestracja, reset hasła" }
  ],
  "paths": {
    "/.well-known/openid-configuration": {
      "get": {
        "tags": ["discovery"],
        "summary": "OIDC discovery document",
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object" } } } } }
      }
    },
    "/.well-known/jwks.json": {
      "get": {
        "tags": ["discovery"],
        "summary": "JWKS — zawsze { keys: [] }, tokeny są HS256 (sekret współdzielony), nie RS256. Istnieje tylko żeby spec-compliant klienci OIDC nie odrzucali discovery.",
        "responses": { "200": { "description": "OK" } }
      }
    },
    "/api/oauth/authorize": {
      "get": {
        "tags": ["oauth"],
        "summary": "Krok 1 flow Authorization Code — przekierowuje przeglądarkę użytkownika do ekranu logowania (lub od razu wystawia auth code, jeśli już zalogowany).",
        "parameters": [
          { "name": "client_id", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "redirect_uri", "in": "query", "required": true, "schema": { "type": "string", "format": "uri" }, "description": "Musi dokładnie (string-equal) odpowiadać jednemu z redirect_uris zarejestrowanych dla client_id." },
          { "name": "response_type", "in": "query", "required": true, "schema": { "type": "string", "enum": ["code"] } },
          { "name": "scope", "in": "query", "schema": { "type": "string", "default": "openid profile email" } },
          { "name": "state", "in": "query", "schema": { "type": "string" }, "description": "Anty-CSRF, echo-owany z powrotem w redirect." },
          { "name": "code_challenge", "in": "query", "schema": { "type": "string" }, "description": "PKCE, base64url(sha256(code_verifier)). Opcjonalne w API, silnie zalecane dla SPA/mobile." },
          { "name": "code_challenge_method", "in": "query", "schema": { "type": "string", "enum": ["S256", "plain"], "default": "S256" } }
        ],
        "responses": {
          "302": { "description": "Redirect do /login (niezalogowany) albo do redirect_uri?code=...&state=... (już zalogowany)." },
          "400": { "description": "unsupported_response_type | invalid_client | invalid_redirect_uri" }
        }
      }
    },
    "/api/oauth/token": {
      "post": {
        "tags": ["oauth"],
        "summary": "Wymiana authorization code (lub refresh_token) na tokeny. Wywołanie server-to-server — wymaga client_secret.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "oneOf": [
                  {
                    "properties": {
                      "grant_type": { "const": "authorization_code" },
                      "code": { "type": "string" },
                      "redirect_uri": { "type": "string", "format": "uri" },
                      "code_verifier": { "type": "string", "description": "Wymagane, jeśli /authorize użyło code_challenge." },
                      "client_id": { "type": "string" },
                      "client_secret": { "type": "string" }
                    },
                    "required": ["grant_type", "code", "redirect_uri"]
                  },
                  {
                    "properties": {
                      "grant_type": { "const": "refresh_token" },
                      "refresh_token": { "type": "string" },
                      "client_id": { "type": "string" },
                      "client_secret": { "type": "string" }
                    },
                    "required": ["grant_type", "refresh_token"]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "access_token": { "type": "string", "description": "JWT HS256, TTL 3600s. Weryfikowalny lokalnie tylko z AWNEX_AUTH_TOKEN_SECRET (wewnętrzne usługi); zewnętrzni integratorzy powinni użyć /api/oauth/userinfo." },
                    "token_type": { "const": "Bearer" },
                    "expires_in": { "type": "integer", "example": 3600 },
                    "refresh_token": { "type": "string", "description": "64-znakowy hex, TTL 30 dni, nie rotuje się przy odświeżaniu." },
                    "id_token": { "type": "string", "description": "Obecny tylko, gdy scope zawierał 'openid'." },
                    "scope": { "type": "string" }
                  }
                }
              }
            }
          },
          "400": { "description": "invalid_code | code_already_used | code_expired | client_mismatch | redirect_uri_mismatch | code_verifier_required | invalid_code_verifier | invalid_refresh_token | missing_params" },
          "401": { "description": "invalid_client" },
          "429": { "description": "too_many_attempts — 20 nieudanych prób client_secret / 5 min / IP+client_id" }
        }
      }
    },
    "/api/oauth/userinfo": {
      "get": {
        "tags": ["oauth"],
        "summary": "Aktualne dane zalogowanego użytkownika. Jedyne wiarygodne źródło — nie dekoduj tokenów ręcznie.",
        "security": [{ "bearerAccessToken": [] }],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sub": { "type": "string", "format": "uuid" },
                    "id": { "type": "string", "format": "uuid" },
                    "email": { "type": "string", "format": "email" },
                    "email_verified": { "type": "boolean" },
                    "name": { "type": "string" },
                    "avatar": { "type": "string", "nullable": true },
                    "picture": { "type": "string", "nullable": true },
                    "services": { "type": "array", "items": { "type": "object", "properties": { "service": { "type": "string" }, "localUserId": { "type": "string" }, "role": { "type": "string", "nullable": true }, "linkedAt": { "type": "string", "format": "date-time" } } } },
                    "providers": { "type": "array", "items": { "type": "object", "properties": { "provider": { "type": "string" }, "email": { "type": "string", "nullable": true }, "linkedAt": { "type": "string", "format": "date-time" } } } },
                    "createdAt": { "type": "string", "format": "date-time" }
                  }
                }
              }
            }
          },
          "401": { "description": "unauthenticated | invalid_token" },
          "404": { "description": "user_not_found" }
        }
      }
    },
    "/api/oauth/revoke": {
      "post": {
        "tags": ["oauth"],
        "summary": "RFC 7009 — unieważnia jeden refresh_token. Zawsze 200, niezależnie czy token istniał.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": { "type": "object", "required": ["token", "client_id", "client_secret"], "properties": { "token": { "type": "string" }, "client_id": { "type": "string" }, "client_secret": { "type": "string" } } }
            }
          }
        },
        "responses": { "200": { "description": "OK (zawsze, dla dowolnego dobrze uwierzytelnionego żądania)" }, "401": { "description": "invalid_client" }, "400": { "description": "missing_token" } }
      }
    },
    "/api/auth/logout": {
      "post": {
        "tags": ["oauth"],
        "summary": "Globalne wylogowanie — unieważnia WSZYSTKIE refresh tokeny konta, na wszystkich klientach.",
        "security": [{ "bearerAccessToken": [] }],
        "responses": { "204": { "description": "No Content" }, "401": { "description": "unauthenticated | invalid_token" } }
      }
    },
    "/api/identity/sync": {
      "post": {
        "tags": ["identity"],
        "summary": "Tryb B — utwórz/zaktualizuj konto AWNEX ID i powiąż je z użytkownikiem lokalnym Twojego systemu. Wywołanie server-to-server z kluczem serwisu.",
        "security": [{ "bearerServiceKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["service", "localUserId", "email"],
                "properties": {
                  "service": { "type": "string", "description": "Slug Twojego serwisu." },
                  "localUserId": { "type": "string", "description": "ID użytkownika w Twoim systemie." },
                  "email": { "type": "string", "format": "email" },
                  "name": { "type": "string" },
                  "password": { "type": "string", "description": "Ustawiane tylko, gdy konto jest nowe." },
                  "role": { "type": "string" },
                  "forcePasswordChange": { "type": "boolean" },
                  "sendWelcomeEmail": { "type": "boolean" },
                  "provider": { "type": "string", "enum": ["password", "google", "facebook", "awnex"] },
                  "providerUserId": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "identity": { "type": "object" },
                    "token": { "type": "string", "description": "Identity token (iss=awnex-auth) — INNY format niż access_token z /api/oauth/token, nie jest z nim zamienny." }
                  }
                }
              }
            }
          },
          "400": { "description": "service_required | sync_failed" },
          "401": { "description": "unauthorized_service" }
        }
      }
    },
    "/api/auth/forgot-password": {
      "post": {
        "tags": ["account"],
        "summary": "Wysyła e-mail z linkiem resetu hasła, jeśli konto istnieje i ma hasło. Zawsze zwraca success:true (nie zdradza istnienia konta).",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["email"], "properties": { "email": { "type": "string", "format": "email" } } } } } },
        "responses": { "200": { "description": "OK" } }
      }
    },
    "/api/auth/reset-password": {
      "post": {
        "tags": ["account"],
        "summary": "Ustawia nowe hasło z tokenu resetu. Jeśli przekazano kontekst OAuth, zwraca gotowy redirectTo z auth code.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["token", "password"],
                "properties": {
                  "token": { "type": "string" },
                  "password": { "type": "string", "minLength": 8 },
                  "client_id": { "type": "string" },
                  "redirect_uri": { "type": "string" },
                  "scope": { "type": "string" },
                  "state": { "type": "string" },
                  "code_challenge": { "type": "string" },
                  "code_challenge_method": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": { "200": { "description": "OK, opcjonalnie z polem redirectTo" }, "400": { "description": "invalid_or_expired_token | password_min_8_chars" } }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAccessToken": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT", "description": "access_token z /api/oauth/token." },
      "bearerServiceKey": { "type": "http", "scheme": "bearer", "description": "Sekretny klucz serwisu, wydawany ręcznie per integracja (Tryb B)." }
    }
  }
}
