openapi: "3.0.3"
info:
  title: Mandaitor Delegation Mandate Registry API
  version: "1.0.0"
  description: |
    Neutral, trustworthy infrastructure for verifiable delegated authority
    between humans and AI agents.
  contact:
    name: Mandaitor API Support
    email: api@mandaitor.io
  license:
    name: Proprietary
    url: https://mandaitor.io/legal/license

servers:
  - url: https://api.mandaitor.io/v1
    description: Production
  - url: https://api.dev.mandaitor.io/v1
    description: Development

security:
  - ApiKeyAuth: []
  - BearerAuth: []

tags:
  - name: Mandates
    description: Core mandate lifecycle operations
  - name: Verification
    description: Real-time action authorization
  - name: Events
    description: Immutable audit trail
  - name: Onboarding
    description: Tenant access request and activation
  - name: Admin
    description: Admin-only operations
  - name: Widget Config
    description: Tenant widget configuration
  - name: EUDI Wallet
    description: eIDAS 2.0 EUDI Wallet identity verification (OpenID4VP)
  - name: Identity
    description: Identity provider configuration, token exchange, and SCIM provisioning
  - name: SCIM
    description: SCIM 2.0 user provisioning (Okta/Entra ID lifecycle management)
  - name: Public
    description: Unauthenticated public endpoints

# ═══════════════════════════════════════════════════════════════
# PATHS
# ═══════════════════════════════════════════════════════════════

paths:
  # ─── Core Mandates ──────────────────────────────────────────

  /mandates:
    post:
      operationId: createMandate
      summary: Create a new mandate
      tags: [Mandates]
      description: |
        Creates a new delegation mandate between a principal and a delegate.
        The mandate defines the scope of delegated authority, optional constraints,
        and an expiration date. A KMS-signed audit event is emitted on success.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateMandateRequest"
      responses:
        "201":
          description: Mandate created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Mandate"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

    get:
      operationId: listMandates
      summary: List mandates (paginated)
      tags: [Mandates]
      description: |
        Returns a paginated list of mandates for the authenticated tenant.
        Supports optional filtering by status and cursor-based pagination.
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [DRAFT, ACTIVE, SUSPENDED, REVOKED, EXPIRED]
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Paginated list of mandates
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Mandate"
                  next_cursor:
                    type: string
                    nullable: true
                    description: Base64url-encoded cursor for the next page
        "401":
          $ref: "#/components/responses/Unauthorized"

  /mandates/{id}:
    get:
      operationId: getMandate
      summary: Get mandate by ID
      tags: [Mandates]
      description: |
        Retrieves a single mandate by its ID. Returns the full mandate record
        including principal, delegate, scope, constraints, and proof metadata.
      parameters:
        - $ref: "#/components/parameters/MandateId"
      responses:
        "200":
          description: Mandate details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Mandate"
        "404":
          $ref: "#/components/responses/NotFound"

  /mandates/{id}/revoke:
    post:
      operationId: revokeMandate
      summary: Revoke a mandate
      tags: [Mandates]
      description: |
        Permanently revokes a mandate. Only active or suspended mandates can be
        revoked. An optional reason can be provided. A KMS-signed audit event is emitted.
      parameters:
        - $ref: "#/components/parameters/MandateId"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  example: "Contract terminated"
      responses:
        "200":
          description: Mandate revoked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatusChange"
        "409":
          $ref: "#/components/responses/Conflict"

  /mandates/{id}/suspend:
    post:
      operationId: suspendMandate
      summary: Suspend a mandate
      tags: [Mandates]
      description: |
        Temporarily suspends an active mandate. Suspended mandates can be
        reactivated later. An optional reason can be provided.
      parameters:
        - $ref: "#/components/parameters/MandateId"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  example: "Under review"
      responses:
        "200":
          description: Mandate suspended
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatusChange"
        "409":
          $ref: "#/components/responses/Conflict"

  /mandates/{id}/reactivate:
    post:
      operationId: reactivateMandate
      summary: Reactivate a suspended mandate
      tags: [Mandates]
      description: |
        Reactivates a previously suspended mandate, returning it to ACTIVE status.
        Only mandates in SUSPENDED state can be reactivated.
      parameters:
        - $ref: "#/components/parameters/MandateId"
      responses:
        "200":
          description: Mandate reactivated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatusChange"
        "409":
          $ref: "#/components/responses/Conflict"

  # ─── Approval Workflow ──────────────────────────────────────

  /mandates/{id}/approve:
    post:
      operationId: approveMandate
      summary: Approve a pending mandate
      tags: [Mandates]
      description: |
        Approves a mandate that is in PENDING_APPROVAL state, transitioning it
        to ACTIVE. Only mandates created with `require_approval: true` enter
        this state. An audit event (MANDATE_APPROVED) is emitted.
      parameters:
        - $ref: "#/components/parameters/MandateId"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  description: Optional reason/notes for the approval
      responses:
        "200":
          description: Mandate approved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatusChange"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"

  /mandates/{id}/reject:
    post:
      operationId: rejectMandate
      summary: Reject a pending mandate
      tags: [Mandates]
      description: |
        Rejects a mandate that is in PENDING_APPROVAL state, transitioning it
        to REVOKED. An audit event (MANDATE_REJECTED) is emitted.
      parameters:
        - $ref: "#/components/parameters/MandateId"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  description: Reason for rejection
      responses:
        "200":
          description: Mandate rejected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatusChange"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"

  # ─── Verification ───────────────────────────────────────────

  /verify:
    post:
      operationId: verifyAction
      summary: Verify if an action is authorized by a mandate
      tags: [Verification]
      description: |
        Performance-critical endpoint. Target: < 50ms p99 latency (without PoM),
        < 200ms p99 (with PoM). Returns ALLOW or DENY with optional escalation
        metadata and an optional Proof-of-Mandate Verifiable Credential.
      parameters:
        - name: pom
          in: query
          description: |
            Request a Proof-of-Mandate Verifiable Credential in the response.
            The VC is an SD-JWT signed by the Mandaitor issuer DID.
          required: false
          schema:
            type: string
            enum: [sd-jwt-vc]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VerifyRequest"
      responses:
        "200":
          description: Verification result (with optional Proof-of-Mandate VC)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VerifyResponseWithPoM"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ─── EUDI Wallet (eIDAS 2.0 / OpenID4VP) ───────────────────

  /eudi/sessions:
    post:
      operationId: initiateEudiSession
      summary: Initiate EUDI Wallet verification session
      tags: [EUDI Wallet]
      description: |
        Creates a new OpenID4VP session for EUDI Wallet identity verification.
        Returns a QR code URI (cross-device) and deep link URI (same-device)
        that the user scans/opens with their EUDI Wallet.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                presentation_definition_id:
                  type: string
                  description: ID of the presentation definition to use
                  enum:
                    - mandaitor-pid-minimal
                    - mandaitor-pid-standard
                    - mandaitor-pid-delegation
                  default: mandaitor-pid-standard
      responses:
        "201":
          description: Session created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EudiSessionResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /eudi/sessions/{sessionId}:
    get:
      operationId: getEudiSessionStatus
      summary: Poll EUDI session status
      tags: [EUDI Wallet]
      description: |
        Polls the status of an EUDI Wallet verification session.
        Returns the resolved identity when the session is completed.
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Session status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EudiSessionStatus"
        "404":
          $ref: "#/components/responses/NotFound"

  /eudi/sessions/{sessionId}/response:
    post:
      operationId: eudiSessionCallback
      summary: Receive VP Token from EUDI Wallet
      tags: [EUDI Wallet]
      security: []
      description: |
        Receives the VP Token from the EUDI Wallet via direct_post response mode.
        This endpoint is called by the wallet after user consent. No authentication
        required as the wallet POSTs directly.
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [vp_token]
              properties:
                vp_token:
                  type: string
                  description: The VP Token (SD-JWT VC compact serialization)
                presentation_submission:
                  type: object
                  description: DIF Presentation Submission mapping
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [vp_token]
              properties:
                vp_token:
                  type: string
                presentation_submission:
                  type: string
      responses:
        "200":
          description: VP Token accepted and verified
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [COMPLETED]
                  session_id:
                    type: string
        "400":
          $ref: "#/components/responses/BadRequest"

  /eudi/request/{requestId}:
    get:
      operationId: getEudiRequestObject
      summary: Serve OpenID4VP Authorization Request Object
      tags: [EUDI Wallet]
      security: []
      description: |
        Serves the signed JWT Authorization Request Object that the EUDI Wallet
        fetches after scanning the QR code. Returns the JWT directly with
        content type application/oauth-authz-req+jwt.
      parameters:
        - name: requestId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Signed JWT Request Object
          content:
            application/oauth-authz-req+jwt:
              schema:
                type: string
        "404":
          $ref: "#/components/responses/NotFound"

  # ─── Identity Provider Configuration ────────────────────────

  /tenants/{id}/identity-providers:
    get:
      operationId: getIdpConfig
      summary: Get tenant identity provider configuration
      tags: [Identity]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Identity provider configuration
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantIdpConfig"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateIdpConfig
      summary: Update tenant identity provider configuration
      tags: [Identity]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TenantIdpConfigUpdate"
      responses:
        "200":
          description: Configuration updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantIdpConfig"
        "400":
          $ref: "#/components/responses/BadRequest"

  # ─── Token Exchange (OBO) ──────────────────────────────────

  /identity/token-exchange:
    post:
      operationId: tokenExchange
      summary: OAuth 2.0 Token Exchange for delegation chains
      tags: [Identity]
      description: |
        Exchange a user's access token for a scoped delegation token.
        Implements RFC 8693 Token Exchange for On-Behalf-Of (OBO) flows
        where AI agents act on behalf of authenticated users.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [subject_token, mandate_id]
              properties:
                subject_token:
                  type: string
                  description: The original user's access token
                subject_token_type:
                  type: string
                  default: "urn:ietf:params:oauth:token-type:access_token"
                mandate_id:
                  type: string
                  description: Mandate ID that authorizes the delegation
                scope:
                  type: string
                audience:
                  type: string
      responses:
        "200":
          description: Token exchanged
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TokenExchangeResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"

  # ─── SCIM 2.0 Provisioning ────────────────────────────────

  /scim/v2/Users:
    get:
      operationId: scimListUsers
      summary: List provisioned users
      tags: [SCIM]
      parameters:
        - name: startIndex
          in: query
          schema:
            type: integer
            default: 1
        - name: count
          in: query
          schema:
            type: integer
            default: 100
      responses:
        "200":
          description: SCIM ListResponse
          content:
            application/scim+json:
              schema:
                type: object
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: scimCreateUser
      summary: Provision a new user
      tags: [SCIM]
      requestBody:
        required: true
        content:
          application/scim+json:
            schema:
              type: object
      responses:
        "201":
          description: User created
          content:
            application/scim+json:
              schema:
                type: object
        "400":
          $ref: "#/components/responses/BadRequest"

  /scim/v2/Users/{userId}:
    get:
      operationId: scimGetUser
      summary: Get a provisioned user
      tags: [SCIM]
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: SCIM User
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: scimReplaceUser
      summary: Replace a user
      tags: [SCIM]
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/scim+json:
            schema:
              type: object
      responses:
        "200":
          description: User updated
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      operationId: scimUpdateUser
      summary: Partially update a user
      tags: [SCIM]
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/scim+json:
            schema:
              type: object
      responses:
        "200":
          description: User updated
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: scimDeleteUser
      summary: Deprovision a user (revokes all mandates)
      tags: [SCIM]
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        "204":
          description: User deleted
        "404":
          $ref: "#/components/responses/NotFound"

  # ─── Health ────────────────────────────────────────────────

  /health:
    get:
      operationId: getHealth
      summary: Health/status check
      tags: [Public]
      security: []
      description: |
        Returns service health status. No sensitive data is exposed.
        Suitable for monitoring, load balancers, and Trust Center embedding.
      responses:
        "200":
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                required: [status, timestamp, version]
                properties:
                  status:
                    type: string
                    enum: [ok]
                    description: Service status
                  timestamp:
                    type: string
                    format: date-time
                    description: Current server time (ISO 8601)
                  version:
                    type: string
                    description: Build version identifier
                    example: "1.2.3-beta.1"
                  stage:
                    type: string
                    description: Deployment stage
                    example: "prod"

  # ─── DID Document ──────────────────────────────────────────

  /.well-known/did.json:
    get:
      operationId: getDidDocument
      summary: Resolve the Mandaitor issuer DID document
      tags: [Public]
      security: []
      description: |
        Returns the W3C DID Document for the Mandaitor issuer DID
        (did:web:api.mandaitor.io). Contains the public key used to
        verify Proof-of-Mandate SD-JWT VCs.
      responses:
        "200":
          description: DID Document
          content:
            application/did+json:
              schema:
                type: object
                properties:
                  "@context":
                    type: array
                    items:
                      type: string
                  id:
                    type: string
                    example: "did:web:api.mandaitor.io"
                  verificationMethod:
                    type: array
                    items:
                      type: object
                  authentication:
                    type: array
                    items:
                      type: string
                  assertionMethod:
                    type: array
                    items:
                      type: string
                  service:
                    type: array
                    items:
                      type: object
        "404":
          $ref: "#/components/responses/NotFound"

  # ─── Trust Signals ────────────────────────────────────────

  /.well-known/trust.json:
    get:
      operationId: getTrustSignals
      summary: Public trust signals for machine-readable trust metadata
      tags: [Public]
      security: []
      description: |
        Returns machine-readable trust metadata including region, partition,
        issuer DID, data residency claims, and supported open standards.
        Unauthenticated and cache-friendly (5-minute client, 10-minute CDN).
      responses:
        "200":
          description: Trust signals
          content:
            application/json:
              schema:
                type: object
                required: [schema_version, region, partition, issuer_did, did_document_url]
                properties:
                  schema_version:
                    type: string
                    example: "1.0.0"
                  region:
                    type: string
                    description: AWS region where the service operates
                    example: "eusc-de-east-1"
                  partition:
                    type: string
                    description: AWS partition (aws, aws-eusc)
                    example: "aws-eusc"
                  issuer_did:
                    type: string
                    description: DID of the Mandaitor VC issuer
                    example: "did:web:api.mandaitor.io"
                  did_document_url:
                    type: string
                    format: uri
                    description: URL to resolve the issuer DID document
                    example: "https://api.mandaitor.io/.well-known/did.json"
                  data_residency:
                    type: object
                    properties:
                      primary_region:
                        type: string
                      partition:
                        type: string
                      jurisdiction:
                        type: string
                        example: "EU (European Sovereign Cloud)"
                  open_standards:
                    type: array
                    items:
                      type: string
                    description: List of open standards implemented
                  build_version:
                    type: string
                  stage:
                    type: string
                  timestamp:
                    type: string
                    format: date-time

  # ─── Events ─────────────────────────────────────────────────

  /mandates/{id}/events:
    get:
      operationId: getMandateEvents
      summary: Get audit events for a mandate
      tags: [Events]
      description: |
        Returns a paginated list of audit events for a specific mandate.
        Events form an immutable, hash-chained audit trail.
      parameters:
        - $ref: "#/components/parameters/MandateId"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Paginated list of events
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/AuditEvent"
                  next_cursor:
                    type: string
                    nullable: true
                    description: Base64url-encoded cursor for the next page
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /mandates/{id}/evidence-pack:
    get:
      operationId: getEvidencePack
      summary: Export a court-ready evidence pack
      tags: [Evidence]
      description: |
        Exports a comprehensive, court-ready evidence pack for a mandate containing:
        - Mandate snapshot at export time
        - Hash-chained audit event log
        - SD-JWT proof tokens from verification events
        - Case log hash (SHA-256 fingerprint over canonical event chain)
        - Issuer DID document reference for independent verification

        The `case_log_hash` is a deterministic SHA-256 digest computed over the
        canonical JSON representation of the event chain, enabling tamper detection.

        The `schema_version` field allows forward-compatible evolution of the pack format.
      parameters:
        - $ref: "#/components/parameters/MandateId"
        - name: event_id
          in: query
          description: Scope the evidence pack to a specific verification event
          schema:
            type: string
      responses:
        "200":
          description: Evidence pack exported successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EvidencePack"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /events:
    get:
      operationId: listEvents
      summary: Query all events for the tenant
      tags: [Events]
      description: |
        Queries all audit events for the authenticated tenant, filtered by mandate_id.
        Supports optional event_type filtering and cursor-based pagination.
      parameters:
        - name: mandate_id
          in: query
          required: true
          schema:
            type: string
          description: Mandate ID to filter events by
        - name: event_type
          in: query
          schema:
            type: string
            enum:
              - MANDATE_CREATED
              - MANDATE_SUSPENDED
              - MANDATE_REACTIVATED
              - MANDATE_REVOKED
              - VERIFICATION_ALLOWED
              - VERIFICATION_DENIED
              - ESCALATION_TRIGGERED
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Paginated list of events
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/AuditEvent"
                  next_cursor:
                    type: string
                    nullable: true
                    description: Base64url-encoded cursor for the next page
        "401":
          $ref: "#/components/responses/Unauthorized"

  /events/{id}:
    get:
      operationId: getEvent
      summary: Get a specific event by ID
      tags: [Events]
      description: |
        Retrieves a single audit event by its ID. Requires the mandate_id query
        parameter to locate the event in the partition.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            example: "evt_01HXYZ..."
        - name: mandate_id
          in: query
          required: true
          schema:
            type: string
          description: Mandate ID the event belongs to
      responses:
        "200":
          description: Event details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuditEvent"
        "404":
          $ref: "#/components/responses/NotFound"

  # ─── Onboarding ─────────────────────────────────────────────

  /onboarding/request:
    post:
      operationId: requestAccess
      summary: Submit an access request (no auth required)
      tags: [Onboarding, Public]
      description: |
        Submits a new tenant access request. No authentication required.
        The request enters PENDING state and must be approved by an admin.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AccessRequest"
      responses:
        "201":
          description: Access request submitted
          content:
            application/json:
              schema:
                type: object
                properties:
                  request_id:
                    type: string
                  status:
                    type: string
                    example: "PENDING"
                  message:
                    type: string

        "400":
          $ref: "#/components/responses/BadRequest"
  /admin/onboarding:
    get:
      operationId: listOnboardingRequests
      summary: List onboarding access requests (admin only)
      tags: [Admin]
      description: |
        Lists onboarding access requests. Supports optional status filtering
        (PENDING, APPROVED, REJECTED) and cursor-based pagination.
        Requires Cognito JWT with mandaitor-admins group membership.
      security:
        - BearerAuth: []
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [PENDING, APPROVED, REJECTED]
          description: Filter by request status
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Paginated list of onboarding requests
          content:
            application/json:
              schema:
                type: object
                properties:
                  requests:
                    type: array
                    items:
                      type: object
                      properties:
                        request_id:
                          type: string
                        company_name:
                          type: string
                        contact_name:
                          type: string
                        contact_email:
                          type: string
                          format: email
                        use_case:
                          type: string
                        website:
                          type: string
                          format: uri
                        status:
                          type: string
                          enum: [PENDING, APPROVED, REJECTED]
                        submitted_at:
                          type: string
                          format: date-time
                        reviewed_at:
                          type: string
                          format: date-time
                        reviewed_by:
                          type: string
                        tenant_id:
                          type: string
                  next_cursor:
                    type: string
                    nullable: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Admin group membership required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /admin/onboarding/{requestId}/{action}:
    post:
      operationId: reviewOnboardingRequest
      summary: Approve or reject an access request
      description: |
        Processes an admin action on an onboarding request.
        Supported values for `{action}`: `approve`, `reject`.
      tags: [Admin]
      parameters:
        - name: requestId
          in: path
          required: true
          schema:
            type: string
        - name: action
          in: path
          required: true
          schema:
            type: string
            enum: [approve, reject]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                notes:
                  type: string
      responses:
        "200":
          description: Request reviewed
        "201":
          description: Tenant activated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantRecord"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /admin/tenants/{id}/kill-switch:
    post:
      operationId: tenantEmergencyKillSwitch
      summary: Emergency kill-switch — revoke all tenant mandates
      tags: [Admin]
      description: |
        **EMERGENCY OPERATION** — Immediately revokes all ACTIVE and SUSPENDED
        mandates for a tenant. This is an irreversible bulk operation intended
        for security incidents, compliance breaches, or contract terminations.

        Requires Cognito JWT with `mandaitor-admins` group membership.
        A `MANDATE_REVOKED` audit event is emitted per mandate with
        `details.kill_switch_triggered = true`.
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            example: "tnt_ABC123"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reason]
              properties:
                reason:
                  type: string
                  description: Mandatory reason for the emergency kill-switch
                  example: "Security incident — unauthorized access detected"
      responses:
        "200":
          description: Kill-switch executed
          content:
            application/json:
              schema:
                type: object
                properties:
                  tenant_id:
                    type: string
                  revoked_count:
                    type: integer
                  skipped_count:
                    type: integer
                  execution_id:
                    type: string
                    description: Unique ID for this kill-switch operation
                  timestamp:
                    type: string
                    format: date-time
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /admin/system-mode:
    get:
      operationId: getSystemMode
      summary: Get current system operational mode
      tags: [Admin]
      description: |
        Returns the current system mode (growth, sustain, maintenance, frozen)
        and associated metadata. Requires Cognito JWT with `mandaitor-admins`
        group membership.
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Current system mode
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SystemMode"
        "401":
          $ref: "#/components/responses/Unauthorized"

    post:
      operationId: setSystemMode
      summary: Set system operational mode
      tags: [Admin]
      description: |
        Changes the system operational mode. This immediately affects all API
        endpoints:

        - **growth** — Normal operation. All features active.
        - **sustain** — Normal operation. No new features (policy only).
        - **maintenance** — All authenticated endpoints return 503.
        - **frozen** — Write endpoints return 503. Reads still work.

        Mode changes propagate within ~10 seconds via in-memory cache TTL.
        Requires Cognito JWT with `mandaitor-admins` group membership.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [mode]
              properties:
                mode:
                  type: string
                  enum: [growth, sustain, maintenance, frozen]
                  example: "maintenance"
                reason:
                  type: string
                  example: "CDK stack update — estimated 30 minutes"
                estimated_duration_minutes:
                  type: integer
                  example: 30
      responses:
        "200":
          description: System mode updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SystemMode"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /tenants/{id}/api-keys:
    post:
      operationId: createApiKey
      summary: Generate a new API key for the tenant
      tags: [Onboarding]
      description: |
        Generates a new API key for a tenant. The raw key is returned only once
        in the response and cannot be retrieved again. Requires Cognito JWT.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  example: "production-key"
                scopes:
                  type: array
                  items:
                    type: string
                  example: ["mandates:read", "mandates:write", "verify"]
      responses:
        "201":
          description: API key created
          content:
            application/json:
              schema:
                type: object
                properties:
                  key_id:
                    type: string
                  api_key:
                    type: string
                    description: "Only shown once"
                  name:
                    type: string
                  scopes:
                    type: array
                    items:
                      type: string
                  created_at:
                    type: string
                    format: date-time

        "401":
          $ref: "#/components/responses/Unauthorized"
  # ─── Widget Configuration ───────────────────────────────────

  /tenants/{id}/widget-config:
    get:
      operationId: getWidgetConfig
      summary: Get tenant widget configuration
      tags: [Widget Config]
      description: |
        Retrieves the widget configuration for a tenant. Returns the latest
        version by default, or a specific version if the version query parameter is provided.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Widget configuration
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WidgetConfig"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

    put:
      operationId: updateWidgetConfig
      summary: Update tenant widget configuration
      tags: [Widget Config]
      description: |
        Updates the widget configuration for a tenant. Creates a new versioned
        record. IdP credentials are stored in AWS Secrets Manager.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WidgetConfigUpdate"
      responses:
        "200":
          description: Configuration updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WidgetConfig"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /tenants/{id}/widget-config/validate-idp:
    post:
      operationId: validateIdpConnection
      summary: Validate an IdP connection
      tags: [Widget Config]
      description: |
        Validates an Identity Provider configuration by attempting a client
        credentials flow against the specified provider (Entra ID, Auth0, Okta, or eIDAS).

        **Alias support:** Accepts both `provider` (canonical) and `idpType` (legacy).
        If both are present, `provider` takes precedence.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                provider:
                  type: string
                  enum: [eidas_eudi, entra_id, auth0, okta]
                  description: "Canonical field. The IdP type to validate."
                idpType:
                  type: string
                  deprecated: true
                  description: "**Deprecated** legacy alias for `provider`."
                config:
                  type: object
              required: [config]
      responses:
        "200":
          description: Validation result
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: object
                    additionalProperties:
                      type: object
                      properties:
                        valid:
                          type: boolean
                        message:
                          type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /tenants/{id}/widget-config/versions:
    get:
      operationId: listConfigVersions
      summary: List widget configuration versions
      tags: [Widget Config]
      description: |
        Returns a paginated list of all configuration versions for the tenant,
        sorted by version number descending (newest first). Each entry contains
        version metadata (not the full configuration body).
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        "200":
          description: List of configuration versions
          content:
            application/json:
              schema:
                type: object
                properties:
                  versions:
                    type: array
                    items:
                      type: object
                      properties:
                        configVersion:
                          type: integer
                        updatedAt:
                          type: string
                          format: date-time
                        createdAt:
                          type: string
                          format: date-time
                        status:
                          type: string
                        widgetId:
                          type: string
                        rollbackFrom:
                          type: integer
                          description: Present if this version was created by a rollback
                  next_cursor:
                    type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /tenants/{id}/widget-config/rollback:
    post:
      operationId: rollbackConfig
      summary: Rollback widget configuration to a previous version
      tags: [Widget Config]
      description: |
        Rolls back the widget configuration to a previous version by creating a
        new version that is a copy of the target version. This preserves the
        full version history for audit purposes — no versions are deleted.

        The new version includes a `rollbackFrom` field indicating which version
        it was copied from.
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [version]
              properties:
                version:
                  type: integer
                  description: The version number to roll back to
                  minimum: 1
      responses:
        "200":
          description: Rollback successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  version:
                    type: integer
                    description: The new version number created by the rollback
                  rollbackFrom:
                    type: integer
                    description: The version number that was rolled back to
                  widgetId:
                    type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /widgets/{widgetId}/config:
    get:
      operationId: getPublicWidgetConfig
      summary: Public widget config for embed (no auth)
      tags: [Public]
      description: |
        Returns the public-safe widget configuration for embedding. No authentication
        required. Sensitive fields (secrets, internal IDs) are stripped from the response.
      security: []
      parameters:
        - name: widgetId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Public widget configuration
          content:
            application/json:
              schema:
                type: object
                properties:
                  widget_id:
                    type: string
                  tenant_name:
                    type: string
                  branding:
                    type: object
                  enabled_idps:
                    type: array
                    items:
                      type: string
                  taxonomy_libraries:
                    type: array
                    items:
                      type: string
        "404":
          $ref: "#/components/responses/NotFound"

# ═══════════════════════════════════════════════════════════════
# COMPONENTS
# ═══════════════════════════════════════════════════════════════

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Api-Key
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  parameters:
    MandateId:
      name: id
      in: path
      required: true
      schema:
        type: string
        example: "mdt_01HXYZ..."
    Limit:
      name: limit
      in: query
      schema:
        type: integer
        default: 50
        minimum: 1
        maximum: 200
    Cursor:
      name: cursor
      in: query
      schema:
        type: string
        description: Base64url-encoded pagination cursor

  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Authentication required
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: Insufficient permissions
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Conflict:
      description: State conflict
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  schemas:
    # ─── Core Schemas ─────────────────────────────────────────

    Subject:
      type: object
      description: Represents an entity (human, AI agent, service, or organization) that participates in a mandate as either principal or delegate.
      required: [type, subject_id]
      properties:
        type:
          type: string
          enum: [HUMAN, AI_AGENT, SERVICE, ORGANIZATION]
        subject_id:
          type: string
          example: "monco:agent:validate-agent-v2"
        display_name:
          type: string
        identity_provider:
          type: string
        identity_token_hash:
          type: string

    Scope:
      type: object
      description: Defines the actions, resources, and effect (ALLOW/DENY) that a mandate authorizes, with optional conditions.
      required: [actions, resources]
      properties:
        actions:
          type: array
          items:
            type: string
          example: ["construction.validation.approve"]
        resources:
          type: array
          items:
            type: string
          example: ["monco:project:*/zone:*/installation:*"]
        effect:
          type: string
          enum: [ALLOW, DENY]
          default: ALLOW
        conditions:
          type: object

    Constraints:
      type: object
      description: Optional restrictions on mandate usage including time windows, geographic fencing, MFA requirements, IP whitelisting, and escalation rules.
      properties:
        max_uses:
          type: integer
        time_window:
          type: object
          properties:
            start:
              type: string
              format: date-time
            end:
              type: string
              format: date-time
        geo_fence:
          type: object
          properties:
            allowed_regions:
              type: array
              items:
                type: string
        require_mfa:
          type: boolean
        ip_whitelist:
          type: array
          items:
            type: string
        escalation_rules:
          type: object
          properties:
            deviation_above_percent:
              type: number
            escalate_to:
              type: string
            escalation_method:
              type: string

    Proof:
      type: object
      description: Cryptographic proof of mandate creation, including the creation method, issuer, mandate hash, and KMS signature.
      properties:
        creation_method:
          type: string
          enum: [API_KEY, EUDI_WALLET, OAUTH2, MANUAL]
        issuer:
          type: string
        created_by:
          type: string
        mandate_hash:
          type: string
        signature:
          type: string
        eidas_loa:
          type: string

    Mandate:
      type: object
      description: A verifiable delegation of authority from a principal to a delegate, with defined scope, constraints, and lifecycle status.
      properties:
        mandate_id:
          type: string
          example: "mdt_01HXYZ..."
        tenant_id:
          type: string
        version:
          type: integer
        status:
          type: string
          enum: [DRAFT, ACTIVE, SUSPENDED, REVOKED, EXPIRED]
        principal:
          $ref: "#/components/schemas/Subject"
        delegate:
          $ref: "#/components/schemas/Subject"
        scope:
          $ref: "#/components/schemas/Scope"
        constraints:
          $ref: "#/components/schemas/Constraints"
        proof:
          $ref: "#/components/schemas/Proof"
        metadata:
          type: object
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time

    CreateMandateRequest:
      type: object
      description: Request body for creating a new mandate. Requires principal, delegate, and scope. Optionally includes constraints, metadata, expiration, and taxonomy reference.
      required: [principal, delegate, scope]
      properties:
        principal:
          $ref: "#/components/schemas/Subject"
        delegate:
          $ref: "#/components/schemas/Subject"
        scope:
          $ref: "#/components/schemas/Scope"
        constraints:
          $ref: "#/components/schemas/Constraints"
        metadata:
          type: object
        expires_at:
          type: string
          format: date-time
        taxonomy_id:
          type: string
          description: Taxonomy identifier to validate the mandate scope against (e.g., "construction", "realestate", "venture").
          example: "construction"
        taxonomy_version:
          type: string
          description: |
            Versioned taxonomy reference. Accepts either a plain ID (uses latest version)
            or `id@semver` format (validates exact version match).
          pattern: "^[a-z][a-z0-9-]*(@\\d+\\.\\d+\\.\\d+(-[a-z0-9.]+)?)?$"
          example: "construction@2.0.0"
        require_approval:
          type: boolean
          description: When true, the mandate starts in PENDING_APPROVAL status and requires explicit approval before becoming ACTIVE.
          default: false

    SystemMode:
      type: object
      description: Current system operational mode with metadata.
      properties:
        mode:
          type: string
          enum: [growth, sustain, maintenance, frozen]
          example: "growth"
        reason:
          type: string
          example: "Default operational mode"
        changed_at:
          type: string
          format: date-time
          nullable: true
        changed_by:
          type: string
          nullable: true
        estimated_duration_minutes:
          type: integer
          nullable: true

    StatusChange:
      type: object
      description: Response returned when a mandate's status is changed (revoked, suspended, or reactivated).
      properties:
        mandate_id:
          type: string
        status:
          type: string
        updated_at:
          type: string
          format: date-time

    VerifyRequest:
      type: object
      description: |
        Request body for the real-time verification endpoint. Specifies the delegate, action,
        and resource to check against active mandates.

        **Alias support:** The server accepts `agent_id` as a legacy alias for
        `delegate_subject_id`. If both fields are present, `delegate_subject_id`
        takes precedence. New integrations should always use `delegate_subject_id`.
      required: [action, resource]
      properties:
        delegate_subject_id:
          type: string
          description: "Canonical field. Subject ID of the delegate to verify."
          example: "monco:agent:validate-agent-v2"
        agent_id:
          type: string
          description: "**Deprecated** legacy alias for `delegate_subject_id`. Use `delegate_subject_id` for new integrations."
          example: "monco:agent:validate-agent-v2"
          deprecated: true
        action:
          type: string
          example: "construction.validation.approve"
        resource:
          type: string
          example: "monco:project:proj_ABC123/zone:EG/installation:stk_42"
        context:
          type: object
          description: |
            Additional context for constraint evaluation. When a mandate has
            `require_mfa: true`, the caller must supply MFA proof via one of:
            - `amr` (array of strings) — must include `"mfa"`
            - `loa` (string) — must be `"SUBSTANTIAL"` or `"HIGH"`
            - `mfa_timestamp` (string, ISO 8601) — timestamp of last MFA verification

    VerifyResponse:
      type: object
      description: Verification result containing the ALLOW/DENY decision, matching mandate ID, event ID, reason codes, and remaining constraints.
      properties:
        decision:
          type: string
          enum: [ALLOW, DENY]
        mandate_id:
          type: string
        event_id:
          type: string
        reason_codes:
          type: array
          description: "Reason codes for DENY decisions."
          items:
            type: string
            enum: [NO_MATCHING_MANDATE, ESCALATION_REQUIRED, MFA_REQUIRED, APPROVAL_REQUIRED]
        constraints_remaining:
          type: object

    VerifyResponseWithPoM:
      description: |
        Extended verification response that optionally includes a
        Proof-of-Mandate Verifiable Credential (SD-JWT VC).

        **Alias support:** The response includes `proof_token` as a legacy
        alias for `proof_of_mandate.compact`. New integrations should use
        `proof_of_mandate.compact`.
      allOf:
        - $ref: "#/components/schemas/VerifyResponse"
        - type: object
          properties:
            proof_of_mandate:
              $ref: "#/components/schemas/ProofOfMandateVC"
            proof_token:
              type: string
              deprecated: true
              description: "**Deprecated** legacy alias for `proof_of_mandate.compact`. Contains the SD-JWT compact serialization string."

    ProofOfMandateVC:
      type: object
      description: |
        A W3C Verifiable Credential in SD-JWT format proving the
        outcome of a mandate verification. The compact form is the
        authoritative representation; the payload is provided for
        convenience only.
      properties:
        compact:
          type: string
          description: SD-JWT compact serialization (header.payload.signature~disclosure1~...~)
          example: "eyJhbGciOiJQUzI1NiIsInR5cCI6InZjK3NkLWp3dCJ9.eyJ2Y3QiOiJQcm9vZk9mTWFuZGF0ZSJ9.sig~disc1~disc2~"
        payload:
          $ref: "#/components/schemas/ProofOfMandateClaims"

    ProofOfMandateClaims:
      type: object
      description: Decoded PoM VC payload (not authoritative — use compact form for verification)
      properties:
        vct:
          type: string
          enum: [ProofOfMandate]
        decision:
          type: string
          enum: [ALLOW, DENY]
        mandate_id:
          type: string
        verification_event_id:
          type: string
        verification_timestamp:
          type: string
          format: date-time
        requested_action:
          type: string
        requested_resource:
          type: string
        delegate_subject_id:
          type: string
        principal_subject_id:
          type: string
          description: Selectively disclosable
        tenant_id:
          type: string
          description: Selectively disclosable
        reason_codes:
          type: array
          items:
            type: string
          description: Selectively disclosable
        constraints_snapshot:
          type: object
          description: Selectively disclosable
        latency_ms:
          type: number
          description: Selectively disclosable
        iss:
          type: string
          description: Issuer DID
          example: "did:web:api.mandaitor.io"
        sub:
          type: string
          description: Subject (delegate_subject_id)
        iat:
          type: integer
          description: Issued-at (Unix timestamp)
        exp:
          type: integer
          description: Expiration (Unix timestamp)
        _sd_alg:
          type: string
          description: "Selective-disclosure hash algorithm"
          example: "sha-256"

    AuditEvent:
      type: object
      description: An immutable, hash-chained audit event recording a mandate lifecycle action or verification decision.
      properties:
        event_id:
          type: string
        tenant_id:
          type: string
        mandate_id:
          type: string
        event_type:
          type: string
          enum:
            - MANDATE_CREATED
            - MANDATE_SUSPENDED
            - MANDATE_REACTIVATED
            - MANDATE_REVOKED
            - MANDATE_EXPIRED
            - VERIFICATION_ALLOWED
            - VERIFICATION_DENIED
            - ESCALATION_TRIGGERED
        timestamp:
          type: string
          format: date-time
        actor:
          $ref: "#/components/schemas/Subject"
        details:
          type: object
        previous_event_hash:
          type: string
        event_hash:
          type: string
        kms_signature:
          type: string
        hash_algorithm:
          type: string
          description: "Algorithm used to compute event_hash (e.g., SHA_256). Present from crypto-agility v1."
          example: "SHA_256"
        signing_algorithm:
          type: string
          description: "Algorithm used to produce kms_signature (e.g., RSASSA_PSS_SHA_256). Present from crypto-agility v1."
          example: "RSASSA_PSS_SHA_256"

    # ─── Evidence Pack ────────────────────────────────────────

    EvidencePack:
      type: object
      description: |
        Court-ready evidence pack containing all artifacts needed to
        independently verify a mandate decision. Includes hash-chained
        event log, mandate snapshot, proof tokens, and issuer DID reference.
      properties:
        schema_version:
          type: string
          description: Schema version for forward compatibility
          example: "1.1.0"
        export_timestamp:
          type: string
          format: date-time
        tenant_id:
          type: string
        mandate_id:
          type: string
        mandate_snapshot:
          type: object
          description: Full mandate state at export time
        event_chain:
          type: array
          items:
            $ref: "#/components/schemas/AuditEvent"
          description: Hash-chained audit event log
        event_count:
          type: integer
        case_log_hash:
          type: string
          description: Hash over canonical event chain JSON (algorithm in crypto_metadata)
        chain_integrity:
          type: object
          properties:
            first_event_hash:
              type: string
            last_event_hash:
              type: string
            genesis_verified:
              type: boolean
        issuer:
          type: object
          properties:
            did:
              type: string
              example: "did:web:api.mandaitor.io"
            did_document_url:
              type: string
              format: uri
              example: "https://api.mandaitor.io/.well-known/did.json"
        proof_tokens:
          type: array
          items:
            type: string
          description: SD-JWT compact tokens from verification events
        crypto_metadata:
          type: object
          description: Cryptographic algorithms active at export time
          properties:
            hash_algorithm:
              type: string
              description: "Algorithm used for event chain hashing"
              example: "SHA_256"
            signing_algorithm:
              type: string
              description: "Algorithm used for event signing"
              example: "RSASSA_PSS_SHA_256"
            case_log_hash_algorithm:
              type: string
              description: "Algorithm used for the case_log_hash"
              example: "SHA_256"
        metadata:
          type: object
          properties:
            mandate_created_at:
              type: string
              format: date-time
            mandate_status:
              type: string
            export_requested_by:
              type: string
            event_id_filter:
              type: string

    # Pagination schema removed — Lambda handlers return flat { items, next_cursor }
    # instead of the nested { mandates/events, pagination: { count, cursor, has_more } } shape.

    # ─── Onboarding Schemas ───────────────────────────────────

    AccessRequest:
      type: object
      description: Request body for submitting a new tenant access request during onboarding.
      required: [company_name, contact_email, use_case]
      properties:
        company_name:
          type: string
          example: "monco GmbH"
        contact_name:
          type: string
          example: "Max Mustermann"
        contact_email:
          type: string
          format: email
          example: "max@monco.ai"
        use_case:
          type: string
          example: "AI agent delegation for construction validation"
        industry:
          type: string
          enum: [construction, real_estate, venture_capital, other]
        website:
          type: string
          format: uri

    # AccessRequestRecord removed — was unused (redocly no-unused-components).
    # Re-add when /admin/onboarding list endpoint is implemented.

    TenantRecord:
      type: object
      description: A tenant record containing the tenant's identity, plan, status, and API key metadata.
      properties:
        tenant_id:
          type: string
        company_name:
          type: string
        plan:
          type: string
        status:
          type: string
          enum: [ACTIVE, SUSPENDED, DEACTIVATED]
        created_at:
          type: string
          format: date-time
        api_keys:
          type: array
          items:
            type: object
            properties:
              key_id:
                type: string
              name:
                type: string
              scopes:
                type: array
                items:
                  type: string
              created_at:
                type: string
                format: date-time

    # ─── Widget Config Schemas ────────────────────────────────

    WidgetConfig:
      type: object
      description: Full widget configuration for a tenant, including identity providers, taxonomy libraries, branding, approval workflows, and webhook settings.
      properties:
        tenant_id:
          type: string
        widget_id:
          type: string
        identity_providers:
          type: array
          items:
            type: object
            properties:
              provider:
                type: string
                enum: [eidas_eudi, entra_id, auth0, okta]
              enabled:
                type: boolean
              config:
                type: object
        taxonomy_libraries:
          type: array
          items:
            type: string
          example: ["@mandaitor/taxonomy-construction"]
        mandate_templates:
          type: array
          items:
            type: object
        branding:
          type: object
          properties:
            primary_color:
              type: string
            logo_url:
              type: string
            company_name:
              type: string
        approval_workflow:
          type: object
          properties:
            require_principal_approval:
              type: boolean
            require_admin_approval:
              type: boolean
            auto_approve_trusted_idps:
              type: boolean
        webhook_url:
          type: string
          format: uri
        updated_at:
          type: string
          format: date-time

    WidgetConfigUpdate:
      type: object
      description: Request body for updating a tenant's widget configuration. All fields are optional; only provided fields are updated.
      properties:
        identity_providers:
          type: array
          items:
            type: object
        taxonomy_libraries:
          type: array
          items:
            type: string
        mandate_templates:
          type: array
          items:
            type: object
        branding:
          type: object
        approval_workflow:
          type: object
        webhook_url:
          type: string
          format: uri

    # ─── Error Schema ─────────────────────────────────────────

    # ─── Identity Provider Configuration Schemas ───────────────

    TenantIdpConfig:
      type: object
      properties:
        tenant_id:
          type: string
        identity_providers:
          $ref: "#/components/schemas/TenantIdpConfigUpdate"

    TenantIdpConfigUpdate:
      type: object
      required: [enabled_providers]
      properties:
        enabled_providers:
          type: array
          items:
            type: string
            enum:
              [
                API_KEY,
                COGNITO,
                EUDI_WALLET,
                AUTH0,
                OKTA,
                ENTRA_ID,
                GOOGLE,
                AWS_IAM_IDC,
                GENERIC_OIDC,
              ]
        auth0:
          type: object
          properties:
            domain:
              type: string
              example: "your-tenant.auth0.com"
            audience:
              type: string
            client_id:
              type: string
        okta:
          type: object
          properties:
            issuer:
              type: string
              example: "https://your-org.okta.com/oauth2/default"
            audience:
              type: string
        entra:
          type: object
          properties:
            tenant_id:
              type: string
              format: uuid
            client_id:
              type: string
              format: uuid
        eudi:
          type: object
          properties:
            rp_id:
              type: string
            presentation_policies:
              type: array
              items:
                type: string
        google:
          type: object
          properties:
            client_id:
              type: string
              example: "xxxx.apps.googleusercontent.com"
            hosted_domain:
              type: string
              description: Restrict to Google Workspace org (optional)
              example: "company.com"
        aws_iam_idc:
          type: object
          properties:
            issuer:
              type: string
              format: uri
              example: "https://identitycenter.amazonaws.com/ssoins-xxxxx"
            audience:
              type: string
        generic_oidc:
          type: object
          required: [name, issuer, audience, subject_prefix]
          properties:
            name:
              type: string
              description: Display name for the provider
              example: "Keycloak"
            issuer:
              type: string
              format: uri
              description: OIDC issuer URL (must serve /.well-known/openid-configuration)
              example: "https://auth.company.com/realms/production"
            audience:
              type: string
              example: "mandaitor-client"
            subject_prefix:
              type: string
              description: Prefix for canonical subject IDs (e.g., keycloak → oidc:keycloak:<sub>)
              pattern: "^[a-z][a-z0-9-]*$"
              example: "keycloak"

    TokenExchangeResponse:
      type: object
      properties:
        access_token:
          type: string
        token_type:
          type: string
          enum: [Bearer]
        expires_in:
          type: integer
          example: 3600
        scope:
          type: string
        issued_token_type:
          type: string
        delegation:
          type: object
          properties:
            mandate_id:
              type: string
            principal:
              type: string
            delegate:
              type: string
            provider:
              type: string

    # ─── EUDI Wallet Schemas ──────────────────────────────────

    EudiSessionResponse:
      type: object
      description: Response from initiating an EUDI Wallet verification session.
      properties:
        session_id:
          type: string
          example: "eudi_01HXYZ..."
        status:
          type: string
          enum: [PENDING]
        request_uri:
          type: string
          format: uri
          description: URL where the wallet fetches the Authorization Request Object
        qr_code_uri:
          type: string
          description: URI to encode as QR code (cross-device flow)
        deep_link_uri:
          type: string
          description: URI to open as deep link (same-device flow)
        expires_at:
          type: string
          format: date-time

    EudiSessionStatus:
      type: object
      description: Status of an EUDI Wallet verification session.
      properties:
        session_id:
          type: string
        status:
          type: string
          enum: [PENDING, COMPLETED, FAILED, EXPIRED]
        created_at:
          type: string
          format: date-time
        presentation_definition_id:
          type: string
        resolved_identity:
          type: object
          description: Resolved identity from the EUDI Wallet (present when COMPLETED)
          properties:
            subject_id:
              type: string
              example: "eudi:DE/1234567890abcdef"
            subject_type:
              type: string
              enum: [NATURAL_PERSON]
            provider:
              type: string
              enum: [EUDI_WALLET]
            display_name:
              type: string
            assurance_level:
              type: string
              enum: [HIGH]
            eidas_attributes:
              $ref: "#/components/schemas/EidasAttributes"
        verified_at:
          type: string
          format: date-time
        errors:
          type: array
          items:
            type: string

    EidasAttributes:
      type: object
      description: eIDAS 2.0 Person Identification Data attributes.
      properties:
        unique_id:
          type: string
          description: Unique persistent identifier from the Member State
        family_name:
          type: string
        given_name:
          type: string
        birth_date:
          type: string
          format: date
        nationality:
          type: string
          description: ISO 3166-1 alpha-2
        pid_issuer:
          type: string
        issuing_country:
          type: string
          description: ISO 3166-1 alpha-2
        assurance_level:
          type: string
          enum: [LOW, SUBSTANTIAL, HIGH]

    Error:
      type: object
      description: Standard error response containing an error code and human-readable message.
      properties:
        error:
          type: string
          example: "BAD_REQUEST"
        message:
          type: string
          example: "principal and delegate are required"
