openapi: 3.1.0
info:
  title: Wallet Backend Admin API
  description: |
    Internal administration API for managing the wallet backend multi-tenant infrastructure.
    
    This API is intended to be exposed on a separate port (default: 8081) and should only be
    accessible from internal networks or trusted management systems.
    
    ## Multi-Tenancy Model
    
    The wallet backend supports multiple tenants, each with their own isolated:
    - Users and credentials
    - Credential issuers (where users can request credentials)
    - Verifiers (where users can present credentials)
    
    Users are associated with tenants through memberships. Each user can belong to multiple
    tenants with different roles (user, admin).
    
    ## OIDC Gate
    
    Tenants can optionally require users to authenticate with an enterprise Identity Provider
    (OpenID Connect) before accessing registration and/or login endpoints. The gate supports:
    - `registration` mode: OIDC required for new wallet registrations
    - `login` mode: OIDC required for wallet logins
    - `both` mode: OIDC required for registration and login (with separate OPs per operation)
    - `none` mode: default open behavior (no OIDC pre-authentication)
    
    When enabled, a separate `registration_op` and/or `login_op` (OIDC Provider Config) must
    be supplied. An optional `bind_identity` flag permanently links the enterprise IdP subject
    to the wallet user so that subsequent logins can verify the same enterprise identity.
    
    ## Verifier Client ID
    
    Verifiers may carry an optional `client_id` and `client_id_scheme` that the wallet uses
    when matching incoming OID4VP authorization requests to a trusted verifier. Supported
    schemes follow the OID4VP specification: `redirect_uri`, `pre-registered`, `x509_san_dns`,
    `x509_san_uri`, `verifier_attestation`, and `did`.
    
    ## Security
    
    All `/admin/*` routes (except `/admin/status`) require bearer token authentication via the
    `Authorization: Bearer <token>` header. Set `WALLET_SERVER_ADMIN_TOKEN` to configure a
    fixed token; if not set, a random token is generated at startup and logged. In production,
    you should also:
    - Only expose this API on internal networks
    - Use network-level access control (firewalls, VPNs)
    - Set a strong, persistent admin token via environment variable
  version: 1.1.0
  contact:
    name: SIROS Foundation
    url: https://siros.org
  license:
    name: Apache 2.0
    url: https://www.apache.org/licenses/LICENSE-2.0

servers:
  - url: http://localhost:8081
    description: Local development admin server
  - url: http://admin.wallet.internal:8081
    description: Internal admin server (example)

security:
  - bearerAuth: []

tags:
  - name: Status
    description: Health check and status endpoints
  - name: Tenants
    description: Tenant management operations
  - name: Users
    description: Tenant user membership management
  - name: Issuers
    description: Credential issuer management per tenant
  - name: Verifiers
    description: Verifier management per tenant
  - name: Invites
    description: Invite code management for controlled registration

paths:
  /admin/status:
    get:
      tags:
        - Status
      summary: Get admin server status
      description: Health check endpoint for the admin server
      operationId: getAdminStatus
      security: []
      responses:
        '200':
          description: Server is healthy
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusResponse'

  /admin/tenants:
    get:
      tags:
        - Tenants
      summary: List all tenants
      description: Returns a list of all tenants in the system
      operationId: listTenants
      responses:
        '200':
          description: List of tenants
          content:
            application/json:
              schema:
                type: object
                properties:
                  tenants:
                    type: array
                    items:
                      $ref: '#/components/schemas/TenantResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
    
    post:
      tags:
        - Tenants
      summary: Create a new tenant
      description: |
        Creates a new tenant with the specified configuration.
        
        Tenant IDs must:
        - Be lowercase alphanumeric with hyphens
        - Start with a letter
        - Be between 2-50 characters
      operationId: createTenant
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TenantRequest'
            example:
              id: acme-corp
              name: ACME Corporation
              display_name: ACME Corp Wallet
              enabled: true
      responses:
        '201':
          description: Tenant created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '409':
          description: Tenant already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

  /admin/tenants/{tenantId}:
    parameters:
      - $ref: '#/components/parameters/tenantId'
    
    get:
      tags:
        - Tenants
      summary: Get a specific tenant
      description: Returns details of a specific tenant
      operationId: getTenant
      responses:
        '200':
          description: Tenant details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
    
    put:
      tags:
        - Tenants
      summary: Update a tenant
      description: Updates an existing tenant's configuration
      operationId: updateTenant
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TenantRequest'
      responses:
        '200':
          description: Tenant updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
    
    delete:
      tags:
        - Tenants
      summary: Delete a tenant
      description: |
        Deletes a tenant and all associated data.
        
        **Warning:** This is a destructive operation. All users, credentials,
        issuers, and verifiers associated with this tenant will be deleted.
        
        The default tenant cannot be deleted.
      operationId: deleteTenant
      responses:
        '200':
          description: Tenant deleted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '403':
          description: Cannot delete the default tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

  /admin/tenants/{tenantId}/users:
    parameters:
      - $ref: '#/components/parameters/tenantId'
    
    get:
      tags:
        - Users
      summary: List users in a tenant
      description: Returns a list of all user IDs that are members of the tenant
      operationId: getTenantUsers
      responses:
        '200':
          description: List of user IDs
          content:
            application/json:
              schema:
                type: object
                properties:
                  users:
                    type: array
                    items:
                      type: string
                      format: uuid
                    example:
                      - "550e8400-e29b-41d4-a716-446655440000"
                      - "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
    
    post:
      tags:
        - Users
      summary: Add a user to a tenant
      description: |
        Adds an existing user to a tenant with the specified role.
        
        Valid roles are:
        - `user` - Standard user access (default)
        - `admin` - Administrative access within the tenant
      operationId: addUserToTenant
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - user_id
              properties:
                user_id:
                  type: string
                  format: uuid
                  description: The UUID of the user to add
                role:
                  type: string
                  enum: [user, admin]
                  default: user
                  description: The role to assign to the user
            example:
              user_id: "550e8400-e29b-41d4-a716-446655440000"
              role: user
      responses:
        '200':
          description: User added to tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

  /admin/tenants/{tenantId}/users/{userId}:
    parameters:
      - $ref: '#/components/parameters/tenantId'
      - $ref: '#/components/parameters/userId'
    
    delete:
      tags:
        - Users
      summary: Remove a user from a tenant
      description: Removes a user's membership from the tenant
      operationId: removeUserFromTenant
      responses:
        '200':
          description: User removed from tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

  /admin/tenants/{tenantId}/issuers:
    parameters:
      - $ref: '#/components/parameters/tenantId'
    
    get:
      tags:
        - Issuers
      summary: List credential issuers for a tenant
      description: |
        Returns all credential issuers configured for the tenant.
        
        Issuers define where users can request verifiable credentials from.
        The wallet frontend uses this information to display available
        credential sources to users.
      operationId: listIssuers
      responses:
        '200':
          description: List of issuers
          content:
            application/json:
              schema:
                type: object
                properties:
                  issuers:
                    type: array
                    items:
                      $ref: '#/components/schemas/IssuerResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
    
    post:
      tags:
        - Issuers
      summary: Create a credential issuer
      description: |
        Adds a new credential issuer to the tenant.
        
        The `credential_issuer_identifier` is the URL of an OpenID4VCI-compliant
        credential issuer. The wallet will fetch the issuer's metadata from
        `{credential_issuer_identifier}/.well-known/openid-credential-issuer`
        to display available credentials.
      operationId: createIssuer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IssuerRequest'
            example:
              credential_issuer_identifier: "https://issuer.example.com"
              client_id: "wallet-client"
              visible: true
      responses:
        '201':
          description: Issuer created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IssuerResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Issuer with this identifier already exists in tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

  /admin/tenants/{tenantId}/issuers/{issuerId}:
    parameters:
      - $ref: '#/components/parameters/tenantId'
      - $ref: '#/components/parameters/issuerId'
    
    get:
      tags:
        - Issuers
      summary: Get a specific issuer
      description: Returns details of a specific credential issuer
      operationId: getIssuer
      responses:
        '200':
          description: Issuer details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IssuerResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
    
    put:
      tags:
        - Issuers
      summary: Update an issuer
      description: Updates an existing credential issuer's configuration
      operationId: updateIssuer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IssuerRequest'
      responses:
        '200':
          description: Issuer updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IssuerResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
    
    delete:
      tags:
        - Issuers
      summary: Delete an issuer
      description: Removes a credential issuer from the tenant
      operationId: deleteIssuer
      responses:
        '200':
          description: Issuer deleted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

  /admin/tenants/{tenantId}/verifiers:
    parameters:
      - $ref: '#/components/parameters/tenantId'
    
    get:
      tags:
        - Verifiers
      summary: List verifiers for a tenant
      description: |
        Returns all verifiers configured for the tenant.
        
        Verifiers define where users can present their credentials.
        The wallet frontend uses this information to display available
        presentation destinations to users.
      operationId: listVerifiers
      responses:
        '200':
          description: List of verifiers
          content:
            application/json:
              schema:
                type: object
                properties:
                  verifiers:
                    type: array
                    items:
                      $ref: '#/components/schemas/VerifierResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
    
    post:
      tags:
        - Verifiers
      summary: Create a verifier
      description: |
        Adds a new verifier to the tenant.
        
        Verifiers are services that can request credential presentations from
        users. The name and URL are displayed to users when they choose where
        to present their credentials.
        
        An optional `client_id` and `client_id_scheme` may be provided so that
        the wallet can match incoming OID4VP authorization requests from this
        verifier. When `client_id_scheme` is set, `client_id` is required.
      operationId: createVerifier
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifierRequest'
            example:
              name: "University Portal"
              url: "https://verifier.university.edu"
              client_id: "did:web:verifier.university.edu"
              client_id_scheme: "did"
      responses:
        '201':
          description: Verifier created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifierResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Verifier with this URL already exists in tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

  /admin/tenants/{tenantId}/verifiers/{verifierId}:
    parameters:
      - $ref: '#/components/parameters/tenantId'
      - $ref: '#/components/parameters/verifierId'
    
    get:
      tags:
        - Verifiers
      summary: Get a specific verifier
      description: Returns details of a specific verifier
      operationId: getVerifier
      responses:
        '200':
          description: Verifier details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifierResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
    
    put:
      tags:
        - Verifiers
      summary: Update a verifier
      description: Updates an existing verifier's configuration
      operationId: updateVerifier
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifierRequest'
      responses:
        '200':
          description: Verifier updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifierResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
    
    delete:
      tags:
        - Verifiers
      summary: Delete a verifier
      description: Removes a verifier from the tenant
      operationId: deleteVerifier
      responses:
        '200':
          description: Verifier deleted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

  /admin/tenants/{tenantId}/invites:
    parameters:
      - $ref: '#/components/parameters/tenantId'
    get:
      tags:
        - Invites
      summary: List invites for a tenant
      description: Returns all invite codes for the tenant, ordered by creation date (newest first).
      operationId: listInvites
      responses:
        '200':
          description: List of invites
          content:
            application/json:
              schema:
                type: object
                properties:
                  invites:
                    type: array
                    items:
                      $ref: '#/components/schemas/InviteResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags:
        - Invites
      summary: Create an invite code
      description: |
        Creates a new invite code for the tenant. If no `code` is provided in the
        request body, a 256-bit cryptographically random value encoded as base64url
        is generated. The code is returned only once in the response — it cannot be
        retrieved again.
      operationId: createInvite
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InviteRequest'
      responses:
        '201':
          description: Invite created (includes code)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InviteResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

  /admin/tenants/{tenantId}/invites/{inviteId}:
    parameters:
      - $ref: '#/components/parameters/tenantId'
      - $ref: '#/components/parameters/inviteId'
    get:
      tags:
        - Invites
      summary: Get a specific invite
      description: Returns details of a specific invite. The code field is never included in GET responses.
      operationId: getInvite
      responses:
        '200':
          description: Invite details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InviteResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
    put:
      tags:
        - Invites
      summary: Renew or revoke an invite
      description: |
        Action-based update. Supported actions:
        - `renew`: Reset to active status and extend expiry (cannot renew completed invites)
        - `revoke`: Mark as revoked, preventing further use (cannot revoke completed invites)
      operationId: updateInvite
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateInviteRequest'
      responses:
        '200':
          description: Invite updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InviteResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Cannot modify a completed invite
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      tags:
        - Invites
      summary: Delete an invite
      description: Hard-deletes an invite code from the system.
      operationId: deleteInvite
      responses:
        '200':
          description: Invite deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

components:
  parameters:
    tenantId:
      name: tenantId
      in: path
      required: true
      description: The unique identifier of the tenant
      schema:
        type: string
        pattern: '^[a-z][a-z0-9-]{1,49}$'
      example: acme-corp
    
    userId:
      name: userId
      in: path
      required: true
      description: The UUID of the user
      schema:
        type: string
        format: uuid
      example: "550e8400-e29b-41d4-a716-446655440000"
    
    issuerId:
      name: issuerId
      in: path
      required: true
      description: The numeric ID of the issuer
      schema:
        type: integer
        format: int64
      example: 1
    
    verifierId:
      name: verifierId
      in: path
      required: true
      description: The numeric ID of the verifier
      schema:
        type: integer
        format: int64
      example: 1

    inviteId:
      name: inviteId
      in: path
      required: true
      description: The UUID of the invite
      schema:
        type: string
        format: uuid
      example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"

  schemas:
    StatusResponse:
      type: object
      properties:
        status:
          type: string
          example: ok
        service:
          type: string
          example: wallet-backend-admin

    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          description: Error message
      example:
        error: "Resource not found"

    MessageResponse:
      type: object
      properties:
        message:
          type: string
          description: Success message
      example:
        message: "Operation completed successfully"

    TenantRequest:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: string
          pattern: '^[a-z][a-z0-9-]{1,49}$'
          description: |
            Unique tenant identifier. Must be lowercase alphanumeric with hyphens,
            start with a letter, and be 2-50 characters.
          example: acme-corp
        name:
          type: string
          description: Human-readable tenant name
          example: ACME Corporation
        display_name:
          type: string
          description: Display name shown to users
          example: ACME Corp Digital Wallet
        enabled:
          type: boolean
          default: true
          description: Whether the tenant is enabled
        require_invite:
          type: boolean
          default: false
          description: Whether new users must supply a valid invite code to register
        trust_config:
          $ref: '#/components/schemas/TrustConfig'
        oidc_gate:
          $ref: '#/components/schemas/OIDCGateConfig'

    TenantResponse:
      type: object
      properties:
        id:
          type: string
          example: acme-corp
        name:
          type: string
          example: ACME Corporation
        display_name:
          type: string
          example: ACME Corp Digital Wallet
        enabled:
          type: boolean
          example: true
        require_invite:
          type: boolean
          example: false
          description: Whether new users must supply a valid invite code to register
        trust_config:
          $ref: '#/components/schemas/TrustConfig'
        oidc_gate:
          $ref: '#/components/schemas/OIDCGateConfig'
        created_at:
          type: string
          format: date-time
          example: "2025-01-15T10:30:00Z"
        updated_at:
          type: string
          format: date-time
          example: "2025-01-16T14:20:00Z"

    IssuerRequest:
      type: object
      required:
        - credential_issuer_identifier
      properties:
        credential_issuer_identifier:
          type: string
          format: uri
          description: |
            The URL of the OpenID4VCI credential issuer. The wallet will fetch
            metadata from `{url}/.well-known/openid-credential-issuer`.
          example: "https://issuer.example.com"
        client_id:
          type: string
          description: |
            OAuth2 client ID to use when authenticating with this issuer.
            If not specified, the wallet may use dynamic client registration.
          example: "wallet-client-id"
        visible:
          type: boolean
          default: true
          description: Whether this issuer should be visible to users in the wallet UI

    IssuerResponse:
      type: object
      properties:
        id:
          type: integer
          format: int64
          example: 1
        tenant_id:
          type: string
          example: acme-corp
        credential_issuer_identifier:
          type: string
          format: uri
          example: "https://issuer.example.com"
        client_id:
          type: string
          example: "wallet-client-id"
        visible:
          type: boolean
          example: true
        trust_status:
          type: string
          enum: [trusted, untrusted, unknown]
          description: Trust evaluation result for this issuer
          example: trusted
        trust_framework:
          type: string
          description: Name of the trust framework used for evaluation (e.g. EUDIW)
          example: EUDIW
        trust_evaluated_at:
          type: string
          format: date-time
          description: Timestamp of the most recent trust evaluation
          example: "2025-01-16T14:20:00Z"

    VerifierRequest:
      type: object
      required:
        - name
        - url
      properties:
        name:
          type: string
          description: Human-readable name of the verifier
          example: "University Portal"
        url:
          type: string
          format: uri
          description: URL of the verifier service
          example: "https://verifier.university.edu"
        client_id:
          type: string
          description: |
            OAuth2/OID4VP client ID used by this verifier. Required when
            `client_id_scheme` is set. If omitted, the wallet falls back to
            matching by URL (`redirect_uri` scheme).
          example: "did:web:verifier.university.edu"
        client_id_scheme:
          type: string
          enum:
            - redirect_uri
            - pre-registered
            - x509_san_dns
            - x509_san_uri
            - verifier_attestation
            - did
          description: |
            OID4VP `client_id_scheme` associated with `client_id`. Must be one
            of the values defined in the OID4VP specification. When provided,
            `client_id` must also be set.
          example: "did"
      # Enforce mutual dependency: client_id is required when client_id_scheme is set
      oneOf:
        - description: No explicit scheme — client identified by redirect_uri matching
          not:
            required: [client_id_scheme]
        - description: Explicit scheme — both client_id and client_id_scheme required
          required:
            - client_id
            - client_id_scheme

    VerifierResponse:
      type: object
      properties:
        id:
          type: integer
          format: int64
          example: 1
        tenant_id:
          type: string
          example: acme-corp
        name:
          type: string
          example: "University Portal"
        url:
          type: string
          format: uri
          example: "https://verifier.university.edu"
        client_id:
          type: string
          description: OID4VP client ID for this verifier (omitted when not set)
          example: "did:web:verifier.university.edu"
        client_id_scheme:
          type: string
          enum:
            - redirect_uri
            - pre-registered
            - x509_san_dns
            - x509_san_uri
            - verifier_attestation
            - did
          description: OID4VP client_id_scheme for this verifier (omitted when not set)
          example: "did"
        trust_status:
          type: string
          enum: [trusted, untrusted, unknown]
          description: Trust evaluation result for this verifier
          example: trusted
        trust_framework:
          type: string
          description: Name of the trust framework used for evaluation (e.g. EUDIW)
          example: EUDIW

    InviteRequest:
      type: object
      properties:
        code:
          type: string
          description: |
            Optional invite code to use. If omitted, a cryptographically secure
            256-bit code (base64url, 43 characters) is generated automatically.
          example: "my-custom-invite-code"
        expires_in:
          type: integer
          description: |
            Seconds until the invite expires. Defaults to 7 days (604800 seconds)
            if not specified.
          example: 604800
        metadata:
          type: object
          description: |
            Arbitrary JSON metadata to attach to the invite (e.g. intended
            recipient email, department, notes).
          example: {"email": "user@example.com", "department": "Engineering"}

    UpdateInviteRequest:
      type: object
      required:
        - action
      properties:
        action:
          type: string
          enum: [renew, revoke]
          description: |
            The action to perform:
            - `renew`: Reset status to active and extend expiry
            - `revoke`: Mark as revoked, preventing further use
        expires_in:
          type: integer
          description: Seconds until expiry (only used with `renew`, defaults to 7 days)
          example: 604800

    InviteResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique invite identifier
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        tenant_id:
          type: string
          example: acme-corp
        code:
          type: string
          description: |
            The invite code (base64url, 43 characters). Only returned once
            at creation time \u2014 subsequent GET requests omit this field.
          example: "dGhpcyBpcyBhIHNhbXBsZSBpbnZpdGUgY29kZSB2YWw"
        status:
          type: string
          enum: [active, completed, revoked]
          description: |
            Current invite status:
            - `active`: Available for use
            - `completed`: Successfully used for registration
            - `revoked`: Administratively disabled
          example: active
        metadata:
          type: object
          description: Arbitrary metadata attached at creation
        used_by:
          type: string
          format: uuid
          description: UUID of the user who used this invite (only present when status is `completed`)
        expires_at:
          type: string
          format: date-time
          example: "2025-01-22T10:00:00Z"
        created_at:
          type: string
          format: date-time
          example: "2025-01-15T10:00:00Z"
        updated_at:
          type: string
          format: date-time
          example: "2025-01-15T10:00:00Z"

    TrustConfig:
      type: object
      description: Trust evaluation configuration for the tenant
      properties:
        trust_ttl:
          type: integer
          description: |
            How long trust evaluations remain valid, in seconds.
            Defaults to 86400 (24 hours) when not set.
          example: 86400
        pdp_url:
          type: string
          format: uri
          description: |
            AuthZEN PDP URL for this tenant's `/v1/evaluate` proxy endpoint.
            If omitted, the global `authzen_proxy.pdp_url` configuration is used.
            This allows different tenants to use different trust services.
          example: "https://pdp.trust.example.com/v1/evaluate"

    OIDCProviderConfig:
      type: object
      required:
        - issuer
        - client_id
      description: Configuration for an OIDC Identity Provider used by the OIDC gate
      properties:
        display_name:
          type: string
          description: User-friendly name shown on the IdP login button (e.g. "Corporate SSO")
          example: "Corporate SSO"
        issuer:
          type: string
          format: uri
          description: |
            OIDC issuer URL. Used for token validation and OIDC discovery
            (`{issuer}/.well-known/openid-configuration`).
          example: "https://login.acme.com"
        client_id:
          type: string
          description: |
            Public OIDC client ID for PKCE. No client secret is required because
            the frontend performs the authorization code + PKCE flow.
          example: "wallet-app"
        jwks_uri:
          type: string
          format: uri
          description: |
            Optional explicit JWKS URI for token signature validation.
            If omitted, the URI is discovered from the issuer's OpenID configuration.
          example: "https://login.acme.com/.well-known/jwks.json"
        audience:
          type: string
          description: |
            Required `aud` claim value. Defaults to `client_id` when not set.
          example: "wallet-app"
        scopes:
          type: string
          description: |
            Space-separated OIDC scopes to request. Defaults to
            `openid profile email` when not set.
          example: "openid profile email"

    OIDCGateConfig:
      type: object
      description: |
        OIDC pre-authentication gate for the tenant. Protects registration and/or
        login endpoints with enterprise IdP authentication.
        
        - `none`: OIDC gate disabled (default open behavior)
        - `registration`: OIDC required only for new wallet registrations
        - `login`: OIDC required only for wallet logins
        - `both`: OIDC required for both registration and login
        
        When `bind_identity` is true, the enterprise IdP `sub` claim is permanently
        linked to the wallet user during registration. Subsequent logins verify
        that the same enterprise identity is used. `bind_identity` cannot be used
        with mode `login` (binding only occurs during registration).
      properties:
        mode:
          type: string
          enum: [none, registration, login, both]
          default: none
          description: Which endpoints require OIDC pre-authentication
          example: registration
        registration_op:
          $ref: '#/components/schemas/OIDCProviderConfig'
        login_op:
          $ref: '#/components/schemas/OIDCProviderConfig'
        required_claims:
          type: object
          additionalProperties: true
          description: |
            Optional map of claim names to expected values that must be present
            in the validated ID token (e.g. `{"email_verified": true}`).
            Array values are matched as subsets.
          example:
            email_verified: true
        bind_identity:
          type: boolean
          default: false
          description: |
            When true, the enterprise IdP `sub` is permanently bound to the wallet
            user during registration. Cannot be used with mode `login`.
          example: false
      # Enforce: bind_identity cannot be true when mode is "login"
      if:
        properties:
          mode:
            const: login
        required: [mode]
      then:
        properties:
          bind_identity:
            enum: [false]
            description: bind_identity must be false (or omitted) when mode is "login"

  responses:
    BadRequest:
      description: Invalid request parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "Invalid request body"
    
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "Tenant not found"
    
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "Failed to process request"

    Unauthorized:
      description: Missing or invalid admin bearer token
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "Invalid token"

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        Static bearer token for admin API authentication. Configure via the
        `WALLET_SERVER_ADMIN_TOKEN` environment variable or `server.admin_token`
        config key. If not set, a random token is generated at startup and
        logged (development mode only).
