openapi: 3.1.0
info:
  title: Authentication Service API
  description: |
    AuthService is a self-hosted, multi-tenant authentication platform for teams that want provider-grade auth without handing user data, pricing leverage, or service-to-service trust to a third party.

    It provides email/password auth, OAuth2 social login, enterprise SSO with SAML/OIDC, SCIM 2.0 directory sync, machine-to-machine client credentials, magic links, TOTP 2FA, adaptive security and step-up, passkeys/WebAuthn, organization RBAC, refresh-token rotation, tenant-scoped JWTs, JWKS, audit logs, REST, gRPC, and an importable Go JWT validator.

    ## What This Replaces

    Use AuthService when you would otherwise reach for Auth0, Clerk, Firebase Auth, Supabase Auth, Cognito, Stytch, WorkOS, or Keycloak but need a lightweight service your company can own, deploy, inspect, and extend.

    | Need | AuthService Capability |
    |------|------------------------|
    | Product authentication | Signup, login, refresh, logout, profile, password change |
    | Passwordless login | Magic links and passkeys/WebAuthn |
    | MFA | TOTP setup, enable, login verify, and disable lifecycle |
    | Adaptive security | Risk-driven MFA, remembered devices, admin/user step-up, and policy-driven challenge/block/notify decisions |
    | Social identity | Google, GitHub, Microsoft, and Apple OAuth2 |
    | Enterprise SSO | Per-client SAML 2.0 and OIDC connections, domain routing, metadata, and JIT provisioning |
    | Directory sync | SCIM 2.0 inbound provisioning for users, groups, deprovisioning, and token rotation |
    | B2B SaaS authorization | Organizations, owner/admin/member/viewer roles, invitations, permissions, and org-scoped JWT claims |
    | Backend automation | OAuth2 client credentials, service accounts, scoped secrets, token introspection, and key rotation |
    | Multi-product tenancy | Client-specific users, sessions, origins, signing keys, and API keys |
    | Microservice token trust | Local JWT validation with HS256 or RS256/JWKS plus gRPC token validation |
    | Enterprise-style evidence | Queryable audit events with tenant/user/type filters, CSV/NDJSON export, and signed webhook delivery |

    ## Integration Quickstart

    1. **Create a client** for each product, environment, or tenant boundary with `POST /api/admin/clients`.
    2. **Store the returned `api_key` securely**. Send it as `X-API-Key` on app-initiated auth requests.
    3. **Choose a session model**:
       - Browser apps: omit `token_transport` and let AuthService set the `HttpOnly` `auth_refresh` cookie.
       - Native apps, CLIs, server-side rendered apps, and API-only products: pass `token_transport=json` to receive `refresh_token` in JSON. Legacy `session_mode=token` remains supported.
    4. **Authenticate users** through signup, login, OAuth, magic links, TOTP, or passkeys.
    5. **For B2B products**, create organizations, invite members, and mint org-scoped access tokens before calling tenant-aware product APIs.
    6. **For service-to-service automation**, provision service accounts and exchange scoped client credentials at `/oauth/token`.
    7. **Validate access tokens** in downstream services using JWKS, introspection, the Go validator package, or the gRPC TokenService.
    8. **Operate the integration** with admin client rotation, tenant-scoped JWKS lookup, service-account key rotation, audit-event queries/exports, and signed webhooks.

    ```bash
    curl -X POST https://authservice.ayushojha.com/api/auth/login \
      -H "X-API-Key: $AUTH_SERVICE_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "email": "user@example.com",
        "password": "correct-horse-battery-staple",
        "token_transport": "json"
      }'
    ```

    ## Core Concepts

    | Concept | Meaning |
    |---------|---------|
    | Client | A product, app, environment, or tenant boundary registered in AuthService |
    | API key | Client credential sent as `X-API-Key`; never expose it in untrusted public code |
    | Admin identity | Operator identity signed in through `/api/admin/auth/login` or `/api/admin/auth/sso` |
    | Admin key | Break-glass credential sent as `X-Admin-Key`; rate-limited and audited |
    | Access token | Short-lived JWT used as `Authorization: Bearer <token>` |
    | Organization | B2B workspace/account under a client, with role and permission scoped memberships |
    | Service account | Machine identity under a client, with scoped credentials for backend automation |
    | Refresh token | Long-lived, single-use session credential rotated on every refresh |
    | Token transport | JSON refresh-token delivery for mobile, CLI, API, or SSR integrations |
    | Cookie mode | Default browser session mode using an `HttpOnly` `auth_refresh` cookie |
    | JWKS | Public key discovery endpoint for RS256 token validation |

    ## Header Reference

    | Header | Required For | Description |
    |--------|--------------|-------------|
    | `X-API-Key` | App-initiated `/api/auth/*` requests | Identifies the client/tenant and enforces tenant ownership |
    | `Authorization: Bearer <token>` | User-protected endpoints | Carries the user's short-lived access token |
    | `Authorization: Bearer <admin_token>` | `/api/admin/*` endpoints | Carries an admin user's scoped roles and tenant delegation |
    | `X-Step-Up-Token` | Sensitive user/admin mutations after challenge verification | Carries a short-lived step-up proof for the protected action |
    | `X-Admin-Key` | `/api/admin/*` endpoints | Break-glass fallback for bootstrap and emergencies |
    | `X-AuthService-Signature` | Outbound audit webhooks | HMAC-SHA256 signature over `timestamp + "." + raw_body` using `WEBHOOK_SIGNING_SECRET` |

    ## Recommended Integration Patterns

    ### Browser SaaS or Web App

    Use cookie mode for refresh tokens, configure `allowed_origins` for the exact production origins, and keep `X-API-Key` on your backend when possible. If you call AuthService directly from the browser, treat the client API key as product-scoped and rotate it when exposed.

    ### Mobile, Desktop, CLI, or API-only Product

    Use `token_transport=json`, store refresh tokens in the platform's secure storage, call `/api/auth/refresh` before access-token expiry, and send `Authorization: Bearer <access_token>` to your own APIs. Legacy `session_mode=token` maps to the same JSON refresh-token transport.

    ### Microservices

    Prefer RS256/JWKS clients for scalable local validation. Cache `/.well-known/jwks.json?client_id=<client_id>` and enforce the `client_id`, `sub`, `role`, and `email_verified` claims in each service. Go services can use `pkg/jwtvalidator`; non-Go services can validate against JWKS with any standards-compliant JWT library.

    ### Multi-product or B2B Platform

    Create one client per product, environment, or security boundary. This keeps API keys, refresh tokens, audit logs, WebAuthn relying-party settings, CORS origins, and token validation scoped to the right product.

    ## Security Model

    - Access tokens are short-lived JWTs; the default TTL is 15 minutes.
    - Refresh tokens are single-use, SHA-256 hashed at rest, and rotated on refresh.
    - Password changes and password resets revoke existing sessions.
    - API key middleware binds auth flows to the active client.
    - JWT middleware rejects tokens whose `client_id` does not match the request client.
    - OAuth state uses PKCE and tenant-bound signed state.
    - Redis-backed flows protect magic links, OAuth state, passkey ceremonies, TOTP login challenges, and rate limits.
    - CORS is evaluated against configured service origins and per-client `allowed_origins`.

    ## Production Checklist

    - Set `BASE_URL` to the public HTTPS origin of the service.
    - Set `COOKIE_SECURE=true` in production.
    - Use `COOKIE_SAMESITE=lax` for same-site apps or `none` for trusted cross-site browser flows over HTTPS.
    - Configure exact `allowed_origins` on every client.
    - Use RS256/JWKS mode for new service-to-service integrations.
    - Keep `ADMIN_API_KEY`, client API keys, OAuth provider secrets, and Resend keys in a secret manager.
    - Monitor `GET /api/admin/audit-events` for suspicious sign-in, reset, and MFA activity.
    - Set `WEBHOOK_SIGNING_SECRET` before relying on client `webhook_url` audit delivery.
    - Rotate API keys and signing material during incidents or ownership changes.

    ## Token Lifecycle

    - **Access tokens** expire in 15 minutes by default (`expires_in: 900`).
    - **Refresh tokens** are single-use and rotated on each refresh.
    - **Cookie sessions** receive the rotated refresh token in `auth_refresh`.
    - **Token sessions** receive the rotated refresh token in the JSON response.
    - **Session revocation** happens on logout, password change, and password reset.
  version: 1.0.0
  contact:
    name: Ayush Ojha
    url: https://ayushojha.com
  license:
    name: MIT
externalDocs:
  description: Repository README, architecture notes, deployment guide, and provider gap analysis
  url: https://github.com/Ayush10/authentication-service

servers:
  - url: https://authservice.ayushojha.com
    description: Production
  - url: http://localhost:8080
    description: Local development

tags:
  - name: Authentication
    description: Core account lifecycle, session management, profile updates, password changes, and token refresh
  - name: Email Verification
    description: Public email verification plus secure password reset and resend flows
  - name: Magic Links
    description: Passwordless email authentication with JSON token mode or browser redirects
  - name: TOTP
    description: Time-based one-time password MFA setup, enablement, login verification, and disablement
  - name: Adaptive Security
    description: Risk-driven MFA, remembered devices, user/admin step-up, and tenant/org security policy controls
  - name: OAuth2
    description: Tenant-bound social login via Google, GitHub, Microsoft, and Apple with PKCE-backed state
  - name: Passkeys
    description: WebAuthn/FIDO2 registration, login, listing, and deletion for modern passwordless UX
  - name: Organizations
    description: B2B SaaS organizations, memberships, invitations, roles, permissions, and org-scoped token issuance
  - name: Machine Auth
    description: OAuth2 client credentials, service accounts, scoped secrets, token introspection, rotation, and revocation
  - name: Enterprise SSO
    description: Enterprise SAML/OIDC connections, domain-routed login starts, SAML metadata, callbacks, and JIT user provisioning
  - name: SCIM
    description: SCIM 2.0 directory provisioning for users, groups, deprovisioning, and per-directory bearer tokens
  - name: Admin
    description: Client provisioning, key rotation, and audit-event operations requiring the master admin key
  - name: Utility
    description: Health checks and JWKS key discovery for token validation

x-tagGroups:
  - name: Build User Login
    tags: [Authentication, Email Verification, Magic Links, TOTP, Adaptive Security, OAuth2, Passkeys]
  - name: Build B2B SaaS
    tags: [Organizations]
  - name: Connect Services
    tags: [Machine Auth]
  - name: Operate AuthService
    tags: [Admin, Utility]

paths:
  # ──────────────────────────────────────────────
  # Authentication
  # ──────────────────────────────────────────────
  /api/auth/signup:
    post:
      operationId: signup
      summary: Create a new user account
      description: |
        Registers a new user for the client identified by `X-API-Key`. Returns an access token and user profile.

        **Rate limit:** 5 requests per hour per IP.
      tags: [Authentication]
      x-codeSamples:
        - lang: cURL
          label: Create a user in token mode
          source: |
            curl -X POST https://authservice.ayushojha.com/api/auth/signup \
              -H "X-API-Key: $AUTH_SERVICE_API_KEY" \
              -H "Content-Type: application/json" \
              -d '{
                "email": "user@example.com",
                "password": "correct-horse-battery-staple",
                "display_name": "Jane Doe",
                "session_mode": "token"
              }'
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SignupRequest"
            examples:
              browserCookieSession:
                summary: Browser app using HttpOnly refresh cookie
                value:
                  email: user@example.com
                  password: correct-horse-battery-staple
                  display_name: Jane Doe
              tokenSession:
                summary: Mobile, CLI, or API client receiving refresh token in JSON
                value:
                  email: user@example.com
                  password: correct-horse-battery-staple
                  display_name: Jane Doe
                  session_mode: token
      responses:
        "201":
          description: Account created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResponse"
        "400":
          description: Invalid input (bad email, weak password)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Email already registered
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: "email already registered"
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/login:
    post:
      operationId: login
      summary: Sign in with email and password
      description: |
        Authenticates a user and returns an access token. If the user has TOTP 2FA enabled, returns a `two_factor_token` instead — use it with the [TOTP Verify](#tag/TOTP/operation/totpVerify) endpoint to complete login.

        **Rate limit:** 10 requests per 15 minutes per IP. Account locks for 30 minutes after repeated failures.
      tags: [Authentication]
      x-codeSamples:
        - lang: cURL
          label: Login and receive JSON refresh token
          source: |
            curl -X POST https://authservice.ayushojha.com/api/auth/login \
              -H "X-API-Key: $AUTH_SERVICE_API_KEY" \
              -H "Content-Type: application/json" \
              -d '{
                "email": "user@example.com",
                "password": "correct-horse-battery-staple",
                "session_mode": "token"
              }'
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LoginRequest"
            examples:
              browserCookieSession:
                summary: Browser app using HttpOnly refresh cookie
                value:
                  email: user@example.com
                  password: correct-horse-battery-staple
              tokenSession:
                summary: Native app or backend integration using token mode
                value:
                  email: user@example.com
                  password: correct-horse-battery-staple
                  session_mode: token
      responses:
        "200":
          description: Login successful (or 2FA required)
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/AuthResponse"
                  - $ref: "#/components/schemas/TwoFactorChallenge"
        "401":
          description: Invalid credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Account suspended
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rate limit or account locked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/refresh:
    post:
      operationId: refreshToken
      summary: Refresh an access token
      description: |
        Exchanges a refresh token for a new access token. Refresh tokens are single-use and rotated on each call.

        Provide the refresh token either as:
        - `refresh_token` in the request body (token mode)
        - `auth_refresh` cookie (cookie mode)
      tags: [Authentication]
      x-codeSamples:
        - lang: cURL
          label: Rotate a token-mode refresh token
          source: |
            curl -X POST https://authservice.ayushojha.com/api/auth/refresh \
              -H "X-API-Key: $AUTH_SERVICE_API_KEY" \
              -H "Content-Type: application/json" \
              -d '{
                "refresh_token": "$AUTH_REFRESH_TOKEN",
                "session_mode": "token"
              }'
      security:
        - ApiKeyAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RefreshRequest"
            examples:
              cookieMode:
                summary: Browser refresh using auth_refresh cookie
                value: {}
              tokenMode:
                summary: Token refresh for native, CLI, or API integrations
                value:
                  refresh_token: rfr_opaque_refresh_token
                  session_mode: token
      responses:
        "200":
          description: Token refreshed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResponse"
        "401":
          description: Invalid or expired refresh token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/logout:
    post:
      operationId: logout
      summary: Sign out and revoke session
      description: Revokes the current refresh token and clears the session cookie. Browser clients can rely on the `auth_refresh` cookie; token-mode clients can send `refresh_token` in the request body.
      tags: [Authentication]
      security:
        - ApiKeyAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LogoutRequest"
            examples:
              cookieMode:
                summary: Browser logout using auth_refresh cookie
                value: {}
              tokenMode:
                summary: Revoke an explicit refresh token
                value:
                  refresh_token: rfr_opaque_refresh_token
      responses:
        "200":
          description: Logged out
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"

  /api/auth/ui/config:
    get:
      operationId: getAuthUIConfig
      summary: Get hosted and embedded UI configuration
      description: Returns sanitized tenant presentation settings for hosted auth pages and embedded UI widgets.
      tags: [Authentication]
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: Tenant UI configuration
          content:
            application/json:
              schema:
                type: object
                properties:
                  client:
                    type: object
                    properties:
                      id:
                        type: string
                      name:
                        type: string
                      slug:
                        type: string
                      status:
                        type: string
                      token_mode:
                        type: string
                      allowed_origins:
                        type: array
                        items:
                          type: string
                  ui:
                    type: object
                    additionalProperties: true
                  hosted_paths:
                    type: object
                    additionalProperties:
                      type: string
        "401":
          description: Missing or invalid client API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/me:
    get:
      operationId: getMe
      summary: Get current user profile
      description: Returns the profile of the authenticated user.
      tags: [Authentication]
      x-codeSamples:
        - lang: cURL
          label: Read the authenticated user
          source: |
            curl https://authservice.ayushojha.com/api/auth/me \
              -H "X-API-Key: $AUTH_SERVICE_API_KEY" \
              -H "Authorization: Bearer $AUTH_ACCESS_TOKEN"
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      responses:
        "200":
          description: User profile
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "401":
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    patch:
      operationId: updateMe
      summary: Update current user profile
      description: Updates the display name and/or timezone of the authenticated user.
      tags: [Authentication]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateProfileRequest"
            example:
              display_name: Jane Doe
              timezone: America/New_York
      responses:
        "200":
          description: Profile updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "400":
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/sessions:
    get:
      operationId: listSessions
      summary: List active sessions
      description: Returns active refresh sessions for the authenticated user under the current client.
      tags: [Authentication]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      responses:
        "200":
          description: Active sessions
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionListResponse"
        "401":
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      operationId: revokeAllSessions
      summary: Revoke all sessions
      description: Revokes all active refresh sessions for the authenticated user under the current client.
      tags: [Authentication]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      responses:
        "200":
          description: Sessions revoked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "401":
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/sessions/{id}:
    delete:
      operationId: revokeSession
      summary: Revoke a session
      description: Revokes one active refresh session owned by the authenticated user.
      tags: [Authentication]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Session ID
      responses:
        "200":
          description: Session revoked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "401":
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Session not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/change-password:
    post:
      operationId: changePassword
      summary: Change password
      description: |
        Changes the authenticated user's password. **All existing sessions are revoked** — the user must sign in again.
      tags: [Authentication]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChangePasswordRequest"
            example:
              old_password: current-password
              new_password: new-correct-horse-battery-staple
      responses:
        "200":
          description: Password changed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "400":
          description: Incorrect old password or weak new password
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ──────────────────────────────────────────────
  # Email Verification
  # ──────────────────────────────────────────────
  /api/auth/verify-email:
    post:
      operationId: verifyEmail
      summary: Verify email address
      description: Verifies a user's email using the token sent via email. This is a public endpoint — no API key required.
      tags: [Email Verification]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token:
                  type: string
                  description: Verification token from the email link
            example:
              token: ver_email_token_from_link
      responses:
        "200":
          description: Email verified
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "400":
          description: Invalid or expired token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/resend-verification:
    post:
      operationId: resendVerification
      summary: Resend verification email
      description: Sends a new verification email to the authenticated user. Fails if the email is already verified.
      tags: [Email Verification]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      responses:
        "200":
          description: Verification email sent
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "400":
          description: Email already verified
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: Email service not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/forgot-password:
    post:
      operationId: forgotPassword
      summary: Request a password reset
      description: |
        Sends a password reset email. **Always returns 200** regardless of whether the email exists, to prevent email enumeration.
      tags: [Email Verification]
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
            example:
              email: user@example.com
      responses:
        "200":
          description: If the email exists, a reset link was sent
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"

  /api/auth/reset-password:
    post:
      operationId: resetPassword
      summary: Reset password with token
      description: |
        Resets the user's password using a token from the reset email. **All sessions are revoked.** This is a public endpoint.
      tags: [Email Verification]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ResetPasswordRequest"
            example:
              token: pwd_reset_token_from_link
              new_password: new-correct-horse-battery-staple
      responses:
        "200":
          description: Password reset
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "400":
          description: Invalid or expired token, or weak password
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ──────────────────────────────────────────────
  # Magic Links
  # ──────────────────────────────────────────────
  /api/auth/magic-link/send:
    post:
      operationId: sendMagicLink
      summary: Send a magic link email
      description: |
        Sends a magic link to the specified email. **Always returns 200** to prevent enumeration. Requires Redis.

        **Rate limit:** 3 requests per hour per email.
      tags: [Magic Links]
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
            example:
              email: user@example.com
      responses:
        "200":
          description: Magic link sent (if user exists)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: Email service or Redis not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/magic-link/verify:
    get:
      operationId: verifyMagicLink
      summary: Verify a magic link token
      description: |
        Completes passwordless login via magic link. This is a public endpoint.

        - With `Accept: application/json` or `session_mode=token` — returns JSON with tokens
        - Otherwise — redirects to `{base_url}/login.html?auth_code={one_time_code}`. Exchange the code with `/api/auth/redirect/exchange`; access tokens are not placed in browser URLs.
      tags: [Magic Links]
      parameters:
        - name: token
          in: query
          required: true
          schema:
            type: string
          description: Magic link token from email
        - name: session_mode
          in: query
          required: false
          schema:
            type: string
            enum: [token]
          description: Set to `token` for JSON response with refresh token
      responses:
        "200":
          description: Authentication successful (JSON mode)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResponse"
        "302":
          description: Redirect to login page with one-time auth code
        "400":
          description: Invalid or expired token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ──────────────────────────────────────────────
  # TOTP (2FA)
  # ──────────────────────────────────────────────
  /api/auth/totp/setup:
    post:
      operationId: totpSetup
      summary: Initialize TOTP setup
      description: |
        Generates a new TOTP secret and returns a QR code for the user to scan with their authenticator app. Call [Enable TOTP](#tag/TOTP/operation/totpEnable) with a valid code to activate.
      tags: [TOTP]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      responses:
        "200":
          description: TOTP setup data
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TOTPSetupResponse"
        "400":
          description: TOTP already enabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/totp/enable:
    post:
      operationId: totpEnable
      summary: Enable TOTP 2FA
      description: Activates TOTP for the user by verifying a code from their authenticator app against the pending secret.
      tags: [TOTP]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code:
                  type: string
                  description: 6-digit TOTP code from authenticator app
                  example: "123456"
            example:
              code: "123456"
      responses:
        "200":
          description: TOTP enabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "400":
          description: Invalid code or no pending setup
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/totp/verify:
    post:
      operationId: totpVerify
      summary: Complete login with TOTP code
      description: |
        Verifies the TOTP code during login. Use the `two_factor_token` from the login response. Requires Redis.
      tags: [TOTP]
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TOTPVerifyRequest"
            examples:
              cookieMode:
                summary: Browser completion using refresh cookie
                value:
                  two_factor_token: tfa_login_challenge_token
                  code: "123456"
              tokenMode:
                summary: Native or API completion using token mode
                value:
                  two_factor_token: tfa_login_challenge_token
                  code: "123456"
                  session_mode: token
      responses:
        "200":
          description: Authentication successful
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResponse"
        "400":
          description: Invalid 2FA token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Invalid TOTP code
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: Redis required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/redirect/exchange:
    post:
      operationId: exchangeRedirectAuthCode
      summary: Exchange a browser redirect auth code
      description: |
        Exchanges the short-lived one-time `auth_code` issued by OAuth, enterprise SSO, and magic-link browser redirects for an auth response. Codes are single-use and expire quickly, preventing access-token leakage through browser history, referrers, and logs.
      tags: [Authentication]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code:
                  type: string
      responses:
        "200":
          description: Auth code exchanged
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResponse"
        "400":
          description: Invalid or expired code
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: Redis required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/step-up/verify:
    post:
      operationId: verifyStepUp
      summary: Verify a user step-up challenge
      description: Exchanges a protected-action challenge token plus TOTP or recovery code for a short-lived `step_up_token`.
      tags: [Adaptive Security]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StepUpVerifyRequest"
      responses:
        "200":
          description: Step-up verified
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StepUpVerifyResponse"
        "400":
          description: Invalid or expired challenge token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Invalid verification code
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/devices:
    get:
      operationId: listUserDevices
      summary: List remembered devices
      tags: [Adaptive Security]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      responses:
        "200":
          description: Remembered devices for the current user
          content:
            application/json:
              schema:
                type: object
                properties:
                  devices:
                    type: array
                    items:
                      $ref: "#/components/schemas/UserDevice"

  /api/auth/devices/{device_id}:
    patch:
      operationId: updateUserDeviceTrust
      summary: Trust, untrust, or rename a remembered device
      tags: [Adaptive Security]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - name: device_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                trusted:
                  type: boolean
      responses:
        "200":
          description: Updated device
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserDevice"
    delete:
      operationId: deleteUserDevice
      summary: Remove a remembered device
      tags: [Adaptive Security]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - name: device_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Device removed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"

  /api/auth/totp/disable:
    post:
      operationId: totpDisable
      summary: Disable TOTP 2FA
      description: Disables TOTP for the user. Requires a valid TOTP code to confirm identity.
      tags: [TOTP]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code:
                  type: string
                  description: Current 6-digit TOTP code
                  example: "654321"
            example:
              code: "654321"
      responses:
        "200":
          description: TOTP disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "400":
          description: TOTP not enabled or missing code
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Invalid code or token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/recovery-codes:
    get:
      operationId: recoveryCodesCount
      summary: Count unused recovery codes
      description: Returns the number of unused one-time MFA recovery codes without exposing the codes themselves.
      tags: [TOTP]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      responses:
        "200":
          description: Recovery code count
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RecoveryCodesResponse"
    post:
      operationId: recoveryCodesGenerate
      summary: Generate MFA recovery codes
      description: Rotates one-time recovery codes for a user with TOTP enabled. Codes are shown only once and stored as hashes.
      tags: [TOTP]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      responses:
        "200":
          description: Newly generated recovery codes
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RecoveryCodesResponse"
        "400":
          description: TOTP is not enabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/recovery-codes/verify:
    post:
      operationId: recoveryCodesVerify
      summary: Complete login with a recovery code
      description: Verifies a one-time recovery code during a TOTP login challenge and marks the code used.
      tags: [TOTP]
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RecoveryCodeVerifyRequest"
            example:
              two_factor_token: tfa_login_challenge_token
              code: ABCD-1234-EF56
              session_mode: token
      responses:
        "200":
          description: Authentication successful
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResponse"
        "400":
          description: Invalid 2FA token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Invalid or already used recovery code
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ──────────────────────────────────────────────
  # OAuth2
  # ──────────────────────────────────────────────
  /api/auth/oauth/{provider}:
    get:
      operationId: oauthBegin
      summary: Start OAuth2 flow
      description: |
        Redirects the user to the OAuth provider's authorization page. After the user authorizes, they are redirected back to the callback URL.

        Requires Redis for PKCE state management.
      tags: [OAuth2]
      security:
        - ApiKeyAuth: []
      parameters:
        - name: provider
          in: path
          required: true
          schema:
            type: string
            enum: [google, github, microsoft, apple]
      responses:
        "302":
          description: Redirect to OAuth provider
        "503":
          description: Redis required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/oauth/{provider}/callback:
    get:
      operationId: oauthCallback
      summary: OAuth2 callback
      description: |
        Handles the OAuth provider's redirect. On success, redirects to `{base_url}/login.html?auth_code={one_time_code}` and sets a session cookie unless `session_mode=token` was requested.
        On failure, redirects to `{base_url}/login.html?error={message}`.

        This is a public endpoint called by the OAuth provider.
      tags: [OAuth2]
      parameters:
        - name: provider
          in: path
          required: true
          schema:
            type: string
            enum: [google, github, microsoft, apple]
        - name: code
          in: query
          required: true
          schema:
            type: string
          description: Authorization code from the OAuth provider
        - name: state
          in: query
          required: true
          schema:
            type: string
          description: Signed state parameter
      responses:
        "302":
          description: Redirect to application with one-time auth code or error
    post:
      operationId: oauthCallbackFormPost
      summary: OAuth2 callback (form post)
      description: |
        Handles providers that return OAuth callback parameters with `response_mode=form_post`.

        On success, redirects to `{base_url}/login.html?auth_code={one_time_code}` and sets a session cookie unless token mode was requested. On failure, redirects to `{base_url}/login.html?error={message}`.
      tags: [OAuth2]
      parameters:
        - name: provider
          in: path
          required: true
          schema:
            type: string
            enum: [google, github, microsoft, apple]
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [code, state]
              properties:
                code:
                  type: string
                  description: Authorization code from the OAuth provider
                state:
                  type: string
                  description: Signed state parameter
      responses:
        "302":
          description: Redirect to application with token or error

  # ──────────────────────────────────────────────
  # Passkeys (WebAuthn)
  # ──────────────────────────────────────────────
  /api/auth/passkey/register/begin:
    post:
      operationId: passkeyRegisterBegin
      summary: Start passkey registration
      description: |
        Initiates WebAuthn registration. Returns a `publicKey` options object to pass to `navigator.credentials.create()`. Requires Redis.
      tags: [Passkeys]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      responses:
        "200":
          description: Registration options
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PasskeyCreationOptions"
        "401":
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: Redis required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/passkey/register/finish:
    post:
      operationId: passkeyRegisterFinish
      summary: Complete passkey registration
      description: |
        Completes WebAuthn registration with the browser's attestation response. Requires Redis.
      tags: [Passkeys]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - name: name
          in: query
          required: false
          schema:
            type: string
          description: Friendly name for the passkey (e.g. "MacBook Touch ID")
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebAuthnAttestationResponse"
      responses:
        "201":
          description: Passkey registered
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "400":
          description: Invalid attestation or no registration in progress
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: Redis required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/passkey/login/begin:
    post:
      operationId: passkeyLoginBegin
      summary: Start passkey login
      description: |
        Initiates WebAuthn authentication. Returns options to pass to `navigator.credentials.get()` and a `session_id` for the finish step. Requires Redis.
      tags: [Passkeys]
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: Authentication options
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PasskeyRequestOptions"
        "503":
          description: Redis required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/passkey/login/finish:
    post:
      operationId: passkeyLoginFinish
      summary: Complete passkey login
      description: |
        Completes WebAuthn authentication with the browser's assertion response. Returns access and refresh tokens.
      tags: [Passkeys]
      security:
        - ApiKeyAuth: []
      parameters:
        - name: session_id
          in: query
          required: true
          schema:
            type: string
          description: Session ID from the begin step
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebAuthnAssertionResponse"
      responses:
        "200":
          description: Authentication successful
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResponse"
        "400":
          description: Missing session_id or no login in progress
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Account suspended
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: Redis required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/passkeys:
    get:
      operationId: listPasskeys
      summary: List registered passkeys
      description: Returns all passkeys registered by the authenticated user.
      tags: [Passkeys]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      responses:
        "200":
          description: List of passkeys
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Passkey"
        "401":
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      operationId: deletePasskeyQuery
      summary: Delete a passkey (query param)
      description: Deletes a passkey by credential ID passed as a query parameter.
      tags: [Passkeys]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: string
          description: Passkey credential ID
      responses:
        "200":
          description: Passkey deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "400":
          description: Missing passkey ID
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/passkeys/{id}:
    delete:
      operationId: deletePasskeyPath
      summary: Delete a passkey
      description: Deletes a passkey by credential ID.
      tags: [Passkeys]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Passkey credential ID
      responses:
        "200":
          description: Passkey deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "400":
          description: Missing passkey ID
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ──────────────────────────────────────────────
  # Organizations
  # ──────────────────────────────────────────────
  /api/auth/organizations:
    get:
      operationId: listOrganizations
      summary: List organizations for the current user
      tags: [Organizations]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      responses:
        "200":
          description: User organizations
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrganizationListResponse"
        "401":
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      operationId: createOrganization
      summary: Create an organization
      description: Creates a B2B organization under the current client. The authenticated user becomes the `owner`.
      tags: [Organizations]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateOrganizationRequest"
            example:
              name: Acme Inc
      responses:
        "201":
          description: Organization created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrganizationMembershipDetails"
        "400":
          description: Invalid organization
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Organization slug already exists for this client
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/organizations/{org_id}:
    get:
      operationId: getOrganization
      summary: Get an organization
      tags: [Organizations]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OrganizationID"
      responses:
        "200":
          description: Organization and caller membership
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrganizationMembershipDetails"
        "403":
          description: Caller is not a member
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Organization not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    patch:
      operationId: updateOrganization
      summary: Update organization profile
      description: Requires `org:write`.
      tags: [Organizations]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OrganizationID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateOrganizationRequest"
      responses:
        "200":
          description: Updated organization
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Organization"
        "403":
          description: Missing organization write permission
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/organizations/{org_id}/members:
    get:
      operationId: listOrganizationMembers
      summary: List organization members
      description: Requires `members:read`.
      tags: [Organizations]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OrganizationID"
      responses:
        "200":
          description: Active organization members
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrganizationMembersResponse"
        "403":
          description: Missing members read permission
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/organizations/{org_id}/members/{user_id}:
    patch:
      operationId: updateOrganizationMember
      summary: Update member role or custom permissions
      description: Requires `members:write`. The last owner cannot be demoted.
      tags: [Organizations]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OrganizationID"
        - name: user_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateOrganizationMemberRequest"
      responses:
        "200":
          description: Updated membership
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrganizationMembership"
        "403":
          description: Missing members write permission or invalid owner transition
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      operationId: removeOrganizationMember
      summary: Remove a member
      description: Requires `members:write`. The last owner cannot be removed.
      tags: [Organizations]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OrganizationID"
        - name: user_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Member removed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "403":
          description: Missing members write permission or invalid owner transition
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/organizations/{org_id}/invitations:
    get:
      operationId: listOrganizationInvitations
      summary: List organization invitations
      description: Requires `invitations:read`.
      tags: [Organizations]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OrganizationID"
      responses:
        "200":
          description: Organization invitations
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrganizationInvitationsResponse"
    post:
      operationId: createOrganizationInvitation
      summary: Invite a user to an organization
      description: Requires `invitations:write`. Returns the raw invitation token once.
      tags: [Organizations]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OrganizationID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InviteOrganizationMemberRequest"
      responses:
        "201":
          description: Invitation created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrganizationInvitationWithToken"
        "403":
          description: Missing invitations write permission
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/organizations/{org_id}/invitations/{invitation_id}/revoke:
    post:
      operationId: revokeOrganizationInvitation
      summary: Revoke an invitation
      description: Requires `invitations:write`.
      tags: [Organizations]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OrganizationID"
        - name: invitation_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Invitation revoked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"

  /api/auth/organization-invitations/accept:
    post:
      operationId: acceptOrganizationInvitation
      summary: Accept an organization invitation
      description: The authenticated user's email must match the invitation email.
      tags: [Organizations]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AcceptOrganizationInvitationRequest"
      responses:
        "200":
          description: Invitation accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrganizationMembershipDetails"
        "400":
          description: Invalid or expired invitation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/organizations/{org_id}/token:
    post:
      operationId: createOrganizationToken
      summary: Mint an organization-scoped access token
      description: Returns a short-lived access token containing `org_id`, `org_slug`, `org_role`, and `org_permissions`.
      tags: [Organizations]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OrganizationID"
      responses:
        "200":
          description: Organization-scoped token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrganizationTokenResponse"
        "403":
          description: Caller is not an active organization member
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/organizations/{org_id}/security-policy:
    get:
      operationId: getOrganizationSecurityPolicy
      summary: Get organization adaptive security policy
      tags: [Organizations, Adaptive Security]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OrganizationID"
      responses:
        "200":
          description: Effective organization security policy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdaptiveSecurityPolicy"
    put:
      operationId: replaceOrganizationSecurityPolicy
      summary: Replace organization adaptive security policy
      tags: [Organizations, Adaptive Security]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OrganizationID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AdaptiveSecurityPolicy"
      responses:
        "200":
          description: Updated organization security policy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdaptiveSecurityPolicy"
        "403":
          description: Step-up required or organization permission denied
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/AdaptiveActionDecision"
                  - $ref: "#/components/schemas/Error"
    patch:
      operationId: patchOrganizationSecurityPolicy
      summary: Replace organization adaptive security policy
      tags: [Organizations, Adaptive Security]
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OrganizationID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AdaptiveSecurityPolicy"
      responses:
        "200":
          description: Updated organization security policy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdaptiveSecurityPolicy"

  # ──────────────────────────────────────────────
  # Machine-to-Machine Auth
  # ──────────────────────────────────────────────
  /oauth/token:
    post:
      operationId: createClientCredentialsToken
      summary: Issue a machine-to-machine access token
      description: OAuth2 client credentials grant. Use the service account ID as `client_id` and the returned secret as `client_secret`.
      tags: [Machine Auth]
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: "#/components/schemas/ClientCredentialsRequest"
          application/json:
            schema:
              $ref: "#/components/schemas/ClientCredentialsRequest"
      responses:
        "200":
          description: Access token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/M2MTokenResponse"
        "400":
          description: Unsupported grant type or invalid scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Invalid client credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /oauth/introspect:
    post:
      operationId: introspectToken
      summary: Introspect an access token
      description: Authenticates the caller with service account credentials and returns token metadata for tokens under the same parent client.
      tags: [Machine Auth]
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: "#/components/schemas/TokenIntrospectionRequest"
          application/json:
            schema:
              $ref: "#/components/schemas/TokenIntrospectionRequest"
      responses:
        "200":
          description: Introspection response
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/M2MIntrospectionResponse"
        "401":
          description: Invalid client credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/admin/clients/{id}/service-accounts:
    get:
      operationId: listServiceAccounts
      summary: List service accounts for a client
      tags: [Machine Auth, Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Service accounts
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ServiceAccountListResponse"
    post:
      operationId: createServiceAccount
      summary: Create a service account and initial secret
      description: Returns `client_secret` once. Store it securely.
      tags: [Machine Auth, Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateServiceAccountRequest"
      responses:
        "201":
          description: Service account and initial secret
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ServiceAccountKeyWithSecret"

  /api/admin/clients/{id}/service-accounts/{service_account_id}:
    get:
      operationId: getServiceAccount
      summary: Get a service account
      tags: [Machine Auth, Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ClientIDPath"
        - $ref: "#/components/parameters/ServiceAccountID"
      responses:
        "200":
          description: Service account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ServiceAccount"
    patch:
      operationId: updateServiceAccount
      summary: Update service account metadata, scopes, or status
      tags: [Machine Auth, Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ClientIDPath"
        - $ref: "#/components/parameters/ServiceAccountID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateServiceAccountRequest"
      responses:
        "200":
          description: Updated service account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ServiceAccount"
    delete:
      operationId: disableServiceAccount
      summary: Disable a service account
      tags: [Machine Auth, Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ClientIDPath"
        - $ref: "#/components/parameters/ServiceAccountID"
      responses:
        "200":
          description: Disabled service account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ServiceAccount"

  /api/admin/clients/{id}/service-accounts/{service_account_id}/keys:
    get:
      operationId: listServiceAccountKeys
      summary: List service account keys
      tags: [Machine Auth, Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ClientIDPath"
        - $ref: "#/components/parameters/ServiceAccountID"
      responses:
        "200":
          description: Keys
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ServiceAccountKeyListResponse"
    post:
      operationId: createServiceAccountKey
      summary: Create a scoped service account key
      tags: [Machine Auth, Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ClientIDPath"
        - $ref: "#/components/parameters/ServiceAccountID"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateServiceAccountKeyRequest"
      responses:
        "201":
          description: New key and one-time secret
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ServiceAccountKeyWithSecret"

  /api/admin/clients/{id}/service-accounts/{service_account_id}/keys/{key_id}:
    delete:
      operationId: revokeServiceAccountKey
      summary: Revoke a service account key
      tags: [Machine Auth, Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ClientIDPath"
        - $ref: "#/components/parameters/ServiceAccountID"
        - $ref: "#/components/parameters/ServiceAccountKeyID"
      responses:
        "200":
          description: Key revoked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"

  /api/admin/clients/{id}/service-accounts/{service_account_id}/keys/{key_id}/rotate:
    post:
      operationId: rotateServiceAccountKey
      summary: Rotate a service account key
      description: Creates a replacement key with the same scopes and revokes the old key.
      tags: [Machine Auth, Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ClientIDPath"
        - $ref: "#/components/parameters/ServiceAccountID"
        - $ref: "#/components/parameters/ServiceAccountKeyID"
      responses:
        "200":
          description: Replacement key and one-time secret
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ServiceAccountKeyWithSecret"

  # ──────────────────────────────────────────────
  # Enterprise SSO
  # ──────────────────────────────────────────────
  /api/auth/sso:
    get:
      operationId: beginEnterpriseSSOByDomain
      summary: Start enterprise SSO by email domain
      description: Resolves an active SAML or OIDC connection by domain and redirects the browser to the enterprise identity provider.
      tags: [Enterprise SSO]
      security:
        - ApiKeyAuth: []
      parameters:
        - name: domain
          in: query
          required: true
          schema:
            type: string
          example: acme.com
        - name: session_mode
          in: query
          required: false
          schema:
            type: string
            enum: [token]
      responses:
        "302":
          description: Redirect to the configured SAML or OIDC identity provider
        "404":
          description: No active SSO connection is configured for the domain
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/sso/{sso_connection_id}:
    get:
      operationId: beginEnterpriseSSOByConnection
      summary: Start enterprise SSO by connection
      description: Starts SSO using a connection ID or slug and redirects the browser to the enterprise identity provider.
      tags: [Enterprise SSO]
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/SSOConnectionID"
        - name: session_mode
          in: query
          required: false
          schema:
            type: string
            enum: [token]
      responses:
        "302":
          description: Redirect to the configured SAML or OIDC identity provider
        "400":
          description: Invalid or inactive SSO connection
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/sso/callback/{sso_connection_id}:
    get:
      operationId: enterpriseOIDCCallback
      summary: Complete an enterprise OIDC login
      description: Public OIDC redirect callback. Validates cached state, PKCE, issuer, audience, signature, expiry, and nonce before issuing AuthService tokens.
      tags: [Enterprise SSO]
      parameters:
        - $ref: "#/components/parameters/SSOConnectionID"
        - name: code
          in: query
          required: true
          schema:
            type: string
        - name: state
          in: query
          required: true
          schema:
            type: string
      responses:
        "302":
          description: Redirect to login page with one-time auth code or error
    post:
      operationId: enterpriseSAMLCallback
      summary: Complete an enterprise SAML login
      description: Public SAML Assertion Consumer Service endpoint. Validates signed SAML responses using IdP metadata or configured X.509 certificates before issuing AuthService tokens.
      tags: [Enterprise SSO]
      parameters:
        - $ref: "#/components/parameters/SSOConnectionID"
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: "#/components/schemas/SAMLCallbackRequest"
      responses:
        "302":
          description: Redirect to login page with one-time auth code or error

  /api/auth/sso/metadata/{sso_connection_id}:
    get:
      operationId: getSAMLServiceProviderMetadata
      summary: Get SAML service provider metadata
      description: Public metadata document to upload into Okta, Azure AD, Google Workspace, Ping, or another SAML identity provider.
      tags: [Enterprise SSO]
      parameters:
        - $ref: "#/components/parameters/SSOConnectionID"
      responses:
        "200":
          description: SAML service provider metadata XML
          content:
            application/samlmetadata+xml:
              schema:
                type: string

  /api/admin/clients/{id}/sso-connections:
    get:
      operationId: listEnterpriseSSOConnections
      summary: List enterprise SSO connections
      tags: [Enterprise SSO, Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ClientIDPath"
      responses:
        "200":
          description: Enterprise SSO connections
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EnterpriseSSOConnectionListResponse"
    post:
      operationId: createEnterpriseSSOConnection
      summary: Create an enterprise SSO connection
      tags: [Enterprise SSO, Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ClientIDPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateEnterpriseSSOConnectionRequest"
      responses:
        "201":
          description: Created SSO connection with secrets redacted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EnterpriseSSOConnection"

  /api/admin/clients/{id}/sso-connections/{sso_connection_id}:
    get:
      operationId: getEnterpriseSSOConnection
      summary: Get an enterprise SSO connection
      tags: [Enterprise SSO, Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ClientIDPath"
        - $ref: "#/components/parameters/SSOConnectionID"
      responses:
        "200":
          description: Enterprise SSO connection
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EnterpriseSSOConnection"
    patch:
      operationId: updateEnterpriseSSOConnection
      summary: Update an enterprise SSO connection
      tags: [Enterprise SSO, Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ClientIDPath"
        - $ref: "#/components/parameters/SSOConnectionID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateEnterpriseSSOConnectionRequest"
      responses:
        "200":
          description: Updated SSO connection
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EnterpriseSSOConnection"
    delete:
      operationId: deactivateEnterpriseSSOConnection
      summary: Deactivate an enterprise SSO connection
      tags: [Enterprise SSO, Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ClientIDPath"
        - $ref: "#/components/parameters/SSOConnectionID"
      responses:
        "204":
          description: SSO connection deactivated

  # ──────────────────────────────────────────────
  # SCIM 2.0 Directory Sync
  # ──────────────────────────────────────────────
  /scim/v2/{scim_directory_id}/ServiceProviderConfig:
    get:
      operationId: getSCIMServiceProviderConfig
      summary: Get SCIM service provider capabilities
      tags: [SCIM]
      security:
        - SCIMBearerAuth: []
      parameters:
        - $ref: "#/components/parameters/SCIMDirectoryID"
      responses:
        "200":
          description: SCIM service provider config
          content:
            application/scim+json:
              schema:
                type: object

  /scim/v2/{scim_directory_id}/ResourceTypes:
    get:
      operationId: listSCIMResourceTypes
      summary: List supported SCIM resource types
      tags: [SCIM]
      security:
        - SCIMBearerAuth: []
      parameters:
        - $ref: "#/components/parameters/SCIMDirectoryID"
      responses:
        "200":
          description: Supported resource types
          content:
            application/scim+json:
              schema:
                type: array
                items:
                  type: object

  /scim/v2/{scim_directory_id}/Schemas:
    get:
      operationId: listSCIMSchemas
      summary: List supported SCIM schemas
      tags: [SCIM]
      security:
        - SCIMBearerAuth: []
      parameters:
        - $ref: "#/components/parameters/SCIMDirectoryID"
      responses:
        "200":
          description: Supported schemas
          content:
            application/scim+json:
              schema:
                type: array
                items:
                  type: object

  /scim/v2/{scim_directory_id}/Users:
    get:
      operationId: listSCIMUsers
      summary: List SCIM users
      tags: [SCIM]
      security:
        - SCIMBearerAuth: []
      parameters:
        - $ref: "#/components/parameters/SCIMDirectoryID"
      responses:
        "200":
          description: SCIM user list
          content:
            application/scim+json:
              schema:
                $ref: "#/components/schemas/SCIMListResponse"
    post:
      operationId: createSCIMUser
      summary: Provision a SCIM user
      tags: [SCIM]
      security:
        - SCIMBearerAuth: []
      parameters:
        - $ref: "#/components/parameters/SCIMDirectoryID"
      requestBody:
        required: true
        content:
          application/scim+json:
            schema:
              $ref: "#/components/schemas/SCIMUser"
          application/json:
            schema:
              $ref: "#/components/schemas/SCIMUser"
      responses:
        "201":
          description: Provisioned SCIM user
          content:
            application/scim+json:
              schema:
                $ref: "#/components/schemas/SCIMUser"

  /scim/v2/{scim_directory_id}/Users/{scim_user_id}:
    get:
      operationId: getSCIMUser
      summary: Get a SCIM user
      tags: [SCIM]
      security:
        - SCIMBearerAuth: []
      parameters:
        - $ref: "#/components/parameters/SCIMDirectoryID"
        - $ref: "#/components/parameters/SCIMUserID"
      responses:
        "200":
          description: SCIM user
          content:
            application/scim+json:
              schema:
                $ref: "#/components/schemas/SCIMUser"
    put:
      operationId: replaceSCIMUser
      summary: Replace a SCIM user
      tags: [SCIM]
      security:
        - SCIMBearerAuth: []
      parameters:
        - $ref: "#/components/parameters/SCIMDirectoryID"
        - $ref: "#/components/parameters/SCIMUserID"
      requestBody:
        required: true
        content:
          application/scim+json:
            schema:
              $ref: "#/components/schemas/SCIMUser"
          application/json:
            schema:
              $ref: "#/components/schemas/SCIMUser"
      responses:
        "200":
          description: Updated SCIM user
          content:
            application/scim+json:
              schema:
                $ref: "#/components/schemas/SCIMUser"
    patch:
      operationId: patchSCIMUser
      summary: Patch a SCIM user
      tags: [SCIM]
      security:
        - SCIMBearerAuth: []
      parameters:
        - $ref: "#/components/parameters/SCIMDirectoryID"
        - $ref: "#/components/parameters/SCIMUserID"
      requestBody:
        required: true
        content:
          application/scim+json:
            schema:
              $ref: "#/components/schemas/SCIMPatchRequest"
          application/json:
            schema:
              $ref: "#/components/schemas/SCIMPatchRequest"
      responses:
        "200":
          description: Patched SCIM user
          content:
            application/scim+json:
              schema:
                $ref: "#/components/schemas/SCIMUser"
    delete:
      operationId: deleteSCIMUser
      summary: Deprovision a SCIM user
      description: Suspends the linked AuthService user and removes the SCIM mapping.
      tags: [SCIM]
      security:
        - SCIMBearerAuth: []
      parameters:
        - $ref: "#/components/parameters/SCIMDirectoryID"
        - $ref: "#/components/parameters/SCIMUserID"
      responses:
        "204":
          description: User deprovisioned

  /scim/v2/{scim_directory_id}/Groups:
    get:
      operationId: listSCIMGroups
      summary: List SCIM groups
      tags: [SCIM]
      security:
        - SCIMBearerAuth: []
      parameters:
        - $ref: "#/components/parameters/SCIMDirectoryID"
      responses:
        "200":
          description: SCIM group list
          content:
            application/scim+json:
              schema:
                $ref: "#/components/schemas/SCIMListResponse"
    post:
      operationId: createSCIMGroup
      summary: Sync a SCIM group
      tags: [SCIM]
      security:
        - SCIMBearerAuth: []
      parameters:
        - $ref: "#/components/parameters/SCIMDirectoryID"
      requestBody:
        required: true
        content:
          application/scim+json:
            schema:
              $ref: "#/components/schemas/SCIMGroup"
          application/json:
            schema:
              $ref: "#/components/schemas/SCIMGroup"
      responses:
        "201":
          description: Synced SCIM group
          content:
            application/scim+json:
              schema:
                $ref: "#/components/schemas/SCIMGroup"

  /scim/v2/{scim_directory_id}/Groups/{scim_group_id}:
    get:
      operationId: getSCIMGroup
      summary: Get a SCIM group
      tags: [SCIM]
      security:
        - SCIMBearerAuth: []
      parameters:
        - $ref: "#/components/parameters/SCIMDirectoryID"
        - $ref: "#/components/parameters/SCIMGroupID"
      responses:
        "200":
          description: SCIM group
          content:
            application/scim+json:
              schema:
                $ref: "#/components/schemas/SCIMGroup"
    put:
      operationId: replaceSCIMGroup
      summary: Replace a SCIM group
      tags: [SCIM]
      security:
        - SCIMBearerAuth: []
      parameters:
        - $ref: "#/components/parameters/SCIMDirectoryID"
        - $ref: "#/components/parameters/SCIMGroupID"
      requestBody:
        required: true
        content:
          application/scim+json:
            schema:
              $ref: "#/components/schemas/SCIMGroup"
          application/json:
            schema:
              $ref: "#/components/schemas/SCIMGroup"
      responses:
        "200":
          description: Updated SCIM group
          content:
            application/scim+json:
              schema:
                $ref: "#/components/schemas/SCIMGroup"
    patch:
      operationId: patchSCIMGroup
      summary: Patch a SCIM group
      tags: [SCIM]
      security:
        - SCIMBearerAuth: []
      parameters:
        - $ref: "#/components/parameters/SCIMDirectoryID"
        - $ref: "#/components/parameters/SCIMGroupID"
      requestBody:
        required: true
        content:
          application/scim+json:
            schema:
              $ref: "#/components/schemas/SCIMPatchRequest"
          application/json:
            schema:
              $ref: "#/components/schemas/SCIMPatchRequest"
      responses:
        "200":
          description: Patched SCIM group
          content:
            application/scim+json:
              schema:
                $ref: "#/components/schemas/SCIMGroup"
    delete:
      operationId: deleteSCIMGroup
      summary: Delete a SCIM group
      tags: [SCIM]
      security:
        - SCIMBearerAuth: []
      parameters:
        - $ref: "#/components/parameters/SCIMDirectoryID"
        - $ref: "#/components/parameters/SCIMGroupID"
      responses:
        "204":
          description: Group deleted

  /api/admin/clients/{id}/scim-directories:
    get:
      operationId: listSCIMDirectories
      summary: List SCIM directories
      tags: [SCIM, Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ClientIDPath"
      responses:
        "200":
          description: SCIM directories
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SCIMDirectoryListResponse"
    post:
      operationId: createSCIMDirectory
      summary: Create a SCIM directory and bearer token
      tags: [SCIM, Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ClientIDPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSCIMDirectoryRequest"
      responses:
        "201":
          description: SCIM directory and one-time token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SCIMDirectoryWithToken"

  /api/admin/clients/{id}/scim-directories/{scim_directory_id}:
    get:
      operationId: getSCIMDirectory
      summary: Get a SCIM directory
      tags: [SCIM, Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ClientIDPath"
        - $ref: "#/components/parameters/SCIMDirectoryID"
      responses:
        "200":
          description: SCIM directory
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SCIMDirectory"
    patch:
      operationId: updateSCIMDirectory
      summary: Update a SCIM directory
      tags: [SCIM, Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ClientIDPath"
        - $ref: "#/components/parameters/SCIMDirectoryID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateSCIMDirectoryRequest"
      responses:
        "200":
          description: Updated SCIM directory
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SCIMDirectory"

  /api/admin/clients/{id}/scim-directories/{scim_directory_id}/rotate-token:
    post:
      operationId: rotateSCIMDirectoryToken
      summary: Rotate a SCIM directory bearer token
      tags: [SCIM, Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ClientIDPath"
        - $ref: "#/components/parameters/SCIMDirectoryID"
      responses:
        "200":
          description: SCIM directory and replacement one-time token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SCIMDirectoryWithToken"

  # ──────────────────────────────────────────────
  # Admin
  # ──────────────────────────────────────────────
  /api/admin/auth/login:
    post:
      operationId: adminLogin
      summary: Sign in an admin user
      description: Password-based admin login. Admins with MFA enabled must include a valid `totp_code`; otherwise the response sets `mfa_required`.
      tags: [Admin]
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
                  format: password
                totp_code:
                  type: string
      responses:
        "200":
          description: Admin token
        "401":
          description: Invalid credentials or MFA required

  /api/admin/auth/sso:
    post:
      operationId: adminSSOLogin
      summary: Sign in an admin user with SSO
      description: Completes an externally verified admin SSO handoff by matching provider and subject to an admin identity.
      tags: [Admin]
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [provider, subject]
              properties:
                provider:
                  type: string
                subject:
                  type: string
      responses:
        "200":
          description: Admin token

  /api/admin/step-up/verify:
    post:
      operationId: verifyAdminStepUp
      summary: Verify an admin step-up challenge
      description: Exchanges an admin protected-action challenge token plus admin TOTP code for a short-lived `step_up_token`.
      tags: [Admin, Adaptive Security]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StepUpVerifyRequest"
      responses:
        "200":
          description: Step-up verified
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StepUpVerifyResponse"
        "403":
          description: Admin MFA enrollment required or step-up not available for break-glass keys
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/admin/users:
    get:
      operationId: listAdminUsers
      summary: List admin users
      tags: [Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      responses:
        "200":
          description: Admin users
    post:
      operationId: createAdminUser
      summary: Create an admin user
      description: Creates an admin identity with roles, all/client/organization scope, MFA settings, and optional SSO identity.
      tags: [Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      responses:
        "201":
          description: Admin user

  /api/admin/clients:
    post:
      operationId: createClient
      summary: Register a new client
      description: |
        Creates a new tenant (client) and returns the API key and JWT secret. **Save these values** — they are only shown at creation time.
      tags: [Admin]
      x-codeSamples:
        - lang: cURL
          label: Provision a production client
          source: |
            curl -X POST https://authservice.ayushojha.com/api/admin/clients \
              -H "X-Admin-Key: $AUTH_SERVICE_ADMIN_KEY" \
              -H "Content-Type: application/json" \
              -d '{
                "name": "Acme Dashboard",
                "slug": "acme-dashboard-prod",
                "allowed_origins": ["https://app.acme.com"],
                "webhook_url": "https://app.acme.com/webhooks/auth"
              }'
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateClientRequest"
            examples:
              productionWebApp:
                summary: Production web application client
                value:
                  name: Acme Dashboard
                  slug: acme-dashboard-prod
                  allowed_origins:
                    - https://app.acme.com
                    - https://www.acme.com
                  webhook_url: https://app.acme.com/webhooks/auth
              stagingEnvironment:
                summary: Separate staging client for safer testing
                value:
                  name: Acme Dashboard Staging
                  slug: acme-dashboard-staging
                  allowed_origins:
                    - https://staging.acme.com
      responses:
        "201":
          description: Client created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateClientResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Invalid admin key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    get:
      operationId: listClients
      summary: List all clients
      description: Returns all registered clients.
      tags: [Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      responses:
        "200":
          description: List of clients
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Client"
        "401":
          description: Invalid admin key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/admin/clients/{id}:
    get:
      operationId: getClient
      summary: Get a client by ID
      description: Returns a single client's details.
      tags: [Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Client details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Client"
        "401":
          description: Invalid admin key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Client not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    patch:
      operationId: updateClient
      summary: Update a client
      description: Updates mutable client settings such as allowed origins, webhook URL, status, and tenant WebAuthn policy controls.
      tags: [Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateClientRequest"
            example:
              settings:
                webauthn_attestation: direct
                webauthn_require_attestation: true
                webauthn_allowed_attestation_formats: [packed, tpm, apple]
      responses:
        "200":
          description: Client updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Client"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Invalid admin key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Client not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/admin/clients/{id}/security-policy:
    get:
      operationId: getClientSecurityPolicy
      summary: Get client adaptive security policy
      tags: [Admin, Adaptive Security]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ClientIDPath"
      responses:
        "200":
          description: Client security policy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdaptiveSecurityPolicy"
    put:
      operationId: replaceClientSecurityPolicy
      summary: Replace client adaptive security policy
      tags: [Admin, Adaptive Security]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ClientIDPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AdaptiveSecurityPolicy"
      responses:
        "200":
          description: Updated client security policy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdaptiveSecurityPolicy"
        "403":
          description: Step-up required or blocked by policy
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/AdaptiveActionDecision"
                  - $ref: "#/components/schemas/Error"
    patch:
      operationId: patchClientSecurityPolicy
      summary: Replace client adaptive security policy
      tags: [Admin, Adaptive Security]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ClientIDPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AdaptiveSecurityPolicy"
      responses:
        "200":
          description: Updated client security policy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdaptiveSecurityPolicy"

  /api/admin/clients/{id}/rotate-jwt:
    post:
      operationId: rotateJwtSecret
      summary: Rotate JWT secret (alias)
      description: |
        Alias for `/api/admin/clients/{id}/rotate-secret`. Generates a new JWT secret for the client. The old secret is immediately invalidated — all existing tokens signed with it will fail validation.
      tags: [Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: New JWT secret
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RotateSecretResponse"
        "401":
          description: Invalid admin key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/admin/clients/{id}/rotate-secret:
    post:
      operationId: rotateJwtSecretCanonical
      summary: Rotate JWT secret
      description: |
        Generates a new JWT secret for the client. The old secret is immediately invalidated — all existing tokens signed with it will fail validation.

        Use this during incident response, tenant ownership changes, or planned signing-secret rotation.
      tags: [Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: New JWT secret
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RotateSecretResponse"
        "401":
          description: Invalid admin key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/admin/clients/{id}/rotate-key:
    post:
      operationId: rotateApiKey
      summary: Rotate API key (alias)
      description: |
        Alias for `/api/admin/clients/{id}/rotate-api-key`. Generates a new API key for the client. The old key is immediately invalidated.
      tags: [Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: New API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RotateKeyResponse"
        "401":
          description: Invalid admin key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/admin/clients/{id}/rotate-api-key:
    post:
      operationId: rotateApiKeyCanonical
      summary: Rotate API key
      description: |
        Generates a new API key for the client. The old key is immediately invalidated.

        Use this if a client API key was exposed in an untrusted surface, checked into source control, or transferred to a new owner.
      tags: [Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: New API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RotateKeyResponse"
        "401":
          description: Invalid admin key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/admin/audit-events:
    get:
      operationId: listAuditEvents
      summary: Query audit events
      description: Returns authentication audit events, newest first. Use filters to narrow by tenant, user, or event type.
      tags: [Admin]
      x-codeSamples:
        - lang: cURL
          label: Review recent failed logins for a client
          source: |
            curl "https://authservice.ayushojha.com/api/admin/audit-events?client_id=$CLIENT_ID&event_type=login_failed&limit=100" \
              -H "X-Admin-Key: $AUTH_SERVICE_ADMIN_KEY"
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - name: client_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter events for a client/tenant
        - name: user_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter events for a user
        - name: event_type
          in: query
          required: false
          schema:
            type: string
          description: Filter by event type, for example `signup`, `login_success`, or `totp_enabled`
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 10000
            default: 50
          description: Maximum number of events to return
      responses:
        "200":
          description: Audit events
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuditEventsResponse"
        "400":
          description: Invalid query parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Invalid admin key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/admin/audit-events/export:
    get:
      operationId: exportAuditEvents
      summary: Export audit events
      description: Exports filtered audit events as CSV or newline-delimited JSON for security reviews, customer evidence requests, and offline retention.
      tags: [Admin]
      x-codeSamples:
        - lang: cURL
          label: Export recent audit evidence as CSV
          source: |
            curl "https://authservice.ayushojha.com/api/admin/audit-events/export?client_id=$CLIENT_ID&format=csv&limit=500" \
              -H "X-Admin-Key: $AUTH_SERVICE_ADMIN_KEY" \
              -o authservice-audit-events.csv
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - name: client_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter events for a client/tenant
        - name: user_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter events for a user
        - name: event_type
          in: query
          required: false
          schema:
            type: string
          description: Filter by event type
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 10000
            default: 50
          description: Maximum number of events to export
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum: [csv, jsonl, ndjson]
            default: csv
          description: Export format. `jsonl` and `ndjson` both return `application/x-ndjson`.
      responses:
        "200":
          description: Audit export
          content:
            text/csv:
              schema:
                type: string
            application/x-ndjson:
              schema:
                type: string
        "400":
          description: Invalid query parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Invalid admin key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/admin/audit-events/legal-hold:
    post:
      operationId: setAuditLegalHold
      summary: Set audit legal hold
      description: Enables or disables legal hold for specific audit event IDs. Legal hold prevents retention purge and is recorded as mutable operational metadata.
      tags: [Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AuditLegalHoldRequest"
      responses:
        "200":
          description: Legal hold update result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuditLegalHoldResult"

  /api/admin/audit-events/retention/purge:
    post:
      operationId: purgeExpiredAuditEvents
      summary: Purge expired audit events
      description: Dry-runs or deletes audit events whose `retention_until` has passed and `legal_hold` is false.
      tags: [Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AuditRetentionPurgeRequest"
      responses:
        "200":
          description: Purge result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuditRetentionPurgeResult"

  /api/admin/audit-events/chain/verify:
    get:
      operationId: verifyAuditChain
      summary: Verify audit event hash chain
      description: Recomputes audit event hashes for a client-scoped or global chain and reports tamper-evidence failures.
      tags: [Admin]
      security:
        - AdminBearerAuth: []
        - AdminKeyAuth: []
      parameters:
        - name: client_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Client chain to verify. Omit for the global/admin chain.
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100000
            default: 10000
      responses:
        "200":
          description: Chain verification result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuditChainVerification"

  # ──────────────────────────────────────────────
  # Utility
  # ──────────────────────────────────────────────
  /healthz:
    get:
      operationId: healthCheck
      summary: Health check
      description: Returns service health status.
      tags: [Utility]
      responses:
        "200":
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok

  /metrics:
    get:
      operationId: prometheusMetrics
      summary: Prometheus metrics
      description: Exposes operational metrics for login results, MFA challenge rate, SSO errors, SCIM lag, webhook delivery, token refresh reuse, audit streams, and HTTP latency.
      tags: [Utility]
      responses:
        "200":
          description: Prometheus text exposition
          content:
            text/plain:
              schema:
                type: string

  /.well-known/jwks.json:
    get:
      operationId: getJwks
      summary: Get JSON Web Key Set
      description: |
        Returns public signing keys used to validate RS256-signed tokens.

        Without a client hint, the endpoint returns the issuer-level active JWKS. Provide either `X-API-Key` or `client_id` to narrow the response to one client.
      tags: [Utility]
      x-codeSamples:
        - lang: cURL
          label: Fetch client-scoped signing keys
          source: |
            curl "https://authservice.ayushojha.com/.well-known/jwks.json?client_id=$CLIENT_ID"
      parameters:
        - name: X-API-Key
          in: header
          required: false
          schema:
            type: string
          description: Client API key for client-scoped JWKS lookup
        - name: client_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Client ID (alternative to X-API-Key header)
      responses:
        "200":
          description: JWKS
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JWKS"
        "401":
          description: Invalid client
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: JWKS could not be loaded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: Client API key — identifies the tenant
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: User access token (JWT)
    SCIMBearerAuth:
      type: http
      scheme: bearer
      description: Per-directory SCIM provisioning token
    AdminKeyAuth:
      type: apiKey
      in: header
      name: X-Admin-Key
      description: Break-glass master admin key for bootstrap or emergencies; prefer AdminBearerAuth
    AdminBearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Admin access token returned by `/api/admin/auth/login` or `/api/admin/auth/sso`

  parameters:
    ClientIDPath:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: Client ID
    OrganizationID:
      name: org_id
      in: path
      required: true
      schema:
        type: string
      description: Organization ID
    ServiceAccountID:
      name: service_account_id
      in: path
      required: true
      schema:
        type: string
      description: Service account ID used as OAuth client_id
    ServiceAccountKeyID:
      name: key_id
      in: path
      required: true
      schema:
        type: string
      description: Service account key ID
    SSOConnectionID:
      name: sso_connection_id
      in: path
      required: true
      schema:
        type: string
      description: Enterprise SSO connection ID or slug where supported
    SCIMDirectoryID:
      name: scim_directory_id
      in: path
      required: true
      schema:
        type: string
      description: SCIM directory ID
    SCIMUserID:
      name: scim_user_id
      in: path
      required: true
      schema:
        type: string
      description: SCIM user resource ID
    SCIMGroupID:
      name: scim_group_id
      in: path
      required: true
      schema:
        type: string
      description: SCIM group resource ID

  schemas:
    # ── Requests ──────────────────────────────
    SignupRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
          description: User email address
          example: user@example.com
        password:
          type: string
          minLength: 8
          maxLength: 72
          description: Password (8-72 characters, bcrypt limit)
          example: MySecurePass123
        display_name:
          type: string
          description: Display name (defaults to email prefix)
          example: John Doe
        session_mode:
          type: string
          enum: [token]
          description: Legacy alias for `token_transport=json`
        token_transport:
          type: string
          enum: [json, cookie]
          description: Use `json` for native/server clients or `cookie` for HttpOnly browser refresh cookies
        captcha_token:
          type: string
          description: Required when `CAPTCHA_SIGNUP_REQUIRED=true`

    LoginRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
          example: user@example.com
        password:
          type: string
          example: MySecurePass123
        session_mode:
          type: string
          enum: [token]
          description: Legacy alias for `token_transport=json`
        token_transport:
          type: string
          enum: [json, cookie]
          description: Use `json` for native/server clients or `cookie` for HttpOnly browser refresh cookies
        captcha_token:
          type: string
          description: Required when `CAPTCHA_LOGIN_REQUIRED=true`

    RefreshRequest:
      type: object
      properties:
        refresh_token:
          type: string
          description: Refresh token for JSON transport; `refreshToken` is accepted as a compatibility alias
        session_mode:
          type: string
          enum: [token]
          description: Legacy alias for `token_transport=json`
        token_transport:
          type: string
          enum: [json, cookie]
          description: Use `json` to return the rotated refresh token in JSON or `cookie` to rotate `auth_refresh`

    LogoutRequest:
      type: object
      properties:
        refresh_token:
          type: string
          description: Refresh token to revoke when using token session mode

    ChangePasswordRequest:
      type: object
      required: [old_password, new_password]
      properties:
        old_password:
          type: string
          description: Current password
        new_password:
          type: string
          minLength: 8
          maxLength: 72
          description: New password (8-72 characters)

    UpdateProfileRequest:
      type: object
      properties:
        display_name:
          type: string
          description: New display name
          example: Jane Doe
        timezone:
          type: string
          description: IANA timezone
          example: America/New_York

    ResetPasswordRequest:
      type: object
      required: [token, new_password]
      properties:
        token:
          type: string
          description: Reset token from email
        new_password:
          type: string
          minLength: 8
          maxLength: 72
          description: New password (8-72 characters)

    TOTPVerifyRequest:
      type: object
      required: [two_factor_token, code]
      properties:
        two_factor_token:
          type: string
          description: Token from login response when 2FA is required
        code:
          type: string
          description: 6-digit TOTP code from authenticator app
          example: "123456"
        session_mode:
          type: string
          enum: [token]
        remember_device:
          type: boolean
          description: Remember this device after successful MFA verification when policy allows trusted-device bypass
        device_name:
          type: string
          description: Optional friendly name for the remembered device

    RecoveryCodeVerifyRequest:
      type: object
      required: [two_factor_token, code]
      properties:
        two_factor_token:
          type: string
          description: Token from login response when 2FA is required
        code:
          type: string
          description: One-time recovery code generated after TOTP enrollment
          example: ABCD-1234-EF56
        session_mode:
          type: string
          enum: [token]

    StepUpVerifyRequest:
      type: object
      required: [challenge_token, code]
      properties:
        challenge_token:
          type: string
          description: Challenge token returned by a protected action response
        factor:
          type: string
          enum: [totp, recovery_code]
          default: totp
        code:
          type: string
          description: TOTP code or recovery code

    RecoveryCodesResponse:
      type: object
      properties:
        recovery_codes:
          type: array
          description: Newly generated one-time codes. Present only immediately after rotation.
          items:
            type: string
          example: [ABCD-1234-EF56, "9876-ABCD-4321"]
        unused_count:
          type: integer
          description: Count of unused recovery codes
          example: 10

    CreateOrganizationRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          maxLength: 128
          example: Acme Inc
        slug:
          type: string
          maxLength: 80
          description: URL-safe slug. Defaults to a normalized version of name.
          example: acme-inc

    UpdateOrganizationRequest:
      type: object
      properties:
        name:
          type: string
          maxLength: 128
          example: Acme Corp
        slug:
          type: string
          maxLength: 80
          example: acme-corp

    InviteOrganizationMemberRequest:
      type: object
      required: [email]
      properties:
        email:
          type: string
          format: email
          example: teammate@acme.com
        role:
          $ref: "#/components/schemas/OrganizationRole"
        permissions:
          type: array
          items:
            $ref: "#/components/schemas/OrganizationPermission"
          description: Optional custom permissions. Omit to use the role defaults.
        expires_in_hours:
          type: integer
          minimum: 1
          maximum: 720
          default: 168

    UpdateOrganizationMemberRequest:
      type: object
      required: [role]
      properties:
        role:
          $ref: "#/components/schemas/OrganizationRole"
        permissions:
          type: array
          items:
            $ref: "#/components/schemas/OrganizationPermission"
          description: Optional custom permissions. Omit to use the role defaults.

    AcceptOrganizationInvitationRequest:
      type: object
      required: [token]
      properties:
        token:
          type: string
          description: Raw invitation token returned at invite creation.

    CreateServiceAccountRequest:
      type: object
      required: [name, scopes]
      properties:
        name:
          type: string
          maxLength: 128
          example: Billing Worker
        description:
          type: string
          maxLength: 512
        scopes:
          type: array
          items:
            type: string
          example: ["invoices:read", "invoices:write"]

    UpdateServiceAccountRequest:
      type: object
      properties:
        name:
          type: string
          maxLength: 128
        description:
          type: string
          maxLength: 512
        scopes:
          type: array
          items:
            type: string
        status:
          type: string
          enum: [active, disabled]

    CreateServiceAccountKeyRequest:
      type: object
      properties:
        name:
          type: string
          maxLength: 128
          example: read-only deploy key
        scopes:
          type: array
          items:
            type: string
          description: Must be a subset of the parent service account scopes. Omit to inherit all account scopes.
        expires_in_hours:
          type: integer
          minimum: 1
          description: Optional key expiry.

    OIDCConnectionConfig:
      type: object
      properties:
        issuer:
          type: string
          format: uri
          example: https://acme.okta.com/oauth2/default
        client_id:
          type: string
        client_secret:
          type: string
          writeOnly: true
        scopes:
          type: array
          items:
            type: string
          default: [openid, email, profile]

    SAMLConnectionConfig:
      type: object
      properties:
        idp_entity_id:
          type: string
          example: https://idp.example.com/metadata
        idp_sso_url:
          type: string
          format: uri
        idp_certificate:
          type: string
          description: Base64 DER X.509 signing certificate. PEM input is accepted by the API and normalized.
        idp_metadata_xml:
          type: string
          description: Optional IdP metadata XML. If supplied, entity ID, SSO URL, and signing certs can be read from metadata.
        sp_entity_id:
          type: string
          description: Service provider entity ID. Defaults to the generated metadata URL.
        sp_metadata_url:
          type: string
          format: uri
          readOnly: true
        acs_url:
          type: string
          format: uri
          readOnly: true
        sp_certificate_pem:
          type: string
          readOnly: true
          description: Generated SP certificate to upload through metadata when needed.

    CreateEnterpriseSSOConnectionRequest:
      type: object
      required: [name, protocol]
      properties:
        name:
          type: string
          example: Acme Okta
        slug:
          type: string
          example: acme-okta
        protocol:
          type: string
          enum: [oidc, saml]
        status:
          type: string
          enum: [active, inactive]
          default: active
        domains:
          type: array
          items:
            type: string
          example: [acme.com]
        enforce_for_domains:
          type: boolean
          default: false
        oidc:
          $ref: "#/components/schemas/OIDCConnectionConfig"
        saml:
          $ref: "#/components/schemas/SAMLConnectionConfig"
        attribute_mapping:
          type: object
          additionalProperties:
            type: string
          description: Maps local fields such as `email`, `name`, and `external_id` to SAML attribute names.

    UpdateEnterpriseSSOConnectionRequest:
      type: object
      properties:
        name:
          type: string
        slug:
          type: string
        status:
          type: string
          enum: [active, inactive]
        domains:
          type: array
          items:
            type: string
        enforce_for_domains:
          type: boolean
        oidc:
          $ref: "#/components/schemas/OIDCConnectionConfig"
        saml:
          $ref: "#/components/schemas/SAMLConnectionConfig"
        attribute_mapping:
          type: object
          additionalProperties:
            type: string

    SAMLCallbackRequest:
      type: object
      required: [SAMLResponse, RelayState]
      properties:
        SAMLResponse:
          type: string
          description: Base64-encoded SAML response from the IdP.
        RelayState:
          type: string
          description: Opaque state returned from the SAML AuthnRequest.

    CreateSCIMDirectoryRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          example: Acme Directory
        domains:
          type: array
          items:
            type: string
          description: Optional allowed email domains for provisioned users.

    UpdateSCIMDirectoryRequest:
      type: object
      properties:
        name:
          type: string
        status:
          type: string
          enum: [active, disabled]
        domains:
          type: array
          items:
            type: string

    SCIMPatchRequest:
      type: object
      required: [Operations]
      properties:
        schemas:
          type: array
          items:
            type: string
        Operations:
          type: array
          items:
            type: object
            required: [op]
            properties:
              op:
                type: string
                enum: [add, replace, remove]
              path:
                type: string
              value: {}

    ClientCredentialsRequest:
      type: object
      required: [grant_type, client_id, client_secret]
      properties:
        grant_type:
          type: string
          enum: [client_credentials]
        client_id:
          type: string
          description: Service account ID.
        client_secret:
          type: string
          description: One-time secret returned when creating or rotating a service account key.
        scope:
          type: string
          description: Space-delimited requested scopes. Omit to request all allowed scopes.
          example: invoices:read

    TokenIntrospectionRequest:
      type: object
      required: [token]
      properties:
        token:
          type: string
        client_id:
          type: string
          description: Service account ID. May be supplied through HTTP Basic auth instead.
        client_secret:
          type: string
          description: Service account secret. May be supplied through HTTP Basic auth instead.

    CreateClientRequest:
      type: object
      required: [name, slug]
      properties:
        name:
          type: string
          description: Human-readable project name
          example: My SaaS App
        slug:
          type: string
          description: Unique identifier (URL-safe)
          example: my-saas-app
        allowed_origins:
          type: array
          items:
            type: string
          description: Allowed CORS origins
          example: ["https://myapp.com", "https://www.myapp.com"]
        webhook_url:
          type: string
          format: uri
          description: Webhook URL for signed audit-event deliveries when `WEBHOOK_SIGNING_SECRET` is configured
          example: https://myapp.com/webhooks/auth
        settings:
          type: object
          description: Tenant settings. WebAuthn policy keys include `webauthn_attestation`, `webauthn_require_attestation`, and `webauthn_allowed_attestation_formats`.
          additionalProperties: true

    UpdateClientRequest:
      type: object
      properties:
        name:
          type: string
        allowed_origins:
          type: array
          items:
            type: string
        webhook_url:
          type: string
          format: uri
        status:
          type: string
          enum: [active, suspended]
        settings:
          type: object
          additionalProperties: true
          description: Replaces the client settings object when supplied.

    # ── Responses ─────────────────────────────
    OkResponse:
      type: object
      properties:
        ok:
          type: string
          example: "true"

    Error:
      type: object
      properties:
        error:
          type: string
          description: Backwards-compatible human-readable error message
          example: Invalid email or password.
        code:
          type: string
          description: Stable machine-readable error code
          example: invalid_credentials
        message:
          type: string
          description: Human-readable error message safe to show to a user or operator
          example: Invalid email or password.
        request_id:
          type: string
          description: Echoes `X-Request-ID` when supplied
          example: req_123

    AuthResponse:
      type: object
      properties:
        access_token:
          type: string
          description: JWT access token (15 min TTL)
        token_type:
          type: string
          example: Bearer
        expires_in:
          type: integer
          example: 900
          description: Token lifetime in seconds
        user:
          $ref: "#/components/schemas/User"
        refresh_token:
          type: string
          description: Refresh token (only present when `token_transport=json` or legacy `session_mode=token` is requested)
        refresh:
          type: object
          description: Refresh-token transport metadata; always present when a new refresh credential is issued
          properties:
            transport:
              type: string
              enum: [json, cookie]
            cookie_name:
              type: string
              example: auth_refresh
            expires_in:
              type: integer
              example: 604800
        risk:
          $ref: "#/components/schemas/LoginRisk"

    TwoFactorChallenge:
      type: object
      properties:
        requires_2fa:
          type: boolean
          example: true
        two_factor_token:
          type: string
          description: Pass this to the TOTP verify endpoint
        two_factor_methods:
          type: array
          items:
            type: string
          example: ["totp", "recovery_code"]
        risk:
          $ref: "#/components/schemas/LoginRisk"

    LoginRisk:
      type: object
      description: Present when a login is considered suspicious and should be surfaced to the user or security team.
      properties:
        level:
          type: string
          enum: [low, medium, high, critical]
          example: medium
        score:
          type: integer
          minimum: 0
          maximum: 100
        reasons:
          type: array
          items:
            type: string
          example: [new_ip, new_device, failed_velocity]
        new_ip:
          type: boolean
        new_device:
          type: boolean
        signals:
          type: array
          items:
            $ref: "#/components/schemas/RiskSignal"
        context:
          type: object
          additionalProperties: true

    RiskSignal:
      type: object
      properties:
        type:
          type: string
          example: new_device
        level:
          type: string
          enum: [low, medium, high, critical]
        reason:
          type: string
        metadata:
          type: object
          additionalProperties: true

    RiskAssessment:
      type: object
      properties:
        level:
          type: string
          enum: [low, medium, high, critical]
        score:
          type: integer
        signals:
          type: array
          items:
            $ref: "#/components/schemas/RiskSignal"
        context:
          type: object
          additionalProperties: true

    AdaptiveSecurityPolicy:
      type: object
      description: Tenant or organization security policy for MFA, risk, protected actions, and alerts.
      properties:
        mfa:
          type: object
          properties:
            mode:
              type: string
              enum: [off, allow, required, adaptive]
            factors:
              type: array
              items:
                type: string
                enum: [totp, recovery_code]
            challenge_risk_level:
              type: string
              enum: [low, medium, high, critical]
            block_risk_level:
              type: string
              enum: [low, medium, high, critical]
            remember_device_days:
              type: integer
            trusted_device_bypass:
              type: boolean
            enrollment_required:
              type: boolean
        risk:
          type: object
          properties:
            challenge_level:
              type: string
              enum: [low, medium, high, critical]
            block_level:
              type: string
              enum: [low, medium, high, critical]
            failed_velocity_threshold:
              type: integer
            trusted_ip_cidrs:
              type: array
              items:
                type: string
            blocked_ip_cidrs:
              type: array
              items:
                type: string
            tor_ip_cidrs:
              type: array
              items:
                type: string
            vpn_asns:
              type: array
              items:
                type: integer
            high_risk_asns:
              type: array
              items:
                type: integer
        actions:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/StepUpActionPolicy"
        alerts:
          type: object
          properties:
            risk_levels:
              type: array
              items:
                type: string
                enum: [low, medium, high, critical]
            event_types:
              type: array
              items:
                type: string

    StepUpActionPolicy:
      type: object
      properties:
        mode:
          type: string
          enum: [off, notify, challenge, block]
        factors:
          type: array
          items:
            type: string
            enum: [totp, recovery_code]
        risk_challenge_level:
          type: string
          enum: [low, medium, high, critical]
        risk_block_level:
          type: string
          enum: [low, medium, high, critical]
        max_age_seconds:
          type: integer

    StepUpVerifyResponse:
      type: object
      properties:
        step_up_token:
          type: string
        action:
          type: string
        expires_at:
          type: string
          format: date-time

    AdaptiveActionDecision:
      type: object
      properties:
        error:
          type: string
          enum: [step_up_required, blocked_by_security_policy]
        action:
          type: string
        step_up_required:
          type: boolean
        challenge_token:
          type: string
        factors:
          type: array
          items:
            type: string
        expires_in:
          type: integer
        risk:
          $ref: "#/components/schemas/RiskAssessment"

    UserDevice:
      type: object
      properties:
        id:
          type: string
          format: uuid
        client_id:
          type: string
        user_id:
          type: string
        name:
          type: string
        user_agent:
          type: string
        ip_address:
          type: string
        trusted:
          type: boolean
        remembered:
          type: boolean
        trust_expires_at:
          type: string
          format: date-time
          nullable: true
        last_seen_at:
          type: string
          format: date-time

    User:
      type: object
      properties:
        id:
          type: string
          format: uuid
        client_id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        email_verified:
          type: boolean
        display_name:
          type: string
        avatar_url:
          type: string
          nullable: true
        timezone:
          type: string
          example: UTC
        locale:
          type: string
          example: en
        role:
          type: string
          example: user
        status:
          type: string
          enum: [active, suspended, deleted]
        totp_enabled:
          type: boolean
        last_login_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Session:
      type: object
      properties:
        id:
          type: string
        user_id:
          type: string
        client_id:
          type: string
        user_agent:
          type: string
        ip_address:
          type: string
        expires_at:
          type: string
          format: date-time
        revoked:
          type: boolean
        created_at:
          type: string
          format: date-time

    SessionListResponse:
      type: object
      properties:
        sessions:
          type: array
          items:
            $ref: "#/components/schemas/Session"

    OrganizationRole:
      type: string
      description: Built-in roles are `owner`, `admin`, `member`, and `viewer`. Custom role keys such as `org:billing-admin` are accepted when paired with explicit permissions.
      default: member
      pattern: "^[a-z0-9][a-z0-9:_.-]{1,126}[a-z0-9]$"
      examples:
        - owner
        - org:billing-admin

    OrganizationPermission:
      type: string
      description: Built-in permissions plus custom lower-case `namespace:action` permission keys. Omit a permissions list to use the built-in defaults for built-in roles.
      pattern: "^[a-z0-9][a-z0-9:_.-]{1,126}[a-z0-9]$"
      examples:
        - org:read
        - billing:manage

    Organization:
      type: object
      properties:
        id:
          type: string
        client_id:
          type: string
        name:
          type: string
        slug:
          type: string
        metadata:
          type: object
          additionalProperties: true
        created_by_user_id:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    OrganizationMembership:
      type: object
      properties:
        id:
          type: string
        client_id:
          type: string
        organization_id:
          type: string
        user_id:
          type: string
        role:
          $ref: "#/components/schemas/OrganizationRole"
        permissions:
          type: array
          items:
            $ref: "#/components/schemas/OrganizationPermission"
        status:
          type: string
          enum: [active]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    OrganizationMembershipDetails:
      type: object
      properties:
        organization:
          $ref: "#/components/schemas/Organization"
        membership:
          $ref: "#/components/schemas/OrganizationMembership"

    OrganizationInvitation:
      type: object
      properties:
        id:
          type: string
        client_id:
          type: string
        organization_id:
          type: string
        email:
          type: string
          format: email
        role:
          $ref: "#/components/schemas/OrganizationRole"
        permissions:
          type: array
          items:
            $ref: "#/components/schemas/OrganizationPermission"
        status:
          type: string
          enum: [pending, accepted, revoked]
        invited_by_user_id:
          type: string
          nullable: true
        expires_at:
          type: string
          format: date-time
        accepted_at:
          type: string
          format: date-time
          nullable: true
        revoked_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    OrganizationInvitationWithToken:
      type: object
      properties:
        invitation:
          $ref: "#/components/schemas/OrganizationInvitation"
        token:
          type: string
          description: Raw invitation token. Returned once.

    OrganizationListResponse:
      type: object
      properties:
        organizations:
          type: array
          items:
            $ref: "#/components/schemas/OrganizationMembershipDetails"

    OrganizationMembersResponse:
      type: object
      properties:
        members:
          type: array
          items:
            $ref: "#/components/schemas/OrganizationMembership"

    OrganizationInvitationsResponse:
      type: object
      properties:
        invitations:
          type: array
          items:
            $ref: "#/components/schemas/OrganizationInvitation"

    OrganizationTokenResponse:
      type: object
      properties:
        access_token:
          type: string
          description: JWT containing organization claims
        token_type:
          type: string
          example: Bearer
        expires_in:
          type: integer
          example: 900

    EnterpriseSSOConnection:
      type: object
      properties:
        id:
          type: string
        client_id:
          type: string
        name:
          type: string
        slug:
          type: string
        protocol:
          type: string
          enum: [oidc, saml]
        status:
          type: string
          enum: [active, inactive]
        domains:
          type: array
          items:
            type: string
        enforce_for_domains:
          type: boolean
        oidc:
          $ref: "#/components/schemas/OIDCConnectionConfig"
        saml:
          $ref: "#/components/schemas/SAMLConnectionConfig"
        attribute_mapping:
          type: object
          additionalProperties:
            type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    EnterpriseSSOConnectionListResponse:
      type: array
      items:
        $ref: "#/components/schemas/EnterpriseSSOConnection"

    SCIMDirectory:
      type: object
      properties:
        id:
          type: string
        client_id:
          type: string
        name:
          type: string
        status:
          type: string
          enum: [active, disabled]
        token_prefix:
          type: string
          description: Non-secret token prefix for identifying the active SCIM token.
        domains:
          type: array
          items:
            type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    SCIMDirectoryWithToken:
      type: object
      properties:
        directory:
          $ref: "#/components/schemas/SCIMDirectory"
        token:
          type: string
          description: Raw SCIM bearer token. Returned once.

    SCIMDirectoryListResponse:
      type: array
      items:
        $ref: "#/components/schemas/SCIMDirectory"

    SCIMUser:
      type: object
      properties:
        schemas:
          type: array
          items:
            type: string
          example: ["urn:ietf:params:scim:schemas:core:2.0:User"]
        id:
          type: string
          readOnly: true
        externalId:
          type: string
        userName:
          type: string
          format: email
        active:
          type: boolean
        displayName:
          type: string
        name:
          type: object
          properties:
            formatted:
              type: string
            givenName:
              type: string
            familyName:
              type: string
        emails:
          type: array
          items:
            type: object
            properties:
              value:
                type: string
                format: email
              type:
                type: string
              primary:
                type: boolean
        meta:
          $ref: "#/components/schemas/SCIMMeta"

    SCIMGroup:
      type: object
      properties:
        schemas:
          type: array
          items:
            type: string
          example: ["urn:ietf:params:scim:schemas:core:2.0:Group"]
        id:
          type: string
          readOnly: true
        externalId:
          type: string
        displayName:
          type: string
        members:
          type: array
          items:
            type: object
            properties:
              value:
                type: string
              $ref:
                type: string
              display:
                type: string
        meta:
          $ref: "#/components/schemas/SCIMMeta"

    SCIMMeta:
      type: object
      properties:
        resourceType:
          type: string
        created:
          type: string
          format: date-time
        lastModified:
          type: string
          format: date-time
        location:
          type: string

    SCIMListResponse:
      type: object
      properties:
        schemas:
          type: array
          items:
            type: string
        totalResults:
          type: integer
        startIndex:
          type: integer
        itemsPerPage:
          type: integer
        Resources:
          type: array
          items:
            oneOf:
              - $ref: "#/components/schemas/SCIMUser"
              - $ref: "#/components/schemas/SCIMGroup"

    ServiceAccount:
      type: object
      properties:
        id:
          type: string
        client_id:
          type: string
        name:
          type: string
        description:
          type: string
        scopes:
          type: array
          items:
            type: string
        status:
          type: string
          enum: [active, disabled]
        last_used_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ServiceAccountKey:
      type: object
      properties:
        id:
          type: string
        client_id:
          type: string
        service_account_id:
          type: string
        name:
          type: string
        key_prefix:
          type: string
          description: Non-secret prefix for identifying keys in logs and dashboards.
        scopes:
          type: array
          items:
            type: string
        status:
          type: string
          enum: [active, revoked]
        last_used_at:
          type: string
          format: date-time
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        revoked_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ServiceAccountKeyWithSecret:
      type: object
      properties:
        service_account:
          $ref: "#/components/schemas/ServiceAccount"
        key:
          $ref: "#/components/schemas/ServiceAccountKey"
        client_id:
          type: string
          description: Service account ID to send as OAuth client_id.
        client_secret:
          type: string
          description: Raw secret. Returned once and never stored in plaintext.

    ServiceAccountListResponse:
      type: object
      properties:
        service_accounts:
          type: array
          items:
            $ref: "#/components/schemas/ServiceAccount"

    ServiceAccountKeyListResponse:
      type: object
      properties:
        keys:
          type: array
          items:
            $ref: "#/components/schemas/ServiceAccountKey"

    M2MTokenResponse:
      type: object
      properties:
        access_token:
          type: string
        token_type:
          type: string
          example: Bearer
        expires_in:
          type: integer
          example: 900
        scope:
          type: string
          example: invoices:read

    M2MIntrospectionResponse:
      type: object
      properties:
        active:
          type: boolean
        client_id:
          type: string
        sub:
          type: string
        token_use:
          type: string
          enum: [client_credentials]
        scope:
          type: string
        scopes:
          type: array
          items:
            type: string
        service_account_id:
          type: string
        service_account_name:
          type: string
        exp:
          type: integer
          format: int64
        iat:
          type: integer
          format: int64
        jti:
          type: string
        error:
          type: string

    Client:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
        allowed_origins:
          type: array
          items:
            type: string
        webhook_url:
          type: string
          nullable: true
          description: Receives signed `audit.event` webhooks when webhook delivery is enabled
        settings:
          type: object
        status:
          type: string
          enum: [active, suspended]
        token_mode:
          type: string
          enum: [v1_hs256, v2_jwks]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    CreateClientResponse:
      type: object
      properties:
        client:
          $ref: "#/components/schemas/Client"
        api_key:
          type: string
          description: Client API key — save this, shown only at creation
        jwt_secret:
          type: string
          description: JWT signing secret — save this, shown only at creation

    RotateSecretResponse:
      type: object
      properties:
        client:
          $ref: "#/components/schemas/Client"
        jwt_secret:
          type: string
          description: New JWT secret

    RotateKeyResponse:
      type: object
      properties:
        client:
          $ref: "#/components/schemas/Client"
        api_key:
          type: string
          description: New API key

    AuditEventsResponse:
      type: object
      properties:
        events:
          type: array
          items:
            $ref: "#/components/schemas/AuditEvent"

    AuditEvent:
      type: object
      properties:
        id:
          type: integer
          format: int64
        client_id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
          nullable: true
        event_type:
          type: string
          example: login_success
        ip_address:
          type: string
          example: 203.0.113.10
        user_agent:
          type: string
        metadata:
          type: object
          additionalProperties: true
        retention_until:
          type: string
          format: date-time
          nullable: true
        legal_hold:
          type: boolean
        legal_hold_reason:
          type: string
        chain_scope:
          type: string
        previous_hash:
          type: string
        event_hash:
          type: string
        hash_algorithm:
          type: string
        created_at:
          type: string
          format: date-time

    AuditLegalHoldRequest:
      type: object
      required: [event_ids, legal_hold]
      properties:
        event_ids:
          type: array
          items:
            type: integer
            format: int64
        legal_hold:
          type: boolean
        reason:
          type: string

    AuditLegalHoldResult:
      type: object
      properties:
        updated:
          type: integer
          format: int64

    AuditRetentionPurgeRequest:
      type: object
      properties:
        before:
          type: string
          format: date-time
        dry_run:
          type: boolean
          default: true

    AuditRetentionPurgeResult:
      type: object
      properties:
        matched:
          type: integer
          format: int64
        deleted:
          type: integer
          format: int64
        dry_run:
          type: boolean

    AuditChainVerification:
      type: object
      properties:
        scope:
          type: string
        checked:
          type: integer
        valid:
          type: boolean
        legacy_unhashed:
          type: integer
        starts_with_anchor_hash:
          type: boolean
        first_event_id:
          type: integer
          format: int64
        last_event_id:
          type: integer
          format: int64
        last_hash:
          type: string
        failures:
          type: array
          items:
            type: object
            properties:
              event_id:
                type: integer
                format: int64
              problem:
                type: string
              expected:
                type: string
              actual:
                type: string

    TOTPSetupResponse:
      type: object
      properties:
        secret:
          type: string
          description: Base32-encoded TOTP secret
        uri:
          type: string
          description: "otpauth:// URI for QR code generation"
        qr:
          type: string
          description: Base64-encoded PNG QR code image

    Passkey:
      type: object
      properties:
        id:
          type: string
          description: Credential ID
        public_key:
          type: string
        counter:
          type: integer
        created_at:
          type: string
          format: date-time

    PasskeyCreationOptions:
      type: object
      description: WebAuthn PublicKeyCredentialCreationOptions — pass to navigator.credentials.create()
      properties:
        public_key:
          type: object
          properties:
            challenge:
              type: string
              description: Base64url-encoded challenge
            rp:
              type: object
              properties:
                id:
                  type: string
                name:
                  type: string
            user:
              type: object
              properties:
                id:
                  type: string
                name:
                  type: string
                displayName:
                  type: string
            pubKeyCredParams:
              type: array
              items:
                type: object
                properties:
                  alg:
                    type: integer
                  type:
                    type: string
            timeout:
              type: integer
            attestation:
              type: string

    PasskeyRequestOptions:
      type: object
      description: WebAuthn PublicKeyCredentialRequestOptions — pass to navigator.credentials.get()
      properties:
        public_key:
          type: object
          properties:
            challenge:
              type: string
            timeout:
              type: integer
            rpId:
              type: string
            userVerification:
              type: string
            allowCredentials:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                  type:
                    type: string
        session_id:
          type: string
          description: Pass this to the login/finish endpoint

    WebAuthnAttestationResponse:
      type: object
      description: Browser attestation response from navigator.credentials.create()
      properties:
        id:
          type: string
        rawId:
          type: string
        response:
          type: object
          properties:
            clientDataJSON:
              type: string
            attestationObject:
              type: string
        type:
          type: string
          example: public-key

    WebAuthnAssertionResponse:
      type: object
      description: Browser assertion response from navigator.credentials.get()
      properties:
        id:
          type: string
        rawId:
          type: string
        response:
          type: object
          properties:
            clientDataJSON:
              type: string
            authenticatorData:
              type: string
            signature:
              type: string
        type:
          type: string
          example: public-key

    JWKS:
      type: object
      properties:
        keys:
          type: array
          items:
            type: object
            properties:
              kty:
                type: string
                example: RSA
              kid:
                type: string
              use:
                type: string
                example: sig
              alg:
                type: string
                example: RS256
              "n":
                type: string
                description: Base64url-encoded RSA modulus
              e:
                type: string
                description: Base64url-encoded RSA exponent
