openapi: 3.0.3
info:
  title: IGA Platform API
  version: 0.2.0
  description: |
    Contrato HTTP actual de la plataforma IGA.

    Notas:
    - Los errores del servicio se devuelven como JSON con los campos `code` y `error`.
    - Los endpoints de listado usan paginación por page y page_size.
    - Cuando `auth.api_key`, `auth.read_api_key`, `auth.write_api_key` o `auth.clients[].api_key` están configurados, las rutas privadas requieren la cabecera `X-API-Key`.
    - Todas las respuestas incluyen la cabecera `X-Request-ID`; los errores JSON también incluyen `request_id`.
    - El namespace canónico actual es `/v1`; las rutas sin prefijo siguen disponibles como alias heredado.
servers:
  - url: http://localhost:8080/v1
  - url: http://localhost:8080
    description: Alias heredado mantenido temporalmente por compatibilidad.
tags:
  - name: Health
  - name: Sync
  - name: Identities
  - name: Audit
  - name: Recertifications
  - name: Access Requests
  - name: API Clients
  - name: Schemas
paths:
  /ready:
    get:
      tags: [Health]
      summary: Verifica readiness de dependencias configuradas
      responses:
        '200':
          description: Dependencias listas
        '503':
          $ref: '#/components/responses/JsonError'
  /openapi.yaml:
    get:
      tags: [Schemas]
      summary: Devuelve la especificación OpenAPI del servicio
      responses:
        '200':
          description: Documento OpenAPI en YAML
          content:
            application/yaml:
              schema:
                type: string
  /openapi.json:
    get:
      tags: [Schemas]
      summary: Devuelve la especificación OpenAPI del servicio en JSON
      responses:
        '200':
          description: Documento OpenAPI en JSON
          content:
            application/json:
              schema:
                type: object
  /postman-collection.json:
    get:
      tags: [Schemas]
      summary: Devuelve una colección Postman generada desde el contrato OpenAPI
      responses:
        '200':
          description: Colección Postman v2.1
          content:
            application/json:
              schema:
                type: object
  /oidc/test/start:
    post:
      tags: [Health]
      summary: Inicia una prueba OIDC real de sandbox mediante authorization code con PKCE
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OIDCTestStartRequest'
      responses:
        '200':
          description: URL de autorización calculada para redirigir al IdP configurado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OIDCTestStartResponse'
        '400':
          $ref: '#/components/responses/JsonError'
        '502':
          $ref: '#/components/responses/JsonError'
  /oidc/callback:
    get:
      tags: [Health]
      summary: Callback HTML de la prueba OIDC sandbox
      parameters:
        - name: state
          in: query
          schema:
            type: string
        - name: code
          in: query
          schema:
            type: string
        - name: error
          in: query
          schema:
            type: string
        - name: error_description
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Página HTML con el resultado de la prueba OIDC
          content:
            text/html:
              schema:
                type: string
        '400':
          description: Flujo inválido o expirado
          content:
            text/html:
              schema:
                type: string
        '502':
          description: Error del intercambio con el IdP
          content:
            text/html:
              schema:
                type: string
  /health:
    get:
      tags: [Health]
      summary: Health check del proceso
      responses:
        '200':
          description: Proceso disponible
  /sync:
    post:
      tags: [Sync]
      summary: Ejecuta la sincronización configurada
      security:
        - ApiKeyAuth: []
      responses:
        '202':
          description: Sincronización aceptada
        '401':
          $ref: '#/components/responses/JsonError'
        '403':
          $ref: '#/components/responses/JsonError'
        '502':
          $ref: '#/components/responses/JsonError'
  /identities:
    get:
      tags: [Identities]
      summary: Lista identidades
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/Collective'
        - $ref: '#/components/parameters/Source'
        - $ref: '#/components/parameters/Status'
        - $ref: '#/components/parameters/IncludeProfile'
        - $ref: '#/components/parameters/IncludeSensitive'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: Lista paginada de identidades
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedIdentityList'
        '400':
          $ref: '#/components/responses/JsonError'
        '401':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
    post:
      tags: [Identities]
      summary: Crea o actualiza una identidad
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Identity'
      responses:
        '201':
          description: Identidad persistida
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Identity'
        '400':
          $ref: '#/components/responses/JsonError'
        '401':
          $ref: '#/components/responses/JsonError'
        '403':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
  /identities/{id}:
    get:
      tags: [Identities]
      summary: Obtiene una identidad por id
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/IdentityID'
        - $ref: '#/components/parameters/IncludeProfile'
        - $ref: '#/components/parameters/IncludeSensitive'
      responses:
        '200':
          description: Identidad encontrada
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/Identity'
                  - $ref: '#/components/schemas/IdentityWithProfile'
        '404':
          $ref: '#/components/responses/JsonError'
        '401':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
  /identities/{id}/profile:
    get:
      tags: [Identities]
      summary: Obtiene el perfil gobernado de una identidad
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/IdentityID'
        - $ref: '#/components/parameters/IncludeSensitive'
      responses:
        '200':
          description: Perfil gobernado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Profile'
        '404':
          $ref: '#/components/responses/JsonError'
        '401':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
  /identities/{id}/audit:
    get:
      tags: [Audit]
      summary: Lista la auditoría de una identidad
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/IdentityID'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: Eventos paginados de auditoría
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedAuditEventList'
        '400':
          $ref: '#/components/responses/JsonError'
        '401':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
  /identities/{id}/deactivate:
    post:
      tags: [Identities]
      summary: Desactiva una identidad en el target configurado
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/IdentityID'
      responses:
        '204':
          description: Identidad desactivada
        '401':
          $ref: '#/components/responses/JsonError'
        '403':
          $ref: '#/components/responses/JsonError'
        '404':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
  /audit:
    get:
      tags: [Audit]
      summary: Lista eventos de auditoría globales
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/AuditIdentityID'
        - $ref: '#/components/parameters/AuditAction'
        - $ref: '#/components/parameters/AuditFrom'
        - $ref: '#/components/parameters/AuditTo'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: Eventos paginados de auditoría
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedAuditEventList'
        '400':
          $ref: '#/components/responses/JsonError'
        '401':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
  /recertifications:
    get:
      tags: [Recertifications]
      summary: Lista casos provisionales de recertificación para consumo externo
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/AuditIdentityID'
        - $ref: '#/components/parameters/Collective'
        - $ref: '#/components/parameters/Source'
        - $ref: '#/components/parameters/RecertificationApplication'
        - $ref: '#/components/parameters/RecertificationReviewer'
        - $ref: '#/components/parameters/RecertificationRisk'
        - $ref: '#/components/parameters/RecertificationStage'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: Lista paginada de casos provisionales
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedRecertificationList'
        '400':
          $ref: '#/components/responses/JsonError'
        '401':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
  /recertifications/summary:
    get:
      tags: [Recertifications]
      summary: Resume los casos provisionales por etapa, aplicación y revisor
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/AuditIdentityID'
        - $ref: '#/components/parameters/Collective'
        - $ref: '#/components/parameters/Source'
        - $ref: '#/components/parameters/RecertificationApplication'
        - $ref: '#/components/parameters/RecertificationReviewer'
        - $ref: '#/components/parameters/RecertificationRisk'
        - $ref: '#/components/parameters/RecertificationStage'
      responses:
        '200':
          description: Agregados provisionales de recertificación
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecertificationSummary'
        '401':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
  /recertifications/{id}:
    get:
      tags: [Recertifications]
      summary: Obtiene un caso provisional de recertificación
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/IdentityID'
      responses:
        '200':
          description: Caso provisional encontrado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecertificationDetail'
        '404':
          $ref: '#/components/responses/JsonError'
        '401':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
  /recertifications/{id}/decision:
    post:
      tags: [Recertifications]
      summary: Registra una decisión provisional de recertificación
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/IdentityID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecertificationDecisionRequest'
      responses:
        '200':
          description: Caso actualizado con la decisión registrada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecertificationDetail'
        '400':
          $ref: '#/components/responses/JsonError'
        '404':
          $ref: '#/components/responses/JsonError'
        '401':
          $ref: '#/components/responses/JsonError'
        '403':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
  /access-requests:
    get:
      tags: [Access Requests]
      summary: Lista solicitudes provisionales de acceso para consumo externo
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/AccessRequesterID'
        - $ref: '#/components/parameters/AccessApplication'
        - $ref: '#/components/parameters/AccessApprover'
        - $ref: '#/components/parameters/AccessStatus'
        - $ref: '#/components/parameters/Collective'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: Lista paginada de solicitudes de acceso
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedAccessRequestList'
        '400':
          $ref: '#/components/responses/JsonError'
        '401':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
  /access-requests/summary:
    get:
      tags: [Access Requests]
      summary: Resume las solicitudes de acceso por estado, aplicación y aprobador
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/AccessRequesterID'
        - $ref: '#/components/parameters/AccessApplication'
        - $ref: '#/components/parameters/AccessApprover'
        - $ref: '#/components/parameters/AccessStatus'
        - $ref: '#/components/parameters/Collective'
      responses:
        '200':
          description: Agregados de solicitudes de acceso
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessRequestSummary'
        '401':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
  /access-requests/catalog:
    get:
      tags: [Access Requests]
      summary: Devuelve el catálogo provisional de aplicaciones y entitlements disponibles
      security:
        - ApiKeyAuth: []
      responses:
        '200':
          description: Catálogo de aplicaciones para solicitudes de acceso
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/CatalogApplication'
        '401':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
  /access-requests/{id}:
    get:
      tags: [Access Requests]
      summary: Obtiene una solicitud provisional de acceso
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/IdentityID'
      responses:
        '200':
          description: Solicitud encontrada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessRequestDetail'
        '404':
          $ref: '#/components/responses/JsonError'
        '401':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
  /access-requests/{id}/decision:
    post:
      tags: [Access Requests]
      summary: Registra una decisión provisional sobre una solicitud de acceso
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/IdentityID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AccessRequestDecisionRequest'
      responses:
        '200':
          description: Solicitud actualizada con la decisión registrada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessRequestDetail'
        '400':
          $ref: '#/components/responses/JsonError'
        '404':
          $ref: '#/components/responses/JsonError'
        '401':
          $ref: '#/components/responses/JsonError'
        '403':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
  /api-clients:
    get:
      tags: [API Clients]
      summary: Lista clientes API registrados
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: Lista paginada de clientes API
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedAPIClientList'
        '400':
          $ref: '#/components/responses/JsonError'
        '401':
          $ref: '#/components/responses/JsonError'
        '403':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
    post:
      tags: [API Clients]
      summary: Crea o actualiza un cliente API
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/APIClientUpsertRequest'
      responses:
        '201':
          description: Cliente API persistido
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/APIClientCredentialResponse'
        '400':
          $ref: '#/components/responses/JsonError'
        '401':
          $ref: '#/components/responses/JsonError'
        '403':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
  /api-clients/{id}:
    get:
      tags: [API Clients]
      summary: Obtiene un cliente API por id
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/IdentityID'
      responses:
        '200':
          description: Cliente API encontrado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/APIClient'
        '401':
          $ref: '#/components/responses/JsonError'
        '403':
          $ref: '#/components/responses/JsonError'
        '404':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
  /api-clients/{id}/rotate-key:
    post:
      tags: [API Clients]
      summary: Rota la credencial de un cliente API y devuelve la nueva clave una sola vez
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/IdentityID'
      responses:
        '200':
          description: Credencial rotada correctamente
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/APIClientCredentialResponse'
        '401':
          $ref: '#/components/responses/JsonError'
        '403':
          $ref: '#/components/responses/JsonError'
        '404':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
    delete:
      tags: [API Clients]
      summary: Elimina un cliente API por id
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/IdentityID'
      responses:
        '204':
          description: Cliente API eliminado
        '401':
          $ref: '#/components/responses/JsonError'
        '403':
          $ref: '#/components/responses/JsonError'
        '404':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
  /schemas:
    get:
      tags: [Schemas]
      summary: Lista schemas de gobierno
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: Lista paginada de schemas
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedSchemaList'
        '400':
          $ref: '#/components/responses/JsonError'
        '401':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
    post:
      tags: [Schemas]
      summary: Crea o actualiza un schema
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SchemaUpsertRequest'
      responses:
        '201':
          description: Schema persistido
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Schema'
        '400':
          $ref: '#/components/responses/JsonError'
        '401':
          $ref: '#/components/responses/JsonError'
        '403':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
  /schemas/{collective}:
    get:
      tags: [Schemas]
      summary: Obtiene el schema de un colectivo
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/CollectivePath'
      responses:
        '200':
          description: Schema encontrado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Schema'
        '404':
          $ref: '#/components/responses/JsonError'
        '401':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
    delete:
      tags: [Schemas]
      summary: Elimina el schema de un colectivo
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/CollectivePath'
      responses:
        '204':
          description: Schema eliminado
        '401':
          $ref: '#/components/responses/JsonError'
        '403':
          $ref: '#/components/responses/JsonError'
        '404':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
  /schemas/{collective}/versions:
    get:
      tags: [Schemas]
      summary: Lista el historial de versiones de un schema
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/CollectivePath'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: Historial paginado de snapshots de schema
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedSchemaVersionList'
        '400':
          $ref: '#/components/responses/JsonError'
        '404':
          $ref: '#/components/responses/JsonError'
        '401':
          $ref: '#/components/responses/JsonError'
        '500':
          $ref: '#/components/responses/JsonError'
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
  parameters:
    IdentityID:
      name: id
      in: path
      required: true
      schema:
        type: string
    CollectivePath:
      name: collective
      in: path
      required: true
      schema:
        type: string
    Collective:
      name: collective
      in: query
      schema:
        type: string
    Source:
      name: source
      in: query
      schema:
        type: string
    Status:
      name: status
      in: query
      schema:
        type: string
        enum: [active, inactive]
    IncludeProfile:
      name: include_profile
      in: query
      schema:
        type: boolean
    IncludeSensitive:
      name: include_sensitive
      in: query
      schema:
        type: boolean
    Page:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1
    PageSize:
      name: page_size
      in: query
      schema:
        type: integer
        minimum: 1
        default: 50
    AuditIdentityID:
      name: identity_id
      in: query
      schema:
        type: string
    AuditAction:
      name: action
      in: query
      schema:
        type: string
    AuditFrom:
      name: from
      in: query
      schema:
        type: string
        format: date-time
    AuditTo:
      name: to
      in: query
      schema:
        type: string
        format: date-time
    RecertificationApplication:
      name: application
      in: query
      schema:
        type: string
    RecertificationReviewer:
      name: reviewer
      in: query
      schema:
        type: string
    RecertificationRisk:
      name: risk
      in: query
      schema:
        type: string
        enum: [low, medium, high]
    RecertificationStage:
      name: stage
      in: query
      schema:
        type: string
        enum: [pending, in_review, escalated, approved, revoked]
    AccessRequesterID:
      name: requester_id
      in: query
      schema:
        type: string
    AccessApplication:
      name: application
      in: query
      schema:
        type: string
    AccessApprover:
      name: approver
      in: query
      schema:
        type: string
    AccessStatus:
      name: status
      in: query
      schema:
        type: string
        enum: [pending, approved, rejected]
  responses:
    JsonError:
      description: Error JSON devuelto por el servicio
      headers:
        X-Request-ID:
          description: Identificador de la petición para trazabilidad extremo a extremo.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  schemas:
    Identity:
      type: object
      required: [id, source, email]
      properties:
        id:
          type: string
        external_id:
          type: string
        source:
          type: string
        collective:
          type: string
        first_name:
          type: string
        last_name:
          type: string
        email:
          type: string
          format: email
        department:
          type: string
        role:
          type: string
        status:
          type: string
          enum: [active, inactive]
        updated_at:
          type: string
          format: date-time
        deprovisioned:
          type: boolean
    IdentityWithProfile:
      allOf:
        - $ref: '#/components/schemas/Identity'
        - type: object
          properties:
            profile:
              $ref: '#/components/schemas/Profile'
    Profile:
      type: object
      additionalProperties: true
    AuditEvent:
      type: object
      properties:
        action:
          type: string
        identity_id:
          type: string
        details:
          type: string
        created_at:
          type: string
          format: date-time
    RecertificationItem:
      type: object
      properties:
        id:
          type: string
        identityID:
          type: string
        displayName:
          type: string
        collective:
          type: string
        source:
          type: string
        application:
          type: string
        reviewer:
          type: string
        type:
          type: string
          enum: [account, role, entitlement]
        stage:
          type: string
          enum: [pending, in_review, escalated, approved, revoked]
        risk:
          type: string
          enum: [low, medium, high]
        dueDays:
          type: integer
        evidence:
          type: string
        last_decision:
          $ref: '#/components/schemas/RecertificationDecision'
    RecertificationDecision:
      type: object
      properties:
        decision:
          type: string
          enum: [approved, revoked, escalated, in_review]
        reviewer:
          type: string
        comment:
          type: string
        created_at:
          type: string
          format: date-time
    RecertificationDecisionRequest:
      type: object
      required: [decision]
      properties:
        decision:
          type: string
          enum: [approved, revoked, escalated, in_review]
        reviewer:
          type: string
        comment:
          type: string
    RecertificationDetail:
      type: object
      properties:
        item:
          $ref: '#/components/schemas/RecertificationItem'
        history:
          type: array
          items:
            $ref: '#/components/schemas/RecertificationDecision'
    RecertificationBreakdownEntry:
      type: object
      properties:
        name:
          type: string
        count:
          type: integer
        open:
          type: integer
        high_risk:
          type: integer
        overdue:
          type: integer
    RecertificationSummary:
      type: object
      properties:
        total:
          type: integer
        due_soon:
          type: integer
        high_risk:
          type: integer
        completed:
          type: integer
        by_stage:
          type: object
          additionalProperties:
            type: integer
        applications:
          type: array
          items:
            $ref: '#/components/schemas/RecertificationBreakdownEntry'
        reviewers:
          type: array
          items:
            $ref: '#/components/schemas/RecertificationBreakdownEntry'
    AccessRequest:
      type: object
      properties:
        id:
          type: string
        requesterID:
          type: string
        displayName:
          type: string
        collective:
          type: string
        application:
          type: string
        entitlement:
          type: string
        justification:
          type: string
        approverID:
          type: string
        approverName:
          type: string
        status:
          type: string
          enum: [pending, approved, rejected]
        risk:
          type: string
          enum: [low, medium, high]
        created_at:
          type: string
          format: date-time
        last_decision:
          $ref: '#/components/schemas/AccessRequestDecision'
    AccessRequestDecision:
      type: object
      properties:
        decision:
          type: string
          enum: [approved, rejected]
        approver:
          type: string
        comment:
          type: string
        created_at:
          type: string
          format: date-time
    AccessRequestDecisionRequest:
      type: object
      required: [decision]
      properties:
        decision:
          type: string
          enum: [approved, rejected]
        approver:
          type: string
        comment:
          type: string
    AccessRequestDetail:
      type: object
      properties:
        item:
          $ref: '#/components/schemas/AccessRequest'
        history:
          type: array
          items:
            $ref: '#/components/schemas/AccessRequestDecision'
    AccessBreakdownEntry:
      type: object
      properties:
        name:
          type: string
        count:
          type: integer
        pending:
          type: integer
    AccessRequestSummary:
      type: object
      properties:
        total:
          type: integer
        pending:
          type: integer
        approved_today:
          type: integer
        rejected:
          type: integer
        by_application:
          type: array
          items:
            $ref: '#/components/schemas/AccessBreakdownEntry'
        by_approver:
          type: array
          items:
            $ref: '#/components/schemas/AccessBreakdownEntry'
    CatalogApplication:
      type: object
      properties:
        name:
          type: string
        entitlements:
          type: array
          items:
            type: string
    APIClient:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        groups:
          type: array
          items:
            type: string
        scope:
          $ref: '#/components/schemas/APIClientScope'
        permissions:
          type: array
          items:
            type: string
        active:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required: [id, name, permissions, active, created_at, updated_at]
    APIClientUpsertRequest:
      type: object
      required: [id, name, permissions]
      properties:
        id:
          type: string
        name:
          type: string
        api_key:
          type: string
        groups:
          type: array
          items:
            type: string
        scope:
          $ref: '#/components/schemas/APIClientScope'
        permissions:
          type: array
          items:
            type: string
        active:
          type: boolean
    APIClientScope:
      type: object
      properties:
        collectives:
          type: array
          items:
            type: string
        applications:
          type: array
          items:
            type: string
    APIClientCredentialResponse:
      type: object
      properties:
        client:
          $ref: '#/components/schemas/APIClient'
        issued_api_key:
          type: string
      required: [client]
    ErrorResponse:
      type: object
      required: [code, error]
      properties:
        code:
          type: string
        error:
          type: string
        request_id:
          type: string
          description: Copia del identificador devuelto en la cabecera X-Request-ID.
    Attribute:
      type: object
      required: [name, type]
      properties:
        name:
          type: string
        type:
          type: string
          enum: [string, boolean]
        required:
          type: boolean
        sensitive:
          type: boolean
    Schema:
      type: object
      properties:
        name:
          type: string
        collective:
          type: string
        allowed_sources:
          type: array
          items:
            type: string
        allowed_targets:
          type: array
          items:
            type: string
        version:
          type: integer
        updated_at:
          type: string
          format: date-time
        attributes:
          type: array
          items:
            $ref: '#/components/schemas/Attribute'
    SchemaVersion:
      type: object
      properties:
        collective:
          type: string
        version:
          type: integer
        operation:
          type: string
          enum: [created, updated, deleted]
        recorded_at:
          type: string
          format: date-time
        schema:
          $ref: '#/components/schemas/Schema'
    SchemaUpsertRequest:
      type: object
      required: [name, collective, attributes]
      properties:
        name:
          type: string
        collective:
          type: string
        allowed_sources:
          type: array
          items:
            type: string
        allowed_targets:
          type: array
          items:
            type: string
        attributes:
          type: array
          items:
            $ref: '#/components/schemas/Attribute'
    PaginationMeta:
      type: object
      properties:
        page:
          type: integer
        page_size:
          type: integer
        total:
          type: integer
    PaginatedIdentityList:
      allOf:
        - $ref: '#/components/schemas/PaginationMeta'
        - type: object
          properties:
            items:
              type: array
              items:
                oneOf:
                  - $ref: '#/components/schemas/Identity'
                  - $ref: '#/components/schemas/IdentityWithProfile'
    PaginatedAuditEventList:
      allOf:
        - $ref: '#/components/schemas/PaginationMeta'
        - type: object
          properties:
            items:
              type: array
              items:
                $ref: '#/components/schemas/AuditEvent'
    OIDCTestStartRequest:
      type: object
      required: [issuerUrl, clientId, clientSecret]
      properties:
        idp:
          type: string
        issuerUrl:
          type: string
          format: uri
        clientId:
          type: string
        clientSecret:
          type: string
        redirectUri:
          type: string
          format: uri
    OIDCTestStartResponse:
      type: object
      properties:
        authorization_url:
          type: string
          format: uri
        redirect_uri:
          type: string
          format: uri
        issuer:
          type: string
        request_id:
          type: string
    PaginatedRecertificationList:
      allOf:
        - $ref: '#/components/schemas/PaginationMeta'
        - type: object
          properties:
            items:
              type: array
              items:
                $ref: '#/components/schemas/RecertificationItem'
    PaginatedAccessRequestList:
      allOf:
        - $ref: '#/components/schemas/PaginationMeta'
        - type: object
          properties:
            items:
              type: array
              items:
                $ref: '#/components/schemas/AccessRequest'
    PaginatedAPIClientList:
      allOf:
        - $ref: '#/components/schemas/PaginationMeta'
        - type: object
          properties:
            items:
              type: array
              items:
                $ref: '#/components/schemas/APIClient'
    PaginatedSchemaList:
      allOf:
        - $ref: '#/components/schemas/PaginationMeta'
        - type: object
          properties:
            items:
              type: array
              items:
                $ref: '#/components/schemas/Schema'
    PaginatedSchemaVersionList:
      allOf:
        - $ref: '#/components/schemas/PaginationMeta'
        - type: object
          properties:
            items:
              type: array
              items:
                $ref: '#/components/schemas/SchemaVersion'