openapi: "3.0.3"

info:
  title: SentryAgent.ai — Identity Federation
  version: 1.0.0
  description: |
    Cross-IdP identity federation endpoints for the SentryAgent.ai AgentIdP platform.

    The federation subsystem enables this IdP to trust and verify JWT tokens issued by
    external identity providers (partner IdPs). This allows agents from federated
    organizations to authenticate without re-registering.

    **Partner management** (`admin:orgs` scope required):
    - `POST /federation/trust` — Register a new trusted partner IdP
    - `GET /federation/partners` — List all registered partners
    - `GET /federation/partners/:id` — Get a specific partner
    - `PATCH /federation/partners/:id` — Update a partner
    - `DELETE /federation/partners/:id` — Remove a partner

    **Token verification** (any authenticated agent):
    - `POST /federation/verify` — Verify a federated JWT from a trusted partner

servers:
  - url: http://localhost:3000/api/v1
    description: Local development server
  - url: https://api.sentryagent.ai/v1
    description: Production server

tags:
  - name: Federation Partners
    description: Trusted partner IdP management (admin:orgs scope)
  - name: Federation Verification
    description: Cross-IdP token verification (any authenticated agent)

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        JWT access token obtained via `POST /token`.
        Include as `Authorization: Bearer <token>`.

  schemas:
    FederationPartnerStatus:
      type: string
      enum:
        - active
        - suspended
        - expired
      description: |
        Lifecycle status of a federation partner.
        - `active` — partner is trusted; tokens accepted.
        - `suspended` — temporarily disabled; tokens rejected.
        - `expired` — `expires_at` has passed; tokens rejected.
      example: active

    FederationPartner:
      type: object
      description: A registered trusted federation partner IdP.
      required:
        - id
        - name
        - issuer
        - jwks_uri
        - allowed_organizations
        - status
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
          description: Immutable system-assigned UUID for this partner.
          readOnly: true
          example: "fed-abcd-1234-5678-ef01"
        name:
          type: string
          description: Human-readable partner name.
          example: "Acme Partner IdP"
        issuer:
          type: string
          format: uri
          description: Issuer URL — must match the `iss` claim in federated tokens.
          example: "https://idp.acme-partner.example"
        jwks_uri:
          type: string
          format: uri
          description: URL of the partner's JWKS endpoint for token signature verification.
          example: "https://idp.acme-partner.example/.well-known/jwks.json"
        allowed_organizations:
          type: array
          items:
            type: string
          description: |
            Allowlist of organization_id values accepted from this partner.
            An empty array means all organizations from this partner are accepted.
          example: ["org-acme-001", "org-acme-002"]
        status:
          $ref: '#/components/schemas/FederationPartnerStatus'
        created_at:
          type: string
          format: date-time
          readOnly: true
          example: "2026-03-01T08:00:00.000Z"
        updated_at:
          type: string
          format: date-time
          readOnly: true
          example: "2026-03-28T11:30:00.000Z"
        expires_at:
          type: string
          format: date-time
          nullable: true
          description: Optional expiry timestamp. Null means the partner never expires.
          example: "2027-01-01T00:00:00.000Z"

    CreatePartnerRequest:
      type: object
      description: Request body for registering a new trusted federation partner.
      required:
        - name
        - issuer
        - jwks_uri
      properties:
        name:
          type: string
          description: Human-readable partner name.
          minLength: 1
          maxLength: 256
          example: "Acme Partner IdP"
        issuer:
          type: string
          format: uri
          description: Issuer URL of the external IdP. Must match `iss` in federated tokens.
          example: "https://idp.acme-partner.example"
        jwks_uri:
          type: string
          format: uri
          description: |
            URL of the partner's JWKS endpoint. Must be reachable at registration time —
            the endpoint is probed before the partner is persisted.
          example: "https://idp.acme-partner.example/.well-known/jwks.json"
        allowed_organizations:
          type: array
          items:
            type: string
          description: Optional allowlist of organization_id values. Defaults to empty (all allowed).
          example: ["org-acme-001"]
        expires_at:
          type: string
          format: date-time
          description: Optional ISO 8601 date-time after which the partner will be automatically expired.
          example: "2027-01-01T00:00:00.000Z"

    UpdatePartnerRequest:
      type: object
      description: |
        Request body for partially updating a federation partner.
        All fields are optional; only provided fields are updated.
      minProperties: 1
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 256
          example: "Acme Partner IdP (Updated)"
        jwks_uri:
          type: string
          format: uri
          description: Updated JWKS endpoint URL. The JWKS cache will be invalidated on update.
          example: "https://idp.acme-partner.example/.well-known/jwks.json"
        allowed_organizations:
          type: array
          items:
            type: string
          example: ["org-acme-001", "org-acme-003"]
        status:
          $ref: '#/components/schemas/FederationPartnerStatus'
        expires_at:
          type: string
          format: date-time
          nullable: true
          description: Updated expiry. Pass null to remove the expiry.
          example: "2028-01-01T00:00:00.000Z"

    FederationVerifyRequest:
      type: object
      description: Request body for verifying a federated token from a trusted partner.
      required:
        - token
      properties:
        token:
          type: string
          description: The JWT token string issued by the partner IdP.
          example: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ2VudC0wMDEiLCJpc3MiOiJodHRwczovL2lkcC5hY21lLXBhcnRuZXIuZXhhbXBsZSIsImV4cCI6MTc0MzE1NDgwMH0.signature"
        expected_issuer:
          type: string
          format: uri
          description: |
            Optional issuer hint. When provided, the token's `iss` claim must match exactly.
            Useful to prevent issuer confusion when multiple partners share JWKS.
          example: "https://idp.acme-partner.example"

    FederationVerifyResult:
      type: object
      description: Result of a successful federated token verification.
      required:
        - valid
        - issuer
        - subject
        - claims
      properties:
        valid:
          type: boolean
          description: Whether the token is valid (true when verification passed).
          example: true
        issuer:
          type: string
          format: uri
          description: The `iss` claim from the verified token.
          example: "https://idp.acme-partner.example"
        subject:
          type: string
          description: The `sub` claim from the verified token.
          example: "agent-001-external"
        organization_id:
          type: string
          description: The `organization_id` claim from the token, if present.
          example: "org-acme-001"
        claims:
          type: object
          description: All claims from the verified token payload.
          additionalProperties: true
          example:
            iss: "https://idp.acme-partner.example"
            sub: "agent-001-external"
            aud: "https://api.sentryagent.ai"
            iat: 1743151200
            exp: 1743154800
            organization_id: "org-acme-001"

    ErrorResponse:
      type: object
      description: Standard error response envelope.
      required:
        - code
        - message
      properties:
        code:
          type: string
          example: "FEDERATION_PARTNER_NOT_FOUND"
        message:
          type: string
          example: "Federation partner with the specified ID was not found."
        details:
          type: object
          additionalProperties: true

  responses:
    Unauthorized:
      description: Missing or invalid Bearer token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: "UNAUTHORIZED"
            message: "A valid Bearer token is required to access this resource."

    Forbidden:
      description: Valid token but insufficient permissions.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: "FORBIDDEN"
            message: "You do not have permission to perform this action."

    NotFound:
      description: Federation partner not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: "FEDERATION_PARTNER_NOT_FOUND"
            message: "Federation partner with the specified ID was not found."

    InternalServerError:
      description: Unexpected server error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: "INTERNAL_SERVER_ERROR"
            message: "An unexpected error occurred. Please try again later."

security:
  - BearerAuth: []

paths:
  /federation/trust:
    post:
      operationId: registerFederationPartner
      tags:
        - Federation Partners
      summary: Register a trusted federation partner
      description: |
        Registers a new external IdP as a trusted federation partner.

        The partner's JWKS endpoint (`jwks_uri`) is probed at registration time
        to confirm it is reachable. If the endpoint is unreachable, the request
        is rejected with `422 Unprocessable Entity`.

        Requires `admin:orgs` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePartnerRequest'
            example:
              name: "Acme Partner IdP"
              issuer: "https://idp.acme-partner.example"
              jwks_uri: "https://idp.acme-partner.example/.well-known/jwks.json"
              allowed_organizations:
                - "org-acme-001"
              expires_at: "2027-01-01T00:00:00.000Z"
      responses:
        '201':
          description: Federation partner registered successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FederationPartner'
              example:
                id: "fed-abcd-1234-5678-ef01"
                name: "Acme Partner IdP"
                issuer: "https://idp.acme-partner.example"
                jwks_uri: "https://idp.acme-partner.example/.well-known/jwks.json"
                allowed_organizations:
                  - "org-acme-001"
                status: "active"
                created_at: "2026-04-07T09:00:00.000Z"
                updated_at: "2026-04-07T09:00:00.000Z"
                expires_at: "2027-01-01T00:00:00.000Z"
        '400':
          description: Validation error in request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: "VALIDATION_ERROR"
                message: "Request validation failed."
                details:
                  field: "jwks_uri"
                  reason: "Must be a valid HTTPS URL."
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: A partner with the same issuer is already registered.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: "FEDERATION_PARTNER_CONFLICT"
                message: "A federation partner with this issuer is already registered."
        '422':
          description: Partner JWKS endpoint is unreachable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: "JWKS_UNREACHABLE"
                message: "The partner JWKS endpoint is unreachable. Verify the URL and try again."
        '500':
          $ref: '#/components/responses/InternalServerError'

  /federation/partners:
    get:
      operationId: listFederationPartners
      tags:
        - Federation Partners
      summary: List all federation partners
      description: |
        Returns all registered federation partners.
        Partners past their `expires_at` are returned with status `expired`.

        Requires `admin:orgs` scope.
      responses:
        '200':
          description: List of federation partners returned successfully.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/FederationPartner'
              example:
                - id: "fed-abcd-1234-5678-ef01"
                  name: "Acme Partner IdP"
                  issuer: "https://idp.acme-partner.example"
                  jwks_uri: "https://idp.acme-partner.example/.well-known/jwks.json"
                  allowed_organizations: ["org-acme-001"]
                  status: "active"
                  created_at: "2026-03-01T08:00:00.000Z"
                  updated_at: "2026-03-28T11:30:00.000Z"
                  expires_at: "2027-01-01T00:00:00.000Z"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /federation/partners/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: UUID of the federation partner.
        schema:
          type: string
          format: uuid
        example: "fed-abcd-1234-5678-ef01"

    get:
      operationId: getFederationPartner
      tags:
        - Federation Partners
      summary: Get a federation partner by ID
      description: |
        Returns a single federation partner record by its UUID.
        Requires `admin:orgs` scope.
      responses:
        '200':
          description: Federation partner returned successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FederationPartner'
              example:
                id: "fed-abcd-1234-5678-ef01"
                name: "Acme Partner IdP"
                issuer: "https://idp.acme-partner.example"
                jwks_uri: "https://idp.acme-partner.example/.well-known/jwks.json"
                allowed_organizations: ["org-acme-001"]
                status: "active"
                created_at: "2026-03-01T08:00:00.000Z"
                updated_at: "2026-03-28T11:30:00.000Z"
                expires_at: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

    patch:
      operationId: updateFederationPartner
      tags:
        - Federation Partners
      summary: Update a federation partner
      description: |
        Partially updates a federation partner's configuration.
        Only provided fields are updated.

        Changing `jwks_uri` invalidates the cached JWKS for this partner.
        Requires `admin:orgs` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdatePartnerRequest'
            example:
              status: "suspended"
              allowed_organizations:
                - "org-acme-001"
                - "org-acme-003"
      responses:
        '200':
          description: Federation partner updated successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FederationPartner'
              example:
                id: "fed-abcd-1234-5678-ef01"
                name: "Acme Partner IdP"
                issuer: "https://idp.acme-partner.example"
                jwks_uri: "https://idp.acme-partner.example/.well-known/jwks.json"
                allowed_organizations: ["org-acme-001", "org-acme-003"]
                status: "suspended"
                created_at: "2026-03-01T08:00:00.000Z"
                updated_at: "2026-04-07T09:00:00.000Z"
                expires_at: null
        '400':
          description: Validation error in request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: "VALIDATION_ERROR"
                message: "Request validation failed."
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

    delete:
      operationId: deleteFederationPartner
      tags:
        - Federation Partners
      summary: Remove a federation partner
      description: |
        Permanently removes a federation partner. After removal, tokens issued
        by the partner's IdP will no longer be accepted.

        Requires `admin:orgs` scope.
      responses:
        '204':
          description: Federation partner removed successfully. No response body.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /federation/verify:
    post:
      operationId: verifyFederatedToken
      tags:
        - Federation Verification
      summary: Verify a federated JWT token
      description: |
        Verifies a JWT token issued by a registered federation partner.

        The token's `iss` claim is matched against registered active partners.
        The signature is verified against the partner's JWKS.
        The partner's `allowed_organizations` filter is applied if non-empty.

        Returns verification result with claims on success.
        Returns `422` with verification failure details on invalid tokens.

        Requires any valid Bearer token (`agents:read` scope is sufficient).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FederationVerifyRequest'
            example:
              token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ2VudC0wMDEiLCJpc3MiOiJodHRwczovL2lkcC5hY21lLXBhcnRuZXIuZXhhbXBsZSJ9.signature"
              expected_issuer: "https://idp.acme-partner.example"
      responses:
        '200':
          description: Token verification result returned.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FederationVerifyResult'
              examples:
                valid:
                  summary: Token verified successfully
                  value:
                    valid: true
                    issuer: "https://idp.acme-partner.example"
                    subject: "agent-001-external"
                    organization_id: "org-acme-001"
                    claims:
                      iss: "https://idp.acme-partner.example"
                      sub: "agent-001-external"
                      aud: "https://api.sentryagent.ai"
                      iat: 1743151200
                      exp: 1743154800
                invalid:
                  summary: Token invalid or expired
                  value:
                    valid: false
                    issuer: ""
                    subject: ""
                    claims: {}
        '400':
          description: Missing or malformed token in request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: "VALIDATION_ERROR"
                message: "The 'token' field is required."
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          description: |
            Token is well-formed but verification failed (unknown issuer, expired,
            invalid signature, or organization not in allowlist).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                unknownIssuer:
                  summary: Unknown issuer
                  value:
                    code: "UNKNOWN_FEDERATION_ISSUER"
                    message: "No active federation partner found for this token issuer."
                partnerSuspended:
                  summary: Partner is suspended
                  value:
                    code: "FEDERATION_PARTNER_SUSPENDED"
                    message: "The federation partner for this token issuer is suspended."
                orgNotAllowed:
                  summary: Organization not in allowlist
                  value:
                    code: "FEDERATION_ORG_NOT_ALLOWED"
                    message: "The token's organization_id is not in the partner's allowed_organizations list."
        '500':
          $ref: '#/components/responses/InternalServerError'
