openapi: 3.0.3

info:
  title: SentryAgent.ai — OAuth 2.0 Token Service
  version: 1.0.0
  description: |
    The OAuth 2.0 Token Service provides agent authentication via the
    **Client Credentials grant** (RFC 6749 Section 4.4). It issues signed JWT
    access tokens, supports token introspection (RFC 7662), and token revocation
    (RFC 7009).

    Agents authenticate using their `client_id` (= `agentId`) and
    `client_secret` obtained during credential provisioning.

    **Supported Grant Type**: `client_credentials` only. All other grant types
    are rejected with `unsupported_grant_type`.

    **Token Lifetime**: 3600 seconds (1 hour) by default.

    **Scopes**:
    | Scope | Description |
    |-------|-------------|
    | `agents:read` | Read agent identity records |
    | `agents:write` | Create, update, and deactivate agent records |
    | `tokens:read` | Introspect tokens |
    | `audit:read` | Query the audit log |

    **Rate Limit**: 100 requests/minute per `client_id`.

    **Free Tier**: 10,000 token requests per month.

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

tags:
  - name: OAuth 2.0 Tokens
    description: Token issuance, introspection, and revocation

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        JWT access token obtained via `POST /token`.
        Required for `/token/introspect` and `/token/revoke`.

    BasicAuth:
      type: http
      scheme: basic
      description: |
        HTTP Basic authentication using `client_id` as the username and
        `client_secret` as the password. Used as an alternative credential
        method for token endpoint requests (in addition to request body).

  schemas:
    GrantType:
      type: string
      description: OAuth 2.0 grant type. Only `client_credentials` is supported.
      enum:
        - client_credentials
      example: client_credentials

    Scope:
      type: string
      description: |
        Space-separated list of requested OAuth 2.0 scopes.
        Available scopes: `agents:read`, `agents:write`, `tokens:read`, `audit:read`.
      pattern: '^(agents:read|agents:write|tokens:read|audit:read)(\s(agents:read|agents:write|tokens:read|audit:read))*$'
      example: "agents:read agents:write"

    TokenRequest:
      type: object
      description: |
        OAuth 2.0 Client Credentials token request body.
        Credentials may be provided in the request body (as `client_id` +
        `client_secret`) or via HTTP Basic authentication header.
      required:
        - grant_type
      properties:
        grant_type:
          $ref: '#/components/schemas/GrantType'
        client_id:
          type: string
          format: uuid
          description: >
            The agent's `agentId` (UUID). Required if not using HTTP Basic auth.
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        client_secret:
          type: string
          description: >
            The agent's client secret. Required if not using HTTP Basic auth.
            Treated as a sensitive value — never logged or stored in plain text.
          format: password
          example: "sk_live_7f3a2b1c9d8e4f0a6b5c3d2e1f0a9b8c"
        scope:
          $ref: '#/components/schemas/Scope'

    TokenResponse:
      type: object
      description: Successful OAuth 2.0 token response.
      required:
        - access_token
        - token_type
        - expires_in
        - scope
      properties:
        access_token:
          type: string
          description: >
            Signed JWT access token. Include this value in the `Authorization`
            header as `Bearer <access_token>` when calling other API endpoints.
          example: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJjbGllbnRfaWQiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJzY29wZSI6ImFnZW50czpyZWFkIGFnZW50czp3cml0ZSIsImlhdCI6MTc0MzE1MTIwMCwiZXhwIjoxNzQzMTU0ODAwfQ.signature"
        token_type:
          type: string
          description: Token type. Always `Bearer`.
          enum:
            - Bearer
          example: "Bearer"
        expires_in:
          type: integer
          description: Token lifetime in seconds from the time of issuance. Default is `3600`.
          example: 3600
        scope:
          type: string
          description: Space-separated list of scopes granted by this token.
          example: "agents:read agents:write"

    OAuth2ErrorResponse:
      type: object
      description: |
        OAuth 2.0 error response as defined in RFC 6749 Section 5.2.
        Used exclusively for token endpoint errors.
      required:
        - error
        - error_description
      properties:
        error:
          type: string
          description: >
            Machine-readable OAuth 2.0 error code.
          enum:
            - invalid_request
            - invalid_client
            - invalid_grant
            - unauthorized_client
            - unsupported_grant_type
            - invalid_scope
          example: "invalid_client"
        error_description:
          type: string
          description: Human-readable description of the error.
          example: "Client authentication failed. Invalid client_id or client_secret."

    IntrospectRequest:
      type: object
      description: Token introspection request (RFC 7662).
      required:
        - token
      properties:
        token:
          type: string
          description: The token to introspect.
          example: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAifQ.signature"
        token_type_hint:
          type: string
          description: >
            Optional hint about the type of token being introspected.
            Currently only `access_token` is supported.
          enum:
            - access_token
          example: "access_token"

    IntrospectResponse:
      type: object
      description: |
        Token introspection response (RFC 7662).
        When `active` is `false`, no other fields are guaranteed to be present.
      required:
        - active
      properties:
        active:
          type: boolean
          description: >
            Whether the token is currently active (valid, not expired, not revoked).
          example: true
        sub:
          type: string
          format: uuid
          description: Subject — the `agentId` the token was issued for.
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        client_id:
          type: string
          format: uuid
          description: The `client_id` (agentId) that requested the token.
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        scope:
          type: string
          description: Space-separated list of scopes granted by this token.
          example: "agents:read agents:write"
        token_type:
          type: string
          description: Token type. Always `Bearer` for active tokens.
          example: "Bearer"
        iat:
          type: integer
          description: Unix timestamp (seconds) when the token was issued.
          example: 1743151200
        exp:
          type: integer
          description: Unix timestamp (seconds) when the token expires.
          example: 1743154800

    RevokeRequest:
      type: object
      description: Token revocation request (RFC 7009).
      required:
        - token
      properties:
        token:
          type: string
          description: The token to revoke.
          example: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAifQ.signature"
        token_type_hint:
          type: string
          description: Optional hint about the token type.
          enum:
            - access_token
          example: "access_token"

    ErrorResponse:
      type: object
      description: Standard error response envelope used across all SentryAgent.ai APIs.
      required:
        - code
        - message
      properties:
        code:
          type: string
          description: Machine-readable error code.
          example: "UNAUTHORIZED"
        message:
          type: string
          description: Human-readable description of the error.
          example: "A valid Bearer token is required."
        details:
          type: object
          description: Optional structured details providing additional context.
          additionalProperties: true
          example: {}

  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."

    TooManyRequests:
      description: Rate limit exceeded. Retry after the reset time.
      headers:
        X-RateLimit-Limit:
          schema:
            type: integer
          description: Maximum requests allowed per minute.
          example: 100
        X-RateLimit-Remaining:
          schema:
            type: integer
          description: Requests remaining in the current window.
          example: 0
        X-RateLimit-Reset:
          schema:
            type: integer
          description: Unix timestamp when the rate limit window resets.
          example: 1743155400
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: "RATE_LIMIT_EXCEEDED"
            message: "Too many requests. Please retry after the rate limit window resets."

    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."

paths:
  /token:
    post:
      operationId: issueToken
      tags:
        - OAuth 2.0 Tokens
      summary: Issue an access token (Client Credentials)
      description: |
        Issues a signed JWT access token for an agent using the OAuth 2.0
        **Client Credentials grant** (RFC 6749 §4.4).

        The agent authenticates by providing its `client_id` (agentId) and
        `client_secret`. Credentials may be passed either:
        - In the **request body** (`client_id` + `client_secret` fields), or
        - Via **HTTP Basic authentication** header (username = `client_id`, password = `client_secret`).

        The token is a signed JWT containing the agent's identity claims.
        Use it as a `Bearer` token on subsequent API calls.

        **Free Tier Limit**: 10,000 token requests per month. Exceeding this
        returns `403` with `FREE_TIER_LIMIT_EXCEEDED`.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/TokenRequest'
            example:
              grant_type: client_credentials
              client_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              client_secret: "sk_live_7f3a2b1c9d8e4f0a6b5c3d2e1f0a9b8c"
              scope: "agents:read agents:write"
      responses:
        '200':
          description: Access token issued successfully.
          headers:
            X-RateLimit-Limit:
              schema:
                type: integer
              example: 100
            X-RateLimit-Remaining:
              schema:
                type: integer
              example: 99
            X-RateLimit-Reset:
              schema:
                type: integer
              example: 1743155400
            Cache-Control:
              schema:
                type: string
              description: Token responses must not be cached.
              example: "no-store"
            Pragma:
              schema:
                type: string
              example: "no-cache"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
              example:
                access_token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJjbGllbnRfaWQiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJzY29wZSI6ImFnZW50czpyZWFkIGFnZW50czp3cml0ZSIsImlhdCI6MTc0MzE1MTIwMCwiZXhwIjoxNzQzMTU0ODAwfQ.signature"
                token_type: "Bearer"
                expires_in: 3600
                scope: "agents:read agents:write"
        '400':
          description: Malformed or missing required request parameters.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuth2ErrorResponse'
              examples:
                missingGrantType:
                  summary: Missing grant_type
                  value:
                    error: "invalid_request"
                    error_description: "The 'grant_type' parameter is required."
                invalidScope:
                  summary: Invalid scope requested
                  value:
                    error: "invalid_scope"
                    error_description: "Requested scope 'admin:all' is not available."
                unsupportedGrantType:
                  summary: Unsupported grant type
                  value:
                    error: "unsupported_grant_type"
                    error_description: "Only 'client_credentials' grant type is supported."
        '401':
          description: Client authentication failed. Invalid `client_id` or `client_secret`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuth2ErrorResponse'
              example:
                error: "invalid_client"
                error_description: "Client authentication failed. Invalid client_id or client_secret."
        '403':
          description: >
            Client is not authorised to request a token. May indicate the agent
            is suspended, decommissioned, or the free tier monthly limit has been reached.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuth2ErrorResponse'
              examples:
                agentSuspended:
                  summary: Agent is suspended
                  value:
                    error: "unauthorized_client"
                    error_description: "Agent is currently suspended and cannot obtain tokens."
                freeTierLimit:
                  summary: Monthly token limit reached
                  value:
                    error: "unauthorized_client"
                    error_description: "Free tier monthly token limit of 10,000 requests has been reached."
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /token/introspect:
    post:
      operationId: introspectToken
      tags:
        - OAuth 2.0 Tokens
      summary: Introspect a token (RFC 7662)
      description: |
        Determines whether a given access token is currently active (valid,
        not expired, not revoked). Returns the token's metadata if active.

        Compliant with RFC 7662 (OAuth 2.0 Token Introspection).

        The caller must present a valid Bearer token with `tokens:read` scope
        to use this endpoint.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/IntrospectRequest'
            example:
              token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAifQ.signature"
              token_type_hint: "access_token"
      responses:
        '200':
          description: |
            Token introspection result. Note: a `200` response is returned even
            for inactive tokens — check the `active` field to determine token validity.
          headers:
            X-RateLimit-Limit:
              schema:
                type: integer
              example: 100
            X-RateLimit-Remaining:
              schema:
                type: integer
              example: 98
            X-RateLimit-Reset:
              schema:
                type: integer
              example: 1743155400
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntrospectResponse'
              examples:
                activeToken:
                  summary: Active token
                  value:
                    active: true
                    sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    client_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    scope: "agents:read agents:write"
                    token_type: "Bearer"
                    iat: 1743151200
                    exp: 1743154800
                inactiveToken:
                  summary: Inactive (expired or revoked) token
                  value:
                    active: false
        '400':
          description: Missing or malformed `token` parameter.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: "VALIDATION_ERROR"
                message: "The 'token' parameter is required."
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller's token does not have the `tokens:read` scope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: "INSUFFICIENT_SCOPE"
                message: "The 'tokens:read' scope is required to introspect tokens."
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /token/revoke:
    post:
      operationId: revokeToken
      tags:
        - OAuth 2.0 Tokens
      summary: Revoke a token (RFC 7009)
      description: |
        Revokes an access token, immediately invalidating it for all subsequent
        requests. Compliant with RFC 7009 (OAuth 2.0 Token Revocation).

        Revoking an already-revoked or expired token is considered a success
        (idempotent operation per RFC 7009 §2.1).

        The caller must present a valid Bearer token to revoke another token.
        An agent may revoke its own tokens; admin scope is required to revoke
        tokens belonging to other agents.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/RevokeRequest'
            example:
              token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAifQ.signature"
              token_type_hint: "access_token"
      responses:
        '200':
          description: |
            Token revoked successfully (or was already inactive).
            Per RFC 7009, revocation always returns `200` for any valid request,
            even if the token was already revoked or expired.
          headers:
            X-RateLimit-Limit:
              schema:
                type: integer
              example: 100
            X-RateLimit-Remaining:
              schema:
                type: integer
              example: 97
            X-RateLimit-Reset:
              schema:
                type: integer
              example: 1743155400
          content:
            application/json:
              schema:
                type: object
                properties: {}
              example: {}
        '400':
          description: Missing or malformed `token` parameter.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: "VALIDATION_ERROR"
                message: "The 'token' parameter is required."
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Insufficient permissions to revoke this token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: "FORBIDDEN"
                message: "You do not have permission to revoke this token."
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'
