openapi: 3.1.0
info:
  title: Conectar con Sistema Facturación Perú · OAuth 2.0
  version: 1.0.0
  summary: Que el cliente de tu sistema, que ya tiene su cuenta en Sistema Facturación Perú, la conecte con un botón.
  description: |
    Servidor OAuth 2.0 con **authorization code + PKCE (S256)**, ambos obligatorios, y `state` obligatorio
    (RFC 6749, RFC 7636, RFC 7009 y RFC 9207 para `iss`). Solo inicia sesión y conecta una cuenta que **ya existe**:
    nunca registra empresas ni usuarios.

    1. Registramos tu aplicación (Superadmin › Integraciones › Aplicaciones): `client_id` (`sfp_app_…`),
       `client_secret` (`sfp_secret_…`, se muestra una vez), direcciones de retorno exactas y permisos que puede pedir.
    2. Tu botón abre una ventana emergente en `GET /conectar`. El usuario escribe su RUC, entra con su usuario de siempre
       en el subdominio de su empresa y pulsa «Permitir».
    3. Volvemos a tu `redirect_uri` con `code` (un solo uso, 5 minutos), `state` e `iss`.
    4. Tu servidor canjea el código en `POST /oauth/token` y recibe `access_token` (1 hora), `refresh_token` (cambia en cada
       uso) y la dirección de la API v1 de esa empresa (`empresa.api_url`).
    5. Llamas a la API v1 (su referencia está en el mismo portal) con `Authorization: Bearer sfp_oat_…`.

    Errores del token: `{ "error": "...", "error_description": "..." }` (RFC 6749 §5.2). Guía: https://sistemafacturacionperu.com/desarrolladores/conectar/
  contact:
    name: Sistema Facturación Perú
    url: https://sistemafacturacionperu.com/desarrolladores
servers:
  - url: https://api.sistemafacturacionperu.com
    description: Servidor de autorización
tags:
  - name: Autorización
  - name: Tokens

paths:
  /conectar:
    get:
      tags: [Autorización]
      summary: Abre la ventana «Conectar con Sistema Facturación Perú»
      operationId: conectar
      description: |
        Se abre en una ventana emergente (o redirección), nunca en un iframe: responde con `X-Frame-Options: DENY`.
        Primero se validan `client_id` y `redirect_uri` (coincidencia exacta): si fallan, el error se muestra en la
        pantalla y **no** se redirige. Con la dirección ya válida, los demás errores vuelven a tu `redirect_uri` como
        `?error=…&error_description=…&state=…&iss=…`.
      parameters:
        - { name: response_type, in: query, required: true, schema: { type: string, enum: [code] } }
        - { name: client_id, in: query, required: true, schema: { type: string, example: sfp_app_k3j9x0m2q8w1z7c4v6b5n0a1 } }
        - { name: redirect_uri, in: query, required: true, description: 'Una de las registradas, idéntica.', schema: { type: string, format: uri } }
        - { name: state, in: query, required: true, description: 'Al azar, hasta 500 caracteres. Compruébalo a la vuelta (CSRF).', schema: { type: string, maxLength: 500 } }
        - { name: code_challenge, in: query, required: true, description: 'BASE64URL(SHA256(code_verifier)) sin «=», 43 caracteres.', schema: { type: string, minLength: 43, maxLength: 43 } }
        - { name: code_challenge_method, in: query, required: true, schema: { type: string, enum: [S256] } }
        - name: scope
          in: query
          description: 'Permisos separados por espacios (de los que tu aplicación puede pedir). Sin él, todos los registrados.'
          schema: { type: string, example: comprobantes.emitir comprobantes.leer consultas }
      responses:
        '200':
          description: La pantalla del RUC (HTML).
        '302':
          description: |
            Vuelta a tu `redirect_uri`. Con permiso: `code`, `state`, `iss`. Si no: `error` (`invalid_request`,
            `unsupported_response_type`, `invalid_scope`, `access_denied`), `error_description`, `state`, `iss`.
          headers:
            Location: { schema: { type: string }, example: 'https://tusistema.com/sfp/vuelta?code=sfp_oac_…&state=abc&iss=https%3A%2F%2Fapi.sistemafacturacionperu.com' }
        '400':
          description: Aplicación o redirect_uri no registradas (en pantalla, sin redirección).
        '403':
          description: Aplicación suspendida (en pantalla).

  /oauth/token:
    post:
      tags: [Tokens]
      summary: Canjea el código o renueva los tokens
      operationId: oauthToken
      description: |
        De servidor a servidor. El cliente se autentica con `Authorization: Basic base64(client_id:client_secret)` **o**
        con `client_id` y `client_secret` en el cuerpo (no las dos). Límite: 30 por minuto por IP y 120 por `client_id`.
        - `authorization_code`: `code`, `redirect_uri` (la misma) y `code_verifier`. Un código canjeado dos veces
          revoca todo lo que se emitió con él.
        - `refresh_token`: rotatorio. Guarda siempre el nuevo: si reusas uno ya usado, revocamos toda la cadena.
      security:
        - clientBasic: []
        - {}
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              oneOf:
                - $ref: '#/components/schemas/PedidoCodigo'
                - $ref: '#/components/schemas/PedidoRenovacion'
      responses:
        '200':
          description: Tokens y datos de la empresa.
          headers:
            Cache-Control: { schema: { type: string, example: no-store } }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RespuestaToken' }
        '400':
          description: '`invalid_request`, `invalid_grant`, `invalid_scope` o `unsupported_grant_type`.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorOAuth' }
              example: { error: invalid_grant, error_description: code_verifier no corresponde al code_challenge (PKCE). }
        '401':
          description: '`invalid_client` (con `WWW-Authenticate: Basic` si usaste Basic).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorOAuth' }
        '429':
          description: '`temporarily_unavailable`, con `Retry-After`.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorOAuth' }

  /oauth/revoke:
    post:
      tags: [Tokens]
      summary: Revoca un token (RFC 7009)
      operationId: oauthRevoke
      description: |
        Un `access_token` deja de servir; un `refresh_token` **desconecta la empresa** (como si pulsara «Desconectar»,
        sin aviso: lo pediste tú). Un token desconocido o de otra aplicación responde 200 igual.
      security:
        - clientBasic: []
        - {}
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [token]
              properties:
                token: { type: string }
                token_type_hint: { type: string, enum: [access_token, refresh_token] }
                client_id: { type: string }
                client_secret: { type: string }
      responses:
        '200':
          description: Revocado (o no existía).
          content:
            application/json:
              schema: { type: object }
        '400':
          description: Falta `token`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorOAuth' }
        '401':
          description: '`invalid_client`.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorOAuth' }

webhooks:
  conexion.revocada:
    post:
      summary: La empresa pulsó «Desconectar»
      description: |
        A la URL de webhook de tu aplicación, firmado como los webhooks de la API v1:
        `X-SFP-Signature: t=<unix>,v1=<hex HMAC-SHA256(whsec_…, "t.cuerpo")>`, más `X-SFP-Event` y `X-SFP-Delivery`.
        Responde 2xx; reintentamos a 1 m, 5 m, 30 m, 2 h, 6 h y 24 h. Sus tokens ya no sirven.
      requestBody:
        content:
          application/json:
            example:
              id: 7b0e3c8a-5d1f-4f0e-9a51-2a8c7c9e1f30
              evento: conexion.revocada
              creado: '2026-12-26T10:15:00-05:00'
              data:
                conexion_id: 42
                empresa: { ruc: '20601030013', razon_social: EMPRESA CONECTADA S.A.C. }
                motivo: empresa
                revocada: '2026-12-26T10:15:00-05:00'
      responses:
        '200': { description: Recibido }

components:
  securitySchemes:
    clientBasic:
      type: http
      scheme: basic
      description: 'client_id y client_secret (cada uno con application/x-www-form-urlencoded, RFC 6749 §2.3.1).'
  schemas:
    PedidoCodigo:
      type: object
      required: [grant_type, code, redirect_uri, code_verifier]
      properties:
        grant_type: { type: string, const: authorization_code }
        code: { type: string, example: sfp_oac_Qm3x… }
        redirect_uri: { type: string, format: uri }
        code_verifier: { type: string, minLength: 43, maxLength: 128 }
        client_id: { type: string }
        client_secret: { type: string }
    PedidoRenovacion:
      type: object
      required: [grant_type, refresh_token]
      properties:
        grant_type: { type: string, const: refresh_token }
        refresh_token: { type: string, example: sfp_ort_Lk2… }
        scope: { type: string, description: 'Opcional: solo permisos ya concedidos.' }
        client_id: { type: string }
        client_secret: { type: string }
    RespuestaToken:
      type: object
      properties:
        access_token: { type: string, example: sfp_oat_9fK2… }
        token_type: { type: string, const: Bearer }
        expires_in: { type: integer, example: 3600 }
        refresh_token: { type: string, example: sfp_ort_Lk2… }
        scope: { type: string, description: 'Los permisos que sirven HOY con el plan de la empresa (en el Básico, solo los de emitir).', example: comprobantes.emitir comprobantes.leer }
        conexion_id: { type: integer, example: 42 }
        empresa:
          type: object
          properties:
            ruc: { type: string, example: '20601030013' }
            razon_social: { type: string, example: EMPRESA CONECTADA S.A.C. }
            api_url: { type: string, format: uri, example: https://miempresa.sistemafacturacionperu.com/api/v1 }
    ErrorOAuth:
      type: object
      required: [error]
      properties:
        error:
          type: string
          enum: [invalid_request, invalid_client, invalid_grant, invalid_scope, unsupported_grant_type, temporarily_unavailable, server_error]
        error_description: { type: string }
