openapi: 3.1.0
info:
  title: Campuzz Accounts API
  version: "1.0.0"
  description: |
    Referência completa dos endpoints públicos do **api.campuzz.com.br**.

    Esta API combina:
    - **Better Auth 1.6.x** (`/api/auth/*`) — login email+senha, magic-link, email-OTP, SMS (Twilio), 2FA, passkey, multi-session
    - **JWT/SSO** (`/api/auth/jwks`, `/api/auth/token`) — emissão de JWTs Ed25519 para painéis externos
    - **Admin** (`/api/accounts/*`, `/api/admin/*`) — gestão de usuários, organizações, auditoria, alertas, feature flags
    - **IAM Bearer** (`/api/me`, `/api/iam/*`) — consumo por painéis externos via JWT
    - **Webhooks** (`/api/integrations/*`) — recebimento de eventos externos (SendGrid)

    ## Autenticação

    | Tipo     | Header / Cookie                          | Quando usar                                  |
    |----------|-------------------------------------------|----------------------------------------------|
    | público  | —                                         | endpoints abertos (sign-in, jwks, health)    |
    | cookie   | `campuzz.session_token` (HttpOnly, SameSite=Lax) | usuários logados em accounts.campuzz.com.br |
    | bearer   | `Authorization: Bearer <jwt>`             | painéis externos consumindo `/api/me` e `/api/iam/*` |
    | admin    | cookie + `role=admin`                     | rotas administrativas                        |
    | hmac     | `X-Campuzz-Signature: sha256=<hex>`       | webhooks de checkout                         |

    ## Feature flags

    Endpoints marcados com **`*`** são gated por feature flag — quando `enabled=false`,
    devolvem `503 {"error":"feature_disabled","flag":"auth.<key>"}`. Toggle via
    `PATCH /api/admin/feature-flags/:key` propaga em ≤ 30s entre isolates.

    ## Erros

    Resposta padrão: `{"error":"<code>","message":"<human readable>"}`.
    Veja a seção **Códigos de erro** ao fim deste documento.
  contact:
    name: Campuzz Suporte
    email: contato@pablobaptista.com.br
    url: https://campuzz.com.br
  license:
    name: Proprietário — uso interno e parceiros autorizados

servers:
  - url: https://api.campuzz.com.br
    description: Produção
  - url: https://api-staging.campuzz.com.br
    description: Staging
  - url: http://localhost:8787
    description: Dev local

tags:
  - name: Auth — Sign-in / Sign-up
    description: Login, signup, sessão e reset de senha (Better Auth `email` + `magicLink`).
  - name: Auth — Email OTP
    description: Login passwordless com código por email. **Gated por `auth.email_otp`.**
  - name: Auth — Phone (SMS)
    description: Verificação de telefone via Twilio. **Gated por `auth.phone_number`.**
  - name: Auth — Two-Factor
    description: TOTP + backup codes (plugin `twoFactor`).
  - name: Auth — Passkey
    description: WebAuthn (plugin `passkey`).
  - name: Auth — Multi-session
    description: Múltiplas sessões simultâneas. **Gated por `auth.multi_session`.**
  - name: JWT / SSO
    description: JWKS público, emissão de JWT, whitelist de origins, audit do handoff.
  - name: Admin — Usuários
    description: CRUD de usuários, roles, sessões, audit-trail.
  - name: Admin — Convites
    description: Criação e gestão de convites.
  - name: Admin — Organizações
    description: Plugin Better Auth `organization`.
  - name: Admin — Auditoria
    description: Listagem da trilha global de auditoria.
  - name: Admin — Alertas
    description: Alertas operacionais e ack.
  - name: Admin — Dashboards SSO
    description: Whitelist de origins permitidas em `returnTo`.
  - name: Admin — Feature Flags
    description: Toggle on/off dos plugins gated.
  - name: IAM — Self
    description: Perfil do próprio usuário consumido por painéis externos via Bearer JWT.
  - name: IAM — Read-only
    description: Listagem de usuários e checagem de permissões para painéis externos.
  - name: Public utilities
    description: Healthcheck, flags públicos, HIBP check.
  - name: Webhooks
    description: Eventos entrantes de provedores externos.

security:
  - sessionCookie: []

components:
  securitySchemes:
    sessionCookie:
      type: apiKey
      in: cookie
      name: campuzz.session_token
      description: Cookie HttpOnly emitido por Better Auth após sign-in. Domínio `.campuzz.com.br`.
    bearerJWT:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        JWT Ed25519 emitido por `GET /api/auth/token`. TTL 15 min.
        Validar contra `GET /api/auth/jwks` (audience `campuzz-dashboards`,
        issuer `https://api.campuzz.com.br`).
    hmacSignature:
      type: apiKey
      in: header
      name: X-Campuzz-Signature
      description: HMAC-SHA256(CHECKOUT_WEBHOOK_SECRET, raw_body). Formato `sha256=<hex>`.
    turnstileToken:
      type: apiKey
      in: header
      name: X-Turnstile-Token
      description: Token Cloudflare Turnstile. Obrigatório quando `auth.captcha=true`.

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
          example: invalid_token
        message:
          type: string
          nullable: true
          example: JWT expired at 2026-04-25T12:00:00Z
        code:
          type: string
          nullable: true
        flag:
          type: string
          nullable: true
          description: Presente quando `error=feature_disabled`.

    UserStatus:
      type: string
      enum: [invited, pending_verification, active, inactive, blocked, locked, deleted]

    UserRole:
      type: string
      enum: [user, admin]

    User:
      type: object
      properties:
        id: { type: string, format: uuid }
        email: { type: string, format: email }
        name: { type: string }
        image: { type: string, nullable: true }
        role: { $ref: '#/components/schemas/UserRole' }
        status: { $ref: '#/components/schemas/UserStatus' }
        emailVerified: { type: boolean }
        twoFactorEnabled: { type: boolean }
        banned: { type: boolean }
        banReason: { type: string, nullable: true }
        banExpires: { type: string, format: date-time, nullable: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
      required: [id, email, name, role, status]

    UserProfile:
      type: object
      properties:
        user_id: { type: string, format: uuid }
        cpf: { type: string, nullable: true }
        phone: { type: string, nullable: true }
        phone_verified: { type: boolean }
        locale: { type: string, default: pt-BR }
        timezone: { type: string, default: America/Sao_Paulo }
        primary_organization_id: { type: string, format: uuid, nullable: true }
        metadata: { type: object, additionalProperties: true }

    Session:
      type: object
      properties:
        id: { type: string }
        userId: { type: string, format: uuid }
        ipAddress: { type: string, nullable: true }
        userAgent: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
        expiresAt: { type: string, format: date-time }

    Organization:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        slug: { type: string }
        logo: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }

    JWTPayload:
      type: object
      description: Estrutura do JWT emitido por `/api/auth/token`.
      properties:
        sub: { type: string, description: User id }
        email: { type: string, format: email }
        name: { type: string }
        image: { type: string, nullable: true }
        role: { $ref: '#/components/schemas/UserRole' }
        status: { $ref: '#/components/schemas/UserStatus' }
        sid: { type: string, description: Session id }
        iss: { type: string, example: "https://api.campuzz.com.br" }
        aud: { type: string, example: "campuzz-dashboards" }
        exp: { type: integer, format: int64 }
        iat: { type: integer, format: int64 }

    JWKS:
      type: object
      properties:
        keys:
          type: array
          items:
            type: object
            properties:
              kty: { type: string, example: OKP }
              crv: { type: string, example: Ed25519 }
              x: { type: string }
              kid: { type: string }
              use: { type: string, example: sig }
              alg: { type: string, example: EdDSA }

    DashboardOrigin:
      type: object
      properties:
        id: { type: string, format: uuid }
        label: { type: string }
        origin: { type: string, format: uri, example: "https://dash.cliente.com" }
        organization_id: { type: string, format: uuid, nullable: true }
        enabled: { type: boolean }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    FeatureFlag:
      type: object
      properties:
        key:
          type: string
          enum:
            - auth.email_otp
            - auth.phone_number
            - auth.captcha
            - auth.hibp_warn
            - auth.multi_session
        enabled: { type: boolean }
        configured:
          type: boolean
          description: Indica se os secrets necessários estão setados (Twilio/Turnstile).
        description: { type: string }
        config: { type: object, additionalProperties: true }
        updated_at: { type: string, format: date-time }
        updated_by_email: { type: string, nullable: true }

    AuditEvent:
      type: object
      properties:
        id: { type: string, format: uuid }
        event_type: { type: string, example: "user.role_changed" }
        severity:
          type: string
          enum: [baixa, media, alta, critica]
        result:
          type: string
          enum: [success, fail]
        actor_user_id: { type: string, format: uuid, nullable: true }
        actor_email: { type: string, nullable: true }
        target_user_id: { type: string, format: uuid, nullable: true }
        target_email: { type: string, nullable: true }
        ip: { type: string, nullable: true }
        user_agent: { type: string, nullable: true }
        metadata: { type: object, additionalProperties: true }
        created_at: { type: string, format: date-time }

    Alert:
      type: object
      properties:
        id: { type: string, format: uuid }
        alert_type: { type: string, example: "captcha_failure_burst" }
        priority:
          type: string
          enum: [low, medium, high, critical]
        state:
          type: string
          enum: [open, acked, resolved]
        title: { type: string }
        details: { type: object, additionalProperties: true }
        created_at: { type: string, format: date-time }
        acked_at: { type: string, format: date-time, nullable: true }
        acked_by_email: { type: string, nullable: true }

    Permission:
      type: object
      properties:
        resource: { type: string, example: "user" }
        action: { type: string, example: "read" }
        scope: { type: string, nullable: true, example: "org_xxx" }

    PermissionCheckResult:
      type: object
      properties:
        allowed: { type: boolean }
        via:
          type: array
          items: { type: string }
          description: Roles/privilégios que concederam acesso.
        effective:
          type: array
          items: { $ref: '#/components/schemas/Permission' }

    PaginatedUsers:
      type: object
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/User' }
        page: { type: integer }
        pageSize: { type: integer }
        total: { type: integer }

  responses:
    Unauthorized:
      description: Não autenticado
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            no_token:
              value: { error: no_token, message: "Authorization header missing" }
            invalid_token:
              value: { error: invalid_token, message: "JWT signature verification failed" }
            token_expired:
              value: { error: token_expired, message: "JWT expired" }
    Forbidden:
      description: Sem permissão
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            forbidden:
              value: { error: forbidden, message: "Admin role required" }
            not_in_whitelist:
              value: { error: not_in_whitelist, message: "Origin not in dashboard_origin whitelist" }
            captcha_failed:
              value: { error: captcha_failed, message: "Turnstile siteverify rejected token" }
    NotFound:
      description: Recurso não encontrado
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    BadRequest:
      description: Body inválido ou parâmetros obrigatórios ausentes
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            captcha_required:
              value: { error: captcha_required, message: "X-Turnstile-Token header missing" }
    FeatureDisabled:
      description: Feature flag desligada — admin precisa habilitar.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error: feature_disabled
            code: FEATURE_DISABLED
            flag: auth.email_otp

paths:
  # ===========================================================================
  # Public utilities
  # ===========================================================================
  /health:
    get:
      tags: [Public utilities]
      summary: Health check
      security: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  ts: { type: string, format: date-time }

  /api/auth/feature-flags/public:
    get:
      tags: [Public utilities]
      summary: Flags públicos (UI condicional)
      description: |
        Retorna apenas o que o frontend precisa para renderizar UI condicional
        (Turnstile widget, HIBP warning, etc). Cache edge 30s.
      security: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  flags:
                    type: object
                    additionalProperties: { type: boolean }
                    example:
                      auth.email_otp: false
                      auth.phone_number: false
                      auth.captcha: true
                      auth.hibp_warn: true
                      auth.multi_session: false
                  turnstileSiteKey:
                    type: string
                    nullable: true
                    description: Site key público do Turnstile, ou null se captcha off.

  /api/auth/check-password:
    post:
      tags: [Public utilities]
      summary: Verifica vazamento via HIBP (k-anonymity)
      description: |
        Endpoint **warn-only**. Senha NUNCA é enviada em texto pleno —
        usa SHA-1 k-anonymity (envia só os 5 primeiros caracteres do hash).

        **Gated por `auth.hibp_warn`.** Quando off, retorna sempre `{checked:false}`
        sem chamar a API externa.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password]
              properties:
                password: { type: string, minLength: 1 }
      responses:
        '200':
          description: Resultado (não bloqueia signup mesmo se breached=true)
          content:
            application/json:
              schema:
                type: object
                properties:
                  breached: { type: boolean }
                  count: { type: integer, description: "Nº de vazamentos no HIBP" }
                  checked: { type: boolean, description: "false quando flag off ou HIBP indisponível" }

  # ===========================================================================
  # Better Auth — Sign-in / Sign-up
  # ===========================================================================
  /api/auth/sign-in/email:
    post:
      tags: [Auth — Sign-in / Sign-up]
      summary: Login email + senha
      description: Quando `auth.captcha=true`, header `X-Turnstile-Token` é obrigatório.
      security:
        - {}
        - turnstileToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string, format: email }
                password: { type: string, minLength: 8 }
                rememberMe: { type: boolean, default: true }
      responses:
        '200':
          description: Login OK — emite cookie de sessão
          content:
            application/json:
              schema:
                type: object
                properties:
                  user: { $ref: '#/components/schemas/User' }
                  session: { $ref: '#/components/schemas/Session' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { description: Rate-limit ou conta bloqueada }

  /api/auth/sign-up/email:
    post:
      tags: [Auth — Sign-in / Sign-up]
      summary: Signup email + senha
      security:
        - {}
        - turnstileToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password, name]
              properties:
                email: { type: string, format: email }
                password: { type: string, minLength: 8 }
                name: { type: string }
      responses:
        '200':
          description: Conta criada — login automático
          content:
            application/json:
              schema:
                type: object
                properties:
                  user: { $ref: '#/components/schemas/User' }
                  session: { $ref: '#/components/schemas/Session' }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/auth/sign-out:
    post:
      tags: [Auth — Sign-in / Sign-up]
      summary: Logout (revoga sessão atual)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties: { success: { type: boolean } }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /api/auth/session:
    get:
      tags: [Auth — Sign-in / Sign-up]
      summary: Sessão atual
      responses:
        '200':
          description: Retorna user+session ou null
          content:
            application/json:
              schema:
                type: object
                properties:
                  user: { $ref: '#/components/schemas/User', nullable: true }
                  session: { $ref: '#/components/schemas/Session', nullable: true }

  /api/auth/forget-password:
    post:
      tags: [Auth — Sign-in / Sign-up]
      summary: Pedir reset de senha
      security:
        - {}
        - turnstileToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
                redirectTo: { type: string, format: uri }
      responses:
        '200':
          description: |
            Sempre 200 (evita enumeration). Se o email existir, um link é enviado.

  /api/auth/reset-password:
    post:
      tags: [Auth — Sign-in / Sign-up]
      summary: Aplicar nova senha com token
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, newPassword]
              properties:
                token: { type: string }
                newPassword: { type: string, minLength: 12 }
      responses:
        '200': { description: OK }
        '400': { description: Token inválido ou expirado }

  /api/auth/sign-in/magic-link:
    post:
      tags: [Auth — Sign-in / Sign-up]
      summary: Pedir magic link por email (TTL 15 min)
      security:
        - {}
        - turnstileToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
                callbackURL: { type: string, format: uri }
      responses:
        '200': { description: Sempre 200 (evita enumeration) }

  /api/auth/magic-link/verify:
    get:
      tags: [Auth — Sign-in / Sign-up]
      summary: Callback do magic link (autentica e redireciona)
      security: []
      parameters:
        - in: query
          name: token
          required: true
          schema: { type: string }
      responses:
        '302': { description: Redirect para callbackURL }
        '400': { description: Token inválido }

  # ===========================================================================
  # Better Auth — Email OTP (gated)
  # ===========================================================================
  /api/auth/sign-in/email-otp/send-verification-otp:
    post:
      tags: [Auth — Email OTP]
      summary: Enviar código OTP por email *
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
                type:
                  type: string
                  enum: [sign-in, email-verification, forget-password]
                  default: sign-in
      responses:
        '200': { description: Código enviado }
        '503': { $ref: '#/components/responses/FeatureDisabled' }

  /api/auth/sign-in/email-otp/verify-otp:
    post:
      tags: [Auth — Email OTP]
      summary: Validar OTP e criar sessão *
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, otp]
              properties:
                email: { type: string, format: email }
                otp: { type: string, pattern: '^[0-9]{6}$' }
      responses:
        '200':
          description: Login OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  user: { $ref: '#/components/schemas/User' }
                  session: { $ref: '#/components/schemas/Session' }
        '400': { description: OTP inválido ou expirado }
        '503': { $ref: '#/components/responses/FeatureDisabled' }

  # ===========================================================================
  # Better Auth — Phone (SMS, gated)
  # ===========================================================================
  /api/auth/phone-number/send-otp:
    post:
      tags: [Auth — Phone (SMS)]
      summary: Enviar código SMS via Twilio *
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phoneNumber]
              properties:
                phoneNumber:
                  type: string
                  pattern: '^\+[1-9][0-9]{6,14}$'
                  example: "+5511999999999"
      responses:
        '200': { description: SMS enviado }
        '503': { $ref: '#/components/responses/FeatureDisabled' }

  /api/auth/phone-number/verify:
    post:
      tags: [Auth — Phone (SMS)]
      summary: Validar SMS e marcar phone verificado *
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phoneNumber, code]
              properties:
                phoneNumber: { type: string }
                code: { type: string, pattern: '^[0-9]{6}$' }
      responses:
        '200': { description: Phone verificado }
        '400': { description: Código inválido }
        '503': { $ref: '#/components/responses/FeatureDisabled' }

  # ===========================================================================
  # Better Auth — Two-Factor
  # ===========================================================================
  /api/auth/two-factor/enable:
    post:
      tags: [Auth — Two-Factor]
      summary: Habilitar 2FA TOTP
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password]
              properties:
                password: { type: string }
      responses:
        '200':
          description: Retorna URI TOTP + backup codes
          content:
            application/json:
              schema:
                type: object
                properties:
                  totpURI: { type: string, example: "otpauth://totp/..." }
                  backupCodes:
                    type: array
                    items: { type: string }

  /api/auth/two-factor/disable:
    post:
      tags: [Auth — Two-Factor]
      summary: Desabilitar 2FA
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password]
              properties:
                password: { type: string }
      responses:
        '200': { description: OK }

  /api/auth/two-factor/verify-totp:
    post:
      tags: [Auth — Two-Factor]
      summary: Verificar código TOTP no challenge de login
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code: { type: string, pattern: '^[0-9]{6}$' }
      responses:
        '200': { description: OK }
        '401': { description: Código inválido }

  /api/auth/two-factor/verify-backup-code:
    post:
      tags: [Auth — Two-Factor]
      summary: Verificar backup code
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code: { type: string }
      responses:
        '200': { description: OK }
        '401': { description: Código inválido ou já usado }

  # ===========================================================================
  # Better Auth — Passkey
  # ===========================================================================
  /api/auth/passkey/register:
    post:
      tags: [Auth — Passkey]
      summary: WebAuthn registration
      responses:
        '200':
          description: Challenge + opções WebAuthn
          content:
            application/json:
              schema: { type: object, additionalProperties: true }

  /api/auth/passkey/authenticate:
    post:
      tags: [Auth — Passkey]
      summary: WebAuthn assertion → cria sessão
      security: []
      responses:
        '200':
          description: Login OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  user: { $ref: '#/components/schemas/User' }
                  session: { $ref: '#/components/schemas/Session' }

  # ===========================================================================
  # Better Auth — Multi-session (gated)
  # ===========================================================================
  /api/auth/multi-session/list-device-sessions:
    get:
      tags: [Auth — Multi-session]
      summary: Listar até 5 sessões ativas do user *
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Session' }
        '503': { $ref: '#/components/responses/FeatureDisabled' }

  /api/auth/multi-session/revoke:
    post:
      tags: [Auth — Multi-session]
      summary: Revogar sessão específica *
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sessionId]
              properties:
                sessionId: { type: string }
      responses:
        '200': { description: OK }
        '503': { $ref: '#/components/responses/FeatureDisabled' }

  /api/auth/multi-session/set-active:
    post:
      tags: [Auth — Multi-session]
      summary: Trocar sessão ativa *
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sessionId]
              properties:
                sessionId: { type: string }
      responses:
        '200': { description: OK }
        '503': { $ref: '#/components/responses/FeatureDisabled' }

  # ===========================================================================
  # JWT / SSO
  # ===========================================================================
  /api/auth/jwks:
    get:
      tags: [JWT / SSO]
      summary: JWKS público (chaves Ed25519)
      description: |
        Retorna o conjunto de chaves públicas usadas para validar JWTs.
        Recomenda-se cache de 10 min do lado cliente (lib `jose` faz automaticamente).
        CORS aberto (`Access-Control-Allow-Origin: *`).
      security: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/JWKS' }
              example:
                keys:
                  - kty: OKP
                    crv: Ed25519
                    x: "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
                    kid: "ed25519-2026-04"
                    use: sig
                    alg: EdDSA

  /api/auth/token:
    get:
      tags: [JWT / SSO]
      summary: Emite JWT a partir do cookie de sessão
      description: |
        Frontend chama imediatamente antes de redirect SSO.
        TTL fixo de 15 min — sem refresh token. Quando expira, o painel deve
        redirecionar usuário para `accounts.campuzz.com.br/?returnTo=...`
        (o cookie de sessão de 30 dias permite emissão imediata de novo JWT).
      responses:
        '200':
          description: JWT emitido
          content:
            application/json:
              schema:
                type: object
                properties:
                  token: { type: string, description: "JWT compacto (header.payload.sig)" }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /api/auth/dashboard-origins/public:
    get:
      tags: [JWT / SSO]
      summary: Whitelist pública de origins
      description: |
        Lista (apenas com `enabled=true`) usada pelo frontend para validar
        `returnTo` antes de chamar `/api/auth/token`.
      security: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  origins:
                    type: array
                    items: { type: string, format: uri }
                    example: ["https://dash.cliente.com", "https://app.parceiro.com"]

  /api/auth/sso/audit:
    post:
      tags: [JWT / SSO]
      summary: Audit fire-and-forget do painel externo
      description: |
        Painel externo registra eventos `sso.handoff_*` sem precisar de JWT —
        apenas o evento e metadata. Útil para correlacionar redirects rejeitados.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [event_type]
              properties:
                event_type:
                  type: string
                  example: sso.handoff_succeeded
                severity:
                  type: string
                  enum: [baixa, media, alta, critica]
                  default: baixa
                metadata: { type: object, additionalProperties: true }
      responses:
        '202': { description: Evento enfileirado }

  # ===========================================================================
  # Admin — Usuários
  # ===========================================================================
  /api/accounts:
    get:
      tags: [Admin — Usuários]
      summary: Listar usuários (filtros + paginação)
      parameters:
        - { in: query, name: page, schema: { type: integer, default: 1, minimum: 1 } }
        - { in: query, name: pageSize, schema: { type: integer, default: 25, maximum: 100 } }
        - { in: query, name: q, schema: { type: string }, description: "Busca por email/nome" }
        - { in: query, name: status, schema: { $ref: '#/components/schemas/UserStatus' } }
        - { in: query, name: role, schema: { $ref: '#/components/schemas/UserRole' } }
        - { in: query, name: origin, schema: { type: string } }
        - { in: query, name: organization_id, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PaginatedUsers' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [Admin — Usuários]
      summary: Criar usuário
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, name]
              properties:
                email: { type: string, format: email }
                name: { type: string }
                role: { $ref: '#/components/schemas/UserRole' }
                password: { type: string, minLength: 12, description: "Opcional — se omitido, dispara convite" }
                send_invite: { type: boolean, default: true }
      responses:
        '201':
          description: Criado
          content:
            application/json:
              schema: { $ref: '#/components/schemas/User' }

  /api/accounts/{id}:
    parameters:
      - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [Admin — Usuários]
      summary: Detalhe de usuário
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  user: { $ref: '#/components/schemas/User' }
                  profile: { $ref: '#/components/schemas/UserProfile' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Admin — Usuários]
      summary: Atualizar usuário
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                role: { $ref: '#/components/schemas/UserRole' }
                status: { $ref: '#/components/schemas/UserStatus' }
      responses:
        '200': { description: OK }
    delete:
      tags: [Admin — Usuários]
      summary: Soft-delete (status = deleted)
      responses:
        '200': { description: OK }

  /api/accounts/{id}/disable:
    post:
      tags: [Admin — Usuários]
      summary: Bloquear usuário
      parameters:
        - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string }
                expires_at: { type: string, format: date-time }
      responses:
        '200': { description: OK }

  /api/accounts/{id}/enable:
    post:
      tags: [Admin — Usuários]
      summary: Desbloquear usuário
      parameters:
        - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: OK }

  /api/accounts/{id}/sessions:
    get:
      tags: [Admin — Usuários]
      summary: Sessões ativas do usuário
      parameters:
        - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Session' }

  /api/accounts/{id}/audit-trail:
    get:
      tags: [Admin — Usuários]
      summary: Trilha de auditoria do usuário
      parameters:
        - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
        - { in: query, name: from, schema: { type: string, format: date-time } }
        - { in: query, name: to, schema: { type: string, format: date-time } }
        - { in: query, name: page, schema: { type: integer, default: 1 } }
        - { in: query, name: pageSize, schema: { type: integer, default: 50 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/AuditEvent' }

  /api/accounts/{id}/roles:
    parameters:
      - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [Admin — Usuários]
      summary: Roles atribuídas ao usuário
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  roles:
                    type: array
                    items:
                      type: object
                      properties:
                        role_id: { type: string, format: uuid }
                        role_name: { type: string }
                        organization_id: { type: string, format: uuid, nullable: true }
                        expires_at: { type: string, format: date-time, nullable: true }
    post:
      tags: [Admin — Usuários]
      summary: Atribuir role
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [role_id]
              properties:
                role_id: { type: string, format: uuid }
                organization_id: { type: string, format: uuid }
                expires_at: { type: string, format: date-time }
      responses:
        '200': { description: OK }

  /api/accounts/{id}/roles/{role_id}:
    delete:
      tags: [Admin — Usuários]
      summary: Remover role
      parameters:
        - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
        - { in: path, name: role_id, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: OK }

  /api/accounts/{id}/privileges:
    get:
      tags: [Admin — Usuários]
      summary: Privilégios efetivos do usuário (transitivos via role)
      parameters:
        - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  privileges:
                    type: array
                    items: { $ref: '#/components/schemas/Permission' }

  # ===========================================================================
  # Admin — Convites
  # ===========================================================================
  /api/accounts/invites:
    get:
      tags: [Admin — Convites]
      summary: Listar convites
      parameters:
        - { in: query, name: status, schema: { type: string, enum: [pending, accepted, expired, revoked] } }
      responses:
        '200': { description: OK }
    post:
      tags: [Admin — Convites]
      summary: Criar convite (envia email)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
                role: { $ref: '#/components/schemas/UserRole' }
                organization_id: { type: string, format: uuid }
      responses:
        '201': { description: Criado }

  # ===========================================================================
  # Admin — Organizações (plugin Better Auth)
  # ===========================================================================
  /api/auth/organization/list:
    get:
      tags: [Admin — Organizações]
      summary: Lista todas as organizações
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Organization' }

  /api/auth/organization/get-full-organization:
    get:
      tags: [Admin — Organizações]
      summary: Detalhe + members + invitations
      parameters:
        - { in: query, name: organizationId, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: OK }

  /api/auth/organization/create:
    post:
      tags: [Admin — Organizações]
      summary: Criar organização
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, slug]
              properties:
                name: { type: string }
                slug: { type: string, pattern: '^[a-z0-9][a-z0-9-]{1,62}$' }
                logo: { type: string, format: uri }
      responses:
        '201':
          description: Criada
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Organization' }

  # ===========================================================================
  # Admin — Auditoria
  # ===========================================================================
  /api/audit:
    get:
      tags: [Admin — Auditoria]
      summary: Lista global de eventos auditados
      parameters:
        - { in: query, name: from, schema: { type: string, format: date-time } }
        - { in: query, name: to, schema: { type: string, format: date-time } }
        - { in: query, name: event_type, schema: { type: string } }
        - { in: query, name: actor_email, schema: { type: string, format: email } }
        - { in: query, name: target_email, schema: { type: string, format: email } }
        - in: query
          name: severity
          schema: { type: string, enum: [baixa, media, alta, critica] }
        - { in: query, name: page, schema: { type: integer, default: 1 } }
        - { in: query, name: pageSize, schema: { type: integer, default: 50, maximum: 500 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/AuditEvent' }
                  page: { type: integer }
                  pageSize: { type: integer }
                  total: { type: integer }

  # ===========================================================================
  # Admin — Alertas
  # ===========================================================================
  /api/alerts:
    get:
      tags: [Admin — Alertas]
      summary: Listar alertas
      parameters:
        - { in: query, name: state, schema: { type: string, enum: [open, acked, resolved] } }
        - { in: query, name: priority, schema: { type: string, enum: [low, medium, high, critical] } }
        - { in: query, name: alert_type, schema: { type: string } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Alert' }

  /api/alerts/{id}/ack:
    post:
      tags: [Admin — Alertas]
      summary: Marcar alerta como reconhecido
      parameters:
        - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: OK }

  # ===========================================================================
  # Admin — Dashboards SSO
  # ===========================================================================
  /api/admin/dashboard-origins:
    get:
      tags: [Admin — Dashboards SSO]
      summary: Lista whitelist de origins
      parameters:
        - { in: query, name: enabled, schema: { type: boolean } }
        - { in: query, name: q, schema: { type: string } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/DashboardOrigin' }
    post:
      tags: [Admin — Dashboards SSO]
      summary: Cadastrar nova origin
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [label, origin]
              properties:
                label: { type: string }
                origin: { type: string, format: uri }
                organization_id: { type: string, format: uuid }
                enabled: { type: boolean, default: true }
      responses:
        '201':
          description: Criada
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DashboardOrigin' }

  /api/admin/dashboard-origins/{id}:
    parameters:
      - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
    patch:
      tags: [Admin — Dashboards SSO]
      summary: Atualizar origin
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                label: { type: string }
                origin: { type: string, format: uri }
                organization_id: { type: string, format: uuid }
                enabled: { type: boolean }
      responses:
        '200': { description: OK }
    delete:
      tags: [Admin — Dashboards SSO]
      summary: Remover origin
      responses:
        '200': { description: OK }

  # ===========================================================================
  # Admin — Feature Flags
  # ===========================================================================
  /api/admin/feature-flags:
    get:
      tags: [Admin — Feature Flags]
      summary: Lista os 5 flags
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/FeatureFlag' }

  /api/admin/feature-flags/{key}:
    patch:
      tags: [Admin — Feature Flags]
      summary: Toggle / configurar flag
      description: |
        Audita evento `feature_flag.enabled|disabled|config_updated` e
        invalida o cache em memória. Outros isolates atualizam em ≤30s.
      parameters:
        - in: path
          name: key
          required: true
          schema:
            type: string
            enum:
              - auth.email_otp
              - auth.phone_number
              - auth.captcha
              - auth.hibp_warn
              - auth.multi_session
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                enabled: { type: boolean }
                config: { type: object, additionalProperties: true }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FeatureFlag' }
        '404': { $ref: '#/components/responses/NotFound' }

  # ===========================================================================
  # IAM — Self (Bearer JWT)
  # ===========================================================================
  /api/me:
    get:
      tags: [IAM — Self]
      summary: Perfil completo do usuário (do JWT)
      security:
        - bearerJWT: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  user: { $ref: '#/components/schemas/User' }
                  profile: { $ref: '#/components/schemas/UserProfile' }
                  primary_organization: { $ref: '#/components/schemas/Organization', nullable: true }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /api/me/organizations:
    get:
      tags: [IAM — Self]
      summary: Organizações em que o usuário é membro
      security:
        - bearerJWT: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      allOf:
                        - $ref: '#/components/schemas/Organization'
                        - type: object
                          properties:
                            role: { type: string, example: member }

  /api/me/permissions:
    get:
      tags: [IAM — Self]
      summary: Permissões efetivas do usuário no escopo da org
      security:
        - bearerJWT: []
      parameters:
        - { in: query, name: org, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  roles:
                    type: array
                    items: { type: string }
                  privileges:
                    type: array
                    items: { $ref: '#/components/schemas/Permission' }

  # ===========================================================================
  # IAM — Read-only (Bearer JWT)
  # ===========================================================================
  /api/iam/users:
    get:
      tags: [IAM — Read-only]
      summary: Listar usuários da organização
      description: |
        Caller deve ser **membro da org** OU **admin global**.
        Não retorna usuários de outras orgs.
      security:
        - bearerJWT: []
      parameters:
        - { in: query, name: org, required: true, schema: { type: string, format: uuid } }
        - { in: query, name: q, schema: { type: string } }
        - { in: query, name: page, schema: { type: integer, default: 1 } }
        - { in: query, name: pageSize, schema: { type: integer, default: 25, maximum: 100 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PaginatedUsers' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/iam/users/{id}:
    get:
      tags: [IAM — Read-only]
      summary: Detalhe de usuário (cross-org bloqueado a não ser admin)
      security:
        - bearerJWT: []
      parameters:
        - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
        - { in: query, name: org, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  user: { $ref: '#/components/schemas/User' }
                  profile: { $ref: '#/components/schemas/UserProfile' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/iam/permissions/me:
    get:
      tags: [IAM — Read-only]
      summary: Checar permissão fina do caller
      description: |
        Retorna `allowed: true|false` + privilégios efetivos no escopo.
        Use em painéis externos para esconder/mostrar features.
      security:
        - bearerJWT: []
      parameters:
        - { in: query, name: resource, required: true, schema: { type: string }, example: project }
        - { in: query, name: action, required: true, schema: { type: string }, example: edit }
        - { in: query, name: scope, schema: { type: string, format: uuid }, description: "organization_id" }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PermissionCheckResult' }

  # ===========================================================================
  # Webhooks
  # ===========================================================================
  /api/integrations/sendgrid/events:
    post:
      tags: [Webhooks]
      summary: Eventos SendGrid (bounce, spam_report, unsubscribe)
      description: |
        Endpoint entrante. Validamos a assinatura SendGrid (chave pública ECDSA)
        antes de gravar em `iam.email_event`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              items:
                type: object
                additionalProperties: true
                properties:
                  event: { type: string, example: bounce }
                  email: { type: string, format: email }
                  timestamp: { type: integer, format: int64 }
                  reason: { type: string }
      responses:
        '200': { description: OK — eventos processados }
        '401': { description: Assinatura inválida }
