openapi: 3.0.3
info:
  title: O2ID API
  version: 0.1.0
  description: OpenAPI documentation for the O2ID identity service API.
servers:
  - url: http://localhost:8080
    description: Local development server
tags:
  - name: Health
    description: Service health endpoints.
  - name: Applications
    description: Application registration and management endpoints.
  - name: Agents
    description: AI agent identity registration and management endpoints.
  - name: ResourceServers
    description: Resource server (API) registration and application authorization endpoints.
  - name: Users
    description: User identity management endpoints.
  - name: Roles
    description: Role-based access control - roles, scopes, and user role assignments.
  - name: Authentication
    description: Login portal and session creation endpoints.
  - name: OAuth2
    description: OAuth 2.0 authorization code flow endpoints.
  - name: Portal
    description: Embedded static login portal assets.
paths:
  /health:
    get:
      tags:
        - Health
      summary: Check service health
      operationId: getHealth
      responses:
        "200":
          description: The service is healthy.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
              examples:
                ok:
                  value:
                    status: ok

  /applications:
    get:
      tags:
        - Applications
      summary: List applications
      description: Requires the `applications:read` scope.
      operationId: listApplications
      security:
        - BearerToken: []
      responses:
        "200":
          description: Applications currently registered with O2ID.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Application"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"
    post:
      tags:
        - Applications
      summary: Create an application
      description: Requires the `applications:create` scope.
      operationId: createApplication
      security:
        - BearerToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateApplicationRequest"
            examples:
              publicClient:
                value:
                  name: Admin Console
                  callbackUrls:
                    - https://app.example.com/callback
              confidentialClient:
                value:
                  name: Admin Console
                  callbackUrls:
                    - https://app.example.com/callback
                  clientSecret: super-secret
      responses:
        "201":
          description: Application created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Application"
        "400":
          description: Invalid JSON, application name, or callback URL.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                invalidJson:
                  value:
                    error: invalid JSON request body
                invalidName:
                  value:
                    error: invalid application name
                invalidCallbackUrl:
                  value:
                    error: invalid callback URL
        "409":
          description: The generated client ID already exists.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                duplicateClientId:
                  value:
                    error: client ID already exists
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /applications/{id}:
    parameters:
      - $ref: "#/components/parameters/ApplicationID"
    get:
      tags:
        - Applications
      summary: Get an application
      description: Requires the `applications:read` scope.
      operationId: getApplication
      security:
        - BearerToken: []
      responses:
        "200":
          description: Application found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Application"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Application not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                notFound:
                  value:
                    error: application not found
        "500":
          $ref: "#/components/responses/InternalServerError"
    patch:
      tags:
        - Applications
      summary: Update an application
      description: Partial update — only supplied fields are changed. Requires the `applications:update` scope.
      operationId: updateApplication
      security:
        - BearerToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateApplicationRequest"
            examples:
              update:
                value:
                  name: User Portal
                  callbackUrls:
                    - https://portal.example.com/callback
      responses:
        "200":
          description: Application updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Application"
        "400":
          description: Invalid JSON, application name, or callback URL.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Application not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                notFound:
                  value:
                    error: application not found
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          description: The updated client ID conflicts with another application.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalServerError"
    delete:
      tags:
        - Applications
      summary: Delete an application
      description: Requires the `applications:delete` scope.
      operationId: deleteApplication
      security:
        - BearerToken: []
      responses:
        "204":
          description: Application deleted.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Application not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                notFound:
                  value:
                    error: application not found
        "500":
          $ref: "#/components/responses/InternalServerError"

  /applications/{id}/resourceservers:
    parameters:
      - $ref: "#/components/parameters/ApplicationID"
    get:
      tags:
        - Applications
      summary: List an application's resource server authorizations
      description: Requires the `applications:read` scope.
      operationId: listApplicationResourceServers
      security:
        - BearerToken: []
      responses:
        "200":
          description: Resource server authorizations granted to this application.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ResourceServerAuthorization"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"
    post:
      tags:
        - Applications
      summary: Authorize a resource server for an application
      description: >-
        Requires the `applications:authorize-resource-servers` scope.
        Grants a subset of the named resource server's own defined scopes
        to this application. Re-authorizing the same resource server
        replaces the previously granted scopes rather than merging with
        them.
      operationId: authorizeApplicationResourceServer
      security:
        - BearerToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AuthorizeResourceServerRequest"
            examples:
              authorize:
                value:
                  resourceServerId: 3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f
                  scopes:
                    - read:messages
      responses:
        "204":
          description: Resource server authorized.
        "400":
          description: Missing resourceServerId, or a requested scope the resource server doesn't itself define.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                scopeNotDefined:
                  value:
                    error: scope not defined on resource server
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Application or resource server not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /applications/{id}/resourceservers/{resourceServerId}:
    parameters:
      - $ref: "#/components/parameters/ApplicationID"
      - name: resourceServerId
        in: path
        required: true
        schema:
          type: string
        description: Internal resource server ID.
    delete:
      tags:
        - Applications
      summary: Deauthorize a resource server for an application
      description: >-
        Requires the `applications:authorize-resource-servers` scope.
        Deauthorizing an application that was never authorized for this
        resource server is a no-op, not an error.
      operationId: deauthorizeApplicationResourceServer
      security:
        - BearerToken: []
      responses:
        "204":
          description: Resource server deauthorized.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /agents:
    get:
      tags:
        - Agents
      summary: List AI agents
      description: Requires the `agents:read` scope.
      operationId: listAgents
      security:
        - BearerToken: []
      responses:
        "200":
          description: AI agents currently registered with O2ID.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Agent"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"
    post:
      tags:
        - Agents
      summary: Create an AI agent
      description: Requires the `agents:create` scope.
      operationId: createAgent
      security:
        - BearerToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateAgentRequest"
            examples:
              scopedAgent:
                value:
                  name: Research Agent
                  allowedScopes:
                    - users:read
      responses:
        "201":
          description: AI agent created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Agent"
        "400":
          description: Invalid JSON, AI agent name, or scope.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                invalidJson:
                  value:
                    error: invalid JSON request body
                invalidName:
                  value:
                    error: invalid agent name
                invalidScope:
                  value:
                    error: invalid scope
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /agents/{id}:
    parameters:
      - $ref: "#/components/parameters/AgentID"
    get:
      tags:
        - Agents
      summary: Get an AI agent
      description: Requires the `agents:read` scope.
      operationId: getAgent
      security:
        - BearerToken: []
      responses:
        "200":
          description: AI agent found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Agent"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: AI agent not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                notFound:
                  value:
                    error: agent not found
        "500":
          $ref: "#/components/responses/InternalServerError"
    patch:
      tags:
        - Agents
      summary: Update an AI agent
      description: Partial update — only supplied fields are changed. Requires the `agents:update` scope.
      operationId: updateAgent
      security:
        - BearerToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateAgentRequest"
            examples:
              update:
                value:
                  name: Research Agent v2
                  allowedScopes:
                    - users:read
                    - applications:read
      responses:
        "200":
          description: AI agent updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Agent"
        "400":
          description: Invalid JSON, AI agent name, or scope.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: AI agent not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                notFound:
                  value:
                    error: agent not found
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"
    delete:
      tags:
        - Agents
      summary: Delete an AI agent
      description: Requires the `agents:delete` scope.
      operationId: deleteAgent
      security:
        - BearerToken: []
      responses:
        "204":
          description: AI agent deleted.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: AI agent not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                notFound:
                  value:
                    error: agent not found
        "500":
          $ref: "#/components/responses/InternalServerError"

  /agents/{id}/mandates:
    parameters:
      - $ref: "#/components/parameters/AgentID"
    get:
      tags:
        - Agents
      summary: List an AI agent's mandates
      description: Requires the `mandates:read` scope.
      operationId: listMandates
      security:
        - BearerToken: []
      responses:
        "200":
          description: Mandates created for this AI agent.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Mandate"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"
    post:
      tags:
        - Agents
      summary: Create a mandate
      description: >-
        Requires the `mandates:manage` scope. Always authorizes the AI agent
        to act as the caller — there is no request field for naming a
        different subject; SubjectUserID is derived from the caller's own
        bearer token.
      operationId: createMandate
      security:
        - BearerToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateMandateRequest"
            examples:
              mandate:
                value:
                  scope:
                    - users:read
                  expiresAt: "2026-08-01T00:00:00Z"
      responses:
        "201":
          description: Mandate created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Mandate"
        "400":
          description: Invalid JSON, scope, or expiry.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /agents/{id}/mandates/{mandateId}:
    parameters:
      - $ref: "#/components/parameters/AgentID"
      - name: mandateId
        in: path
        required: true
        schema:
          type: string
        description: Internal mandate ID.
    delete:
      tags:
        - Agents
      summary: Revoke a mandate
      description: >-
        Requires the `mandates:manage` scope. Revoking an already-revoked
        mandate is a no-op, not an error. 404s if mandateId doesn't belong
        to the AI agent named in the path.
      operationId: revokeMandate
      security:
        - BearerToken: []
      responses:
        "204":
          description: Mandate revoked.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Mandate not found (or belongs to a different AI agent).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /resourceservers:
    get:
      tags:
        - ResourceServers
      summary: List resource servers
      description: Requires the `resourceservers:read` scope.
      operationId: listResourceServers
      security:
        - BearerToken: []
      responses:
        "200":
          description: Resource servers currently registered with O2ID.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ResourceServer"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"
    post:
      tags:
        - ResourceServers
      summary: Create a resource server
      description: Requires the `resourceservers:create` scope.
      operationId: createResourceServer
      security:
        - BearerToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateResourceServerRequest"
            examples:
              messagesApi:
                value:
                  name: Messages API
                  identifier: https://api.example.com
      responses:
        "201":
          description: Resource server created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResourceServer"
        "400":
          description: Invalid JSON, name, or identifier.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                invalidJson:
                  value:
                    error: invalid JSON request body
                invalidResourceServer:
                  value:
                    error: invalid resource server
        "409":
          description: The identifier already exists.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                duplicateIdentifier:
                  value:
                    error: identifier already exists
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /resourceservers/{id}:
    parameters:
      - $ref: "#/components/parameters/ResourceServerID"
    get:
      tags:
        - ResourceServers
      summary: Get a resource server
      description: Requires the `resourceservers:read` scope.
      operationId: getResourceServer
      security:
        - BearerToken: []
      responses:
        "200":
          description: Resource server found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResourceServer"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Resource server not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                notFound:
                  value:
                    error: resource server not found
        "500":
          $ref: "#/components/responses/InternalServerError"
    patch:
      tags:
        - ResourceServers
      summary: Update a resource server
      description: Partial update — only supplied fields are changed. Requires the `resourceservers:update` scope.
      operationId: updateResourceServer
      security:
        - BearerToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateResourceServerRequest"
            examples:
              update:
                value:
                  name: Messages API v2
      responses:
        "200":
          description: Resource server updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResourceServer"
        "400":
          description: Invalid JSON, name, or identifier.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Resource server not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                notFound:
                  value:
                    error: resource server not found
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          description: The updated identifier conflicts with another resource server.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalServerError"
    delete:
      tags:
        - ResourceServers
      summary: Delete a resource server
      description: Requires the `resourceservers:delete` scope.
      operationId: deleteResourceServer
      security:
        - BearerToken: []
      responses:
        "204":
          description: Resource server deleted.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Resource server not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                notFound:
                  value:
                    error: resource server not found
        "500":
          $ref: "#/components/responses/InternalServerError"

  /resourceservers/{id}/scopes:
    parameters:
      - $ref: "#/components/parameters/ResourceServerID"
    get:
      tags:
        - ResourceServers
      summary: List a resource server's scopes
      description: Requires the `resourceservers:read` scope.
      operationId: listResourceServerScopes
      security:
        - BearerToken: []
      responses:
        "200":
          description: Scopes this resource server defines.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Scope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"
    post:
      tags:
        - ResourceServers
      summary: Define a scope on a resource server
      description: Requires the `resourceservers:create` scope. value must be unique among this resource server's other scopes.
      operationId: createResourceServerScope
      security:
        - BearerToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateScopeRequest"
            examples:
              readMessages:
                value:
                  value: read:messages
                  displayName: Read Messages
                  description: Allows reading messages
      responses:
        "201":
          description: Scope created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Scope"
        "400":
          description: Invalid JSON or missing value.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                invalidScope:
                  value:
                    error: invalid scope
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Resource server not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: A scope with this value already exists on this resource server.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                duplicateValue:
                  value:
                    error: scope value already exists on this resource server
        "500":
          $ref: "#/components/responses/InternalServerError"

  /resourceserverscopes/{scopeId}:
    parameters:
      - $ref: "#/components/parameters/ScopeID"
    get:
      tags:
        - ResourceServers
      summary: Get a scope
      description: >-
        Requires the `resourceservers:read` scope. Addressed by the
        scope's own ID alone — no resource server ID needed, since that
        ID is already globally unique.
      operationId: getResourceServerScope
      security:
        - BearerToken: []
      responses:
        "200":
          description: Scope found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Scope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Scope not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                notFound:
                  value:
                    error: scope not found
        "500":
          $ref: "#/components/responses/InternalServerError"
    patch:
      tags:
        - ResourceServers
      summary: Update a scope
      description: >-
        Partial update — only supplied fields are changed. Requires the
        `resourceservers:update` scope. Renaming value doesn't
        retroactively update any Application authorization already
        granted under the old value.
      operationId: updateResourceServerScope
      security:
        - BearerToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateScopeRequest"
            examples:
              rename:
                value:
                  displayName: Read Message Data
      responses:
        "200":
          description: Scope updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Scope"
        "400":
          description: Invalid JSON or empty value.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Scope not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: The updated value conflicts with another scope on the same resource server.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalServerError"
    delete:
      tags:
        - ResourceServers
      summary: Delete a scope
      description: Requires the `resourceservers:delete` scope.
      operationId: deleteResourceServerScope
      security:
        - BearerToken: []
      responses:
        "204":
          description: Scope deleted.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Scope not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /users:
    get:
      tags:
        - Users
      summary: List users
      description: Requires the `users:read` scope.
      operationId: listUsers
      security:
        - BearerToken: []
      responses:
        "200":
          description: Users currently registered with O2ID.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/User"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"
    post:
      tags:
        - Users
      summary: Create a user
      description: Requires the `users:create` scope.
      operationId: createUser
      security:
        - BearerToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateUserRequest"
            examples:
              passwordUser:
                value:
                  email: alice@example.com
                  displayName: Alice
                  password: secret
              passwordlessUser:
                value:
                  email: bob@example.com
                  displayName: Bob
      responses:
        "201":
          description: User created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "400":
          description: Invalid JSON or email address.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                invalidJson:
                  value:
                    error: invalid JSON request body
                invalidEmail:
                  value:
                    error: invalid email
        "409":
          description: A user already exists for the email address.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                duplicateEmail:
                  value:
                    error: email already exists
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /users/{id}:
    parameters:
      - $ref: "#/components/parameters/UserID"
    get:
      tags:
        - Users
      summary: Get a user
      description: Requires the `users:read` scope.
      operationId: getUser
      security:
        - BearerToken: []
      responses:
        "200":
          description: User found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: User not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                notFound:
                  value:
                    error: user not found
        "500":
          $ref: "#/components/responses/InternalServerError"
    patch:
      tags:
        - Users
      summary: Update a user
      description: Partial update — only supplied fields are changed. Requires the `users:update` scope.
      operationId: updateUser
      security:
        - BearerToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateUserRequest"
            examples:
              update:
                value:
                  displayName: Alice Updated
      responses:
        "200":
          description: User updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "400":
          description: Invalid JSON request body.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: User not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                notFound:
                  value:
                    error: user not found
        "500":
          $ref: "#/components/responses/InternalServerError"

  /users/{id}/roles:
    parameters:
      - $ref: "#/components/parameters/UserID"
    get:
      tags:
        - Roles
      summary: List a user's assigned roles
      description: Requires the `roles:read` scope.
      operationId: listUserRoles
      security:
        - BearerToken: []
      responses:
        "200":
          description: Roles assigned to the user.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Role"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"
    post:
      tags:
        - Roles
      summary: Assign a role to a user
      description: Requires the `roles:assign` scope.
      operationId: assignUserRole
      security:
        - BearerToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AssignRoleRequest"
      responses:
        "204":
          description: Role assigned. Assigning a role the user already holds is a no-op.
        "400":
          description: Invalid JSON request body, or missing roleId.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Role not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /users/{id}/roles/{roleId}:
    parameters:
      - $ref: "#/components/parameters/UserID"
      - name: roleId
        in: path
        required: true
        schema:
          type: string
        description: Internal role ID.
    delete:
      tags:
        - Roles
      summary: Unassign a role from a user
      description: Requires the `roles:assign` scope.
      operationId: unassignUserRole
      security:
        - BearerToken: []
      responses:
        "204":
          description: Role unassigned. Unassigning a role the user doesn't hold is a no-op.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /roles:
    get:
      tags:
        - Roles
      summary: List roles
      description: Requires the `roles:read` scope.
      operationId: listRoles
      security:
        - BearerToken: []
      responses:
        "200":
          description: Roles currently defined in O2ID.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Role"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"
    post:
      tags:
        - Roles
      summary: Create a role
      description: Requires the `roles:create` scope.
      operationId: createRole
      security:
        - BearerToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateRoleRequest"
            examples:
              readOnly:
                value:
                  name: Support
                  scopes:
                    - applications:read
      responses:
        "201":
          description: Role created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Role"
        "400":
          description: Invalid JSON, role name, or scope.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          description: A role already exists for the name.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /roles/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Internal role ID.
    get:
      tags:
        - Roles
      summary: Get a role
      description: Requires the `roles:read` scope.
      operationId: getRole
      security:
        - BearerToken: []
      responses:
        "200":
          description: Role found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Role"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Role not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalServerError"
    patch:
      tags:
        - Roles
      summary: Update a role
      description: Partial update — only supplied fields are changed. Requires the `roles:update` scope.
      operationId: updateRole
      security:
        - BearerToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateRoleRequest"
      responses:
        "200":
          description: Role updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Role"
        "400":
          description: Invalid JSON, role name, or scope.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Role not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: The updated name conflicts with another role.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalServerError"
    delete:
      tags:
        - Roles
      summary: Delete a role
      description: Requires the `roles:delete` scope. Fails if the role is still assigned to any user.
      operationId: deleteRole
      security:
        - BearerToken: []
      responses:
        "204":
          description: Role deleted.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Role not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: The role is still assigned to at least one user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                roleInUse:
                  value:
                    error: role is assigned to at least one user
        "500":
          $ref: "#/components/responses/InternalServerError"

  /login:
    get:
      tags:
        - Authentication
      summary: Render the embedded login page
      operationId: getLoginPage
      parameters:
        - name: transaction_id
          in: query
          required: false
          schema:
            type: string
          description: Login transaction ID created during an OAuth authorize redirect.
      responses:
        "200":
          description: Embedded login page HTML when the portal is served by O2ID.
          content:
            text/html:
              schema:
                type: string
        "302":
          description: Redirect to the external login portal when O2ID is configured with server.external_login_portal.
          headers:
            Location:
              $ref: "#/components/headers/Location"
    post:
      tags:
        - Authentication
      summary: Submit login credentials
      operationId: submitLogin
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: "#/components/schemas/LoginRequest"
      responses:
        "302":
          description: Redirects to the original OAuth authorize URL on success, or back to login with an error on invalid credentials.
          headers:
            Location:
              $ref: "#/components/headers/Location"
            Set-Cookie:
              description: Session cookie set on successful login.
              schema:
                type: string
              example: o2id_session=token; Path=/; Expires=Wed, 27 May 2026 12:00:00 GMT; HttpOnly; SameSite=Lax
        "500":
          $ref: "#/components/responses/InternalServerError"

  /oauth2/authorize:
    get:
      tags:
        - OAuth2
      summary: Start or complete OAuth2 authorization
      operationId: authorizeOAuth2
      security:
        - O2IDSession: []
        - {}
      parameters:
        - name: response_type
          in: query
          required: true
          schema:
            type: string
            enum:
              - code
          description: Must be `code`.
        - name: client_id
          in: query
          required: true
          schema:
            type: string
          description: OAuth client ID for the registered application.
        - name: redirect_uri
          in: query
          required: true
          schema:
            type: string
            format: uri
          description: >-
            Callback URL registered on the application. For a loopback host
            (127.0.0.1, ::1, or localhost) registered without a port, any
            port is accepted (RFC 8252 §7.3) — used by native/CLI clients
            like the O2ID CLI, which bind an ephemeral local port per login.
        - name: scope
          in: query
          required: false
          schema:
            type: string
          description: Space-delimited OAuth scopes.
          example: openid profile
        - name: state
          in: query
          required: false
          schema:
            type: string
          description: Opaque state returned unchanged to the redirect URI.
        - name: code_challenge
          in: query
          required: false
          schema:
            type: string
          description: PKCE code challenge.
        - name: code_challenge_method
          in: query
          required: false
          schema:
            type: string
            enum:
              - plain
              - S256
          description: PKCE code challenge method. Empty defaults to plain when a challenge is supplied.
      responses:
        "302":
          description: Redirects unauthenticated users to login, or authenticated users to the application redirect URI with an authorization code.
          headers:
            Location:
              $ref: "#/components/headers/Location"
        "400":
          description: Invalid authorize request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthErrorResponse"
              examples:
                invalidRequest:
                  value:
                    error: invalid_request
        "500":
          $ref: "#/components/responses/InternalServerError"

  /oauth2/userinfo:
    get:
      tags:
        - OAuth2
      summary: Get authenticated user claims
      operationId: getOAuth2UserInfo
      security:
        - BearerToken: []
      responses:
        "200":
          description: Claims for the user identified by the access token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserInfoResponse"
              examples:
                userInfo:
                  value:
                    sub: 9f8e7d6c5b4a39281726354433221100
                    email: alice@example.com
                    name: Alice
        "401":
          description: Missing or invalid access token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthErrorResponse"
              examples:
                invalidToken:
                  value:
                    error: invalid_token
          headers:
            WWW-Authenticate:
              description: Bearer challenge.
              schema:
                type: string
              example: 'Bearer realm="o2id", error="invalid_token"'
        "500":
          $ref: "#/components/responses/InternalServerError"

  /oauth2/token:
    post:
      tags:
        - OAuth2
      summary: Exchange tokens using a supported grant
      operationId: exchangeOAuth2Token
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: "#/components/schemas/TokenRequest"
            examples:
              authorizationCode:
                summary: Authorization code exchange
                value:
                  grant_type: authorization_code
                  code: abc123
                  client_id: client_abc123
                  redirect_uri: https://app.example.com/callback
                  code_verifier: verifier-1
              refreshToken:
                summary: Refresh token exchange
                value:
                  grant_type: refresh_token
                  refresh_token: def456
                  client_id: client_abc123
              clientCredentials:
                summary: Client credentials
                value:
                  grant_type: client_credentials
                  client_id: client_abc123
                  client_secret: super-secret
                  scope: read write
              tokenExchange:
                summary: Token exchange — downscope a live token
                value:
                  grant_type: "urn:ietf:params:oauth:grant-type:token-exchange"
                  client_id: client_abc123
                  subject_token: 8f421fe...
                  subject_token_type: "urn:ietf:params:oauth:token-type:access_token"
                  scope: users:read
              tokenExchangeDelegation:
                summary: Token exchange — actor-token delegation (AI agent redeeming a mandate)
                value:
                  grant_type: "urn:ietf:params:oauth:grant-type:token-exchange"
                  client_id: client_abc123
                  subject_token: 8f421fe...
                  subject_token_type: "urn:ietf:params:oauth:token-type:access_token"
                  actor_token: 146b7be...
                  actor_token_type: "urn:ietf:params:oauth:token-type:access_token"
              agentClientCredentials:
                summary: Client credentials — AI agent authenticating with private_key_jwt
                value:
                  grant_type: client_credentials
                  client_id: agent_abc123
                  client_assertion: eyJhbGciOiJSUzI1NiJ9...
                  client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
              resourceServerClientCredentials:
                summary: Client credentials — token scoped to a specific resource server
                value:
                  grant_type: client_credentials
                  client_id: client_abc123
                  client_secret: super-secret
                  audience: https://api.example.com
                  scope: read:messages
      responses:
        "200":
          description: Token response.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TokenResponse"
              examples:
                token:
                  value:
                    access_token: 8f421fe...
                    token_type: Bearer
                    expires_in: 3600
                    refresh_token: 146b7be...
                    scope: openid profile
        "400":
          description: Invalid grant, request, or unsupported grant type.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthErrorResponse"
              examples:
                invalidGrant:
                  value:
                    error: invalid_grant
                unsupportedGrantType:
                  value:
                    error: unsupported_grant_type
        "401":
          description: Invalid OAuth client credentials.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthErrorResponse"
              examples:
                invalidClient:
                  value:
                    error: invalid_client
        "500":
          $ref: "#/components/responses/InternalServerError"

  /oauth2/introspect:
    post:
      tags:
        - OAuth2
      summary: Introspect an access token
      description: >-
        RFC 7662 token introspection. O2ID's access tokens are opaque
        random strings, not JWTs — this is the only way to see what one
        carries server-side. There is no admin/operator path: the only
        caller who can ever introspect a token is an Application's own
        `client_credentials` token, and only for a token whose `aud`
        names a resource server that Application is authorized for (see
        the ResourceServer schema) — no separate scope grant needed,
        since that authorization is itself the permission. A token with
        no `aud` at all can never be introspected by anyone. Refresh
        tokens aren't supported; only access tokens can be introspected.
      operationId: introspectToken
      security:
        - BearerToken: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: "#/components/schemas/IntrospectionRequest"
            examples:
              introspect:
                value:
                  token: 8f421fe...
      responses:
        "200":
          description: >-
            Introspection result. Always `200`, even for an unknown,
            expired, or malformed token — RFC 7662 §2.2 requires
            `{"active": false}` in that case rather than an error, so a
            caller can't distinguish *why* a token is inactive.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntrospectionResponse"
              examples:
                active:
                  summary: Active, non-delegated token
                  value:
                    active: true
                    scope: read:messages
                    client_id: client_abc123
                    sub: client_abc123
                    token_type: Bearer
                    exp: 1735689600
                    aud: https://api.example.com
                activeDelegated:
                  summary: Active, delegated (actor-token) token
                  value:
                    active: true
                    scope: users:read
                    client_id: client_abc123
                    sub: 9f8e7d6c5b4a39281726354433221100
                    token_type: Bearer
                    exp: 1735689600
                    aud: https://api.example.com
                    act:
                      sub: agent_e241a1c4447bdc04d755a707dd0dec76
                inactive:
                  summary: Unknown, expired, or malformed token
                  value:
                    active: false
        "400":
          description: Missing `token` parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthErrorResponse"
              examples:
                invalidRequest:
                  value:
                    error: invalid_request
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /portal/static/{path}:
    get:
      tags:
        - Portal
      summary: Serve embedded login portal static assets
      operationId: getPortalStaticAsset
      parameters:
        - name: path
          in: path
          required: true
          schema:
            type: string
          description: Static asset path under `internal/portal/static`.
          example: login.css
      responses:
        "200":
          description: Static portal asset.
          content:
            text/css:
              schema:
                type: string
            application/javascript:
              schema:
                type: string
            text/html:
              schema:
                type: string
        "404":
          description: Static asset not found.

components:
  securitySchemes:
    O2IDSession:
      type: apiKey
      in: cookie
      name: o2id_session
    BearerToken:
      type: http
      scheme: bearer
      description: Access token issued by POST /oauth2/token.

  headers:
    Location:
      description: Redirect target.
      schema:
        type: string

  parameters:
    ApplicationID:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: Internal application ID.
    UserID:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: Internal user ID.
    AgentID:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: Internal AI agent ID.
    ResourceServerID:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: Internal resource server ID.
    ScopeID:
      name: scopeId
      in: path
      required: true
      schema:
        type: string
      description: Internal scope ID — globally unique, not scoped by its parent resource server's ID.

  responses:
    InternalServerError:
      description: Internal server error.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            internalServerError:
              value:
                error: internal server error

    Unauthorized:
      description: Missing or invalid access token.
      headers:
        WWW-Authenticate:
          description: Bearer challenge.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            invalidToken:
              value:
                error: invalid_token

    Forbidden:
      description: The caller's roles don't include the scope this endpoint requires.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            insufficientScope:
              value:
                error: insufficient scope

  schemas:
    HealthResponse:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - ok

    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          type: string

    OAuthErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          type: string
          example: invalid_request

    Application:
      type: object
      required:
        - id
        - clientId
        - confidential
        - name
        - callbackUrls
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          description: Internal application ID for CRUD operations.
          example: 5f2d3e4c9b8a7d6c5e4f3a2b1c0d9e8f
        clientId:
          type: string
          description: OAuth client ID used in authorize and token requests.
          example: client_5f2d3e4c9b8a7d6c5e4f3a2b1c0d9e8f
        confidential:
          type: boolean
          description: Whether the application has a client secret.
        name:
          type: string
          example: Admin Console
        callbackUrls:
          type: array
          items:
            type: string
            format: uri
          example:
            - https://app.example.com/callback
        allowedScopes:
          type: array
          items:
            type: string
          description: >-
            RBAC scopes (see the Role schema's `scopes` field) this
            application's issued tokens may ever carry, regardless of the
            authenticated user's own role scopes. Empty by default — an
            application must be explicitly granted scopes before a user
            logging in through it can exercise their role's management-API
            permissions through it. `*` grants every scope, present and
            future.
          example:
            - users:read
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    CreateApplicationRequest:
      type: object
      required:
        - name
        - callbackUrls
      properties:
        name:
          type: string
          minLength: 1
        callbackUrls:
          type: array
          items:
            type: string
            format: uri
        allowedScopes:
          type: array
          items:
            type: string
          description: Omit for no allowed scopes (deny-by-default).
        clientSecret:
          type: string
          description: Optional client secret for confidential applications.

    UpdateApplicationRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          description: Leave unset to keep the current name.
        callbackUrls:
          type: array
          items:
            type: string
            format: uri
          description: >-
            Leave unset to keep the current callback URLs. When supplied,
            replaces the full list — a partial list isn't merged with the
            existing one.
        allowedScopes:
          type: array
          items:
            type: string
          description: >-
            Leave unset to keep the current allowed scopes. When supplied,
            replaces the full list — a partial list isn't merged with the
            existing one.
        clientSecret:
          type: string
          nullable: true
          description: Leave unset to keep the current client secret. Empty string makes the application public.

    Agent:
      type: object
      required:
        - id
        - agentId
        - name
        - allowedScopes
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          description: Internal agent ID for CRUD operations.
          example: 7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d
        agentId:
          type: string
          description: Public agent identifier, immutable after creation.
          example: agent_7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d
        name:
          type: string
          example: Research Agent
        allowedScopes:
          type: array
          items:
            type: string
          description: >-
            RBAC scopes (see the Role schema's `scopes` field) this AI agent's
            issued tokens may ever carry, regardless of the acting user's
            own role scopes. Empty by default — an AI agent must be explicitly
            granted scopes before it can act with any management-API
            permission. `*` grants every scope, present and future.
          example:
            - users:read
        publicKeyJwk:
          allOf:
            - $ref: "#/components/schemas/JWK"
          nullable: true
          description: >-
            RSA public key this AI agent can use to authenticate itself via
            a private_key_jwt client assertion, either on the
            client_credentials grant directly, or as the actor_token in a
            token-exchange delegation redeeming one of this AI agent's
            mandates (POST /oauth2/token). Unset means the AI agent can
            never authenticate at all, including as a delegation actor.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    CreateAgentRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          minLength: 1
        allowedScopes:
          type: array
          items:
            type: string
          description: Omit for no allowed scopes (deny-by-default).

    UpdateAgentRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          description: Leave unset to keep the current name.
        allowedScopes:
          type: array
          items:
            type: string
          description: >-
            Leave unset to keep the current allowed scopes. When supplied,
            replaces the full list — a partial list isn't merged with the
            existing one.
        publicKeyJwk:
          allOf:
            - $ref: "#/components/schemas/JWK"
          description: >-
            Leave unset to keep the current key (or lack of one). When
            supplied, registers or replaces the AI agent's private_key_jwt
            public key. There is currently no way to clear a registered key
            back to "none" via update.

    JWK:
      type: object
      required:
        - kty
        - n
        - e
      properties:
        kty:
          type: string
          enum:
            - RSA
          description: Key type. Only RSA is currently supported.
        use:
          type: string
          example: sig
        kid:
          type: string
        alg:
          type: string
          example: RS256
        n:
          type: string
          description: RSA modulus, base64url-encoded.
        e:
          type: string
          description: RSA public exponent, base64url-encoded.
          example: AQAB

    Mandate:
      type: object
      required:
        - id
        - agentId
        - subjectUserId
        - scope
        - expiresAt
        - revoked
        - createdAt
      properties:
        id:
          type: string
          description: Internal mandate ID.
        agentId:
          type: string
          description: The agent's internal Agent.ID (not its public agentId).
        subjectUserId:
          type: string
          description: The human this mandate lets the AI agent act for — always the user who created it.
        scope:
          type: array
          items:
            type: string
          description: >-
            The ceiling this mandate grants — intersected with the AI
            agent's AllowedScopes and the subject user's live role scopes,
            at every redemption (see POST /oauth2/token's token-exchange
            actor_token delegation). Can only narrow what the creating
            human could grant at creation time, never widen it.
        expiresAt:
          type: string
          format: date-time
        revoked:
          type: boolean
        createdAt:
          type: string
          format: date-time

    CreateMandateRequest:
      type: object
      required:
        - expiresAt
      properties:
        scope:
          type: array
          items:
            type: string
        expiresAt:
          type: string
          format: date-time
          description: Must be in the future, and within a maximum lifetime (90 days).

    ResourceServer:
      type: object
      required:
        - id
        - identifier
        - name
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          description: Internal resource server ID for CRUD operations.
          example: 3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f
        identifier:
          type: string
          description: The value a caller passes as `resource`/`audience` to target this resource server.
          example: https://api.example.com
        name:
          type: string
          example: Messages API
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    CreateResourceServerRequest:
      type: object
      required:
        - name
        - identifier
      properties:
        name:
          type: string
          minLength: 1
        identifier:
          type: string
          minLength: 1

    UpdateResourceServerRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          description: Leave unset to keep the current name.
        identifier:
          type: string
          minLength: 1
          description: Leave unset to keep the current identifier.

    Scope:
      type: object
      required:
        - id
        - resourceServerId
        - value
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          description: Internal scope ID, globally unique — enough on its own to get, update, or delete this scope.
          example: 7d6c5b4a39281726354433221100ffee
        resourceServerId:
          type: string
          description: The resource server this scope belongs to.
        value:
          type: string
          description: >-
            The scope itself, granted and checked exactly as written (e.g.
            in an Application's resource-server authorization, and in a
            client_credentials request's `scope` parameter when the
            request names this resource server as `audience`). Unique
            among its resource server's other scopes.
          example: read:messages
        displayName:
          type: string
          description: Human-readable name, purely descriptive.
          example: Read Messages
        description:
          type: string
          description: Human-readable description, purely descriptive.
          example: Allows reading messages
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    CreateScopeRequest:
      type: object
      required:
        - value
      properties:
        value:
          type: string
          minLength: 1
        displayName:
          type: string
        description:
          type: string

    UpdateScopeRequest:
      type: object
      properties:
        value:
          type: string
          minLength: 1
          description: Leave unset to keep the current value. Must still be unique among this resource server's other scopes.
        displayName:
          type: string
          description: Leave unset to keep the current display name.
        description:
          type: string
          description: Leave unset to keep the current description.

    ResourceServerAuthorization:
      type: object
      required:
        - resourceServerId
        - scopes
      properties:
        resourceServerId:
          type: string
        scopes:
          type: array
          items:
            type: string
          description: >-
            The subset of the resource server's own scope values granted
            to this application.
          example:
            - read:messages

    AuthorizeResourceServerRequest:
      type: object
      required:
        - resourceServerId
      properties:
        resourceServerId:
          type: string
        scopes:
          type: array
          items:
            type: string
          description: >-
            Scope values to grant — each must be one the target resource
            server itself defines. Replaces any previously granted scopes
            for this application/resource-server pair. Omit for none.

    User:
      type: object
      required:
        - id
        - email
        - displayName
        - createdAt
      properties:
        id:
          type: string
          example: 9f8e7d6c5b4a39281726354433221100
        email:
          type: string
          format: email
          example: alice@example.com
        displayName:
          type: string
          example: Alice
        createdAt:
          type: string
          format: date-time

    CreateUserRequest:
      type: object
      required:
        - email
      properties:
        email:
          type: string
          format: email
        displayName:
          type: string
        password:
          type: string
          description: Optional password. Omit or send an empty value for passwordless users.

    UpdateUserRequest:
      type: object
      properties:
        displayName:
          type: string
          description: Leave unset to keep the current display name.
        password:
          type: string
          description: Leave unset to keep the current password.

    Role:
      type: object
      required:
        - id
        - name
        - scopes
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          example: 3c2b1a0d9e8f7a6b5c4d3e2f1a0b9c8d
        name:
          type: string
          example: Support
        scopes:
          type: array
          items:
            type: string
          description: >-
            Scope strings this role grants (e.g. `users:read`,
            `applications:update`), or `*` for every permission.
          example:
            - applications:read
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    CreateRoleRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
        scopes:
          type: array
          items:
            type: string

    UpdateRoleRequest:
      type: object
      properties:
        name:
          type: string
          description: Leave unset to keep the current name.
        scopes:
          type: array
          items:
            type: string
          description: Leave unset to keep the current scopes.

    AssignRoleRequest:
      type: object
      required:
        - roleId
      properties:
        roleId:
          type: string

    LoginRequest:
      type: object
      properties:
        transaction_id:
          type: string
          description: Login transaction ID passed to the login page.
        email:
          type: string
          format: email
        password:
          type: string

    TokenRequest:
      type: object
      required:
        - grant_type
        - client_id
      properties:
        grant_type:
          type: string
          enum:
            - authorization_code
            - refresh_token
            - client_credentials
            - urn:ietf:params:oauth:grant-type:token-exchange
          example: authorization_code
        code:
          type: string
          description: Required for authorization_code grant.
        client_id:
          type: string
        client_secret:
          type: string
          description: >-
            Required for confidential applications, except for an AI agent
            authenticating via client_credentials with client_assertion
            instead (see below).
        redirect_uri:
          type: string
          format: uri
          description: Required for authorization_code grant.
        code_verifier:
          type: string
          description: PKCE verifier when the authorization request used a code challenge.
        refresh_token:
          type: string
          description: Required for refresh_token grant.
        scope:
          type: string
          description: Space-delimited scopes. Used by client_credentials and token-exchange grants.
        subject_token:
          type: string
          description: >-
            Required for the token-exchange grant: the caller's own live
            access token, proving a real, currently-authenticated session.
            RFC 8693 requires this unconditionally — there is no way to
            exchange with no credential in hand.
        subject_token_type:
          type: string
          enum:
            - urn:ietf:params:oauth:token-type:access_token
          description: Required for the token-exchange grant, alongside subject_token.
        actor_token:
          type: string
          description: >-
            Token-exchange only. An access token identifying a second
            party now acting on the subject's behalf (RFC 8693 §4.1
            delegation) — e.g. an AI agent's own client_credentials token,
            redeeming one of its Mandates. Requires actor_token_type
            alongside it; never one without the other. The resulting
            token's subject stays subject_token's subject; the actor is
            recorded separately, nesting onto any actor subject_token
            itself already carries.
        actor_token_type:
          type: string
          enum:
            - urn:ietf:params:oauth:token-type:access_token
          description: Required exactly when actor_token is present.
        resource:
          type: string
          format: uri
          description: >-
            Token-exchange only. The target resource server, as an
            absolute URI. Rejected with invalid_target if malformed (RFC
            8707 §2), if it doesn't match a registered resource server, or
            if the calling application isn't authorized for it (see the
            ResourceServer schema). Recorded on the minted token; doesn't
            change how the token's own scope is computed, which stays
            purely RBAC-derived.
        audience:
          type: string
          description: >-
            The target resource server, as a free-form identifier.
            Accepted by both the token-exchange grant (same validation and
            caveat as resource above) and the client_credentials grant —
            there, it's the primary way to get a token scoped to a
            specific resource server: the granted scope comes entirely
            from that resource server's own scopes and the subset the
            calling application is authorized for, not from the
            application's allowedScopes.
        client_assertion:
          type: string
          description: >-
            A private_key_jwt JWT (RFC 7523) proving the caller's own
            identity with an asymmetric key instead of a client_secret.
            Used by an AI agent authenticating via the client_credentials
            grant (see below) — not used by the token-exchange grant at all.
        client_assertion_type:
          type: string
          enum:
            - urn:ietf:params:oauth:client-assertion-type:jwt-bearer
          description: Required, and only this value is accepted, when client_assertion is present.

    UserInfoResponse:
      type: object
      required:
        - sub
        - email
      properties:
        sub:
          type: string
          description: Subject identifier — the user's internal ID.
          example: 9f8e7d6c5b4a39281726354433221100
        email:
          type: string
          format: email
          example: alice@example.com
        name:
          type: string
          description: Display name. Omitted when not set.
          example: Alice

    TokenResponse:
      type: object
      required:
        - access_token
        - token_type
        - expires_in
      properties:
        access_token:
          type: string
        token_type:
          type: string
          enum:
            - Bearer
        expires_in:
          type: integer
          format: int32
          example: 3600
        refresh_token:
          type: string
        scope:
          type: string
          description: Space-delimited scopes granted to the token.
          example: openid profile
        issued_token_type:
          type: string
          enum:
            - urn:ietf:params:oauth:token-type:access_token
          description: >-
            Only present in a token-exchange response — RFC 8693 §2.2.1
            requires it there. Always access_token: O2ID only ever issues
            access tokens from this grant.

    IntrospectionRequest:
      type: object
      required:
        - token
      properties:
        token:
          type: string
          description: The access token to introspect.

    Act:
      type: object
      required:
        - sub
      properties:
        sub:
          type: string
          description: The immediate actor's identifier (an AI agent's `agentId`).
        act:
          $ref: "#/components/schemas/Act"
          description: >-
            The next actor further back in the delegation chain, present
            only when this token itself was re-delegated.

    IntrospectionResponse:
      type: object
      required:
        - active
      properties:
        active:
          type: boolean
          description: >-
            false for any unknown, expired, or malformed token — with every
            other field omitted.
        scope:
          type: string
          description: Space-delimited scopes the token carries.
        client_id:
          type: string
        sub:
          type: string
          description: >-
            The token's subject — for a delegated token, this is always the
            original human, never the acting AI agent (see `act`).
        token_type:
          type: string
          enum:
            - Bearer
        exp:
          type: integer
          format: int64
          description: Expiry, as seconds since the Unix epoch.
        aud:
          type: string
          description: >-
            The resource/audience recorded when this token was minted, if
            any (see `resource`/`audience` on TokenRequest) — the
            registered resource server's identifier this token was issued
            for.
        act:
          $ref: "#/components/schemas/Act"
          description: >-
            RFC 8693 §4.1's actor claim — present only for a token minted by
            token exchange's actor-token delegation. `sub` stays the human
            throughout; this names whichever AI agent is currently acting
            for them.
