openapi: 3.1.0
info:
  title: CAIPE Credential Store and Exchange API
  version: 0.1.0
  description: >
    Planning contract for the feature-toggled Connections & Secrets APIs.
    Browser-facing APIs return metadata and masked fields only, except that
    browsers may submit raw values during create, rotate, or OAuth callback
    flows. Retrieval and exchange are standard service-to-service APIs that
    return credential material only to approved server-side consumers after
    JWT, caller classification, intended-use, and OpenFGA authorization.
servers:
  - url: /api
security:
  - bearerAuth: []
paths:
  /credentials/secrets:
    get:
      summary: List credential references visible to the caller
      operationId: listCredentialSecrets
      parameters:
        - name: kind
          in: query
          schema:
            type: string
            enum: [byo_secret, connector_client_secret, provider_token_set, migration_candidate]
        - name: owner_type
          in: query
          schema:
            type: string
            enum: [user, team, service, system]
      responses:
        "200":
          description: Non-secret credential metadata
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/SecretRef"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/FeatureDisabled"
    post:
      summary: Create a user or team credential reference
      operationId: createCredentialSecret
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSecretRequest"
      responses:
        "201":
          description: Created credential reference with masked value metadata
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SecretRef"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
  /credentials/secrets/{secret_id}:
    get:
      summary: Read non-secret metadata for a credential reference
      operationId: getCredentialSecret
      parameters:
        - $ref: "#/components/parameters/SecretId"
      responses:
        "200":
          description: Secret metadata
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SecretRef"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      summary: Update metadata, rotate value, share, revoke, or delete a credential reference
      operationId: updateCredentialSecret
      parameters:
        - $ref: "#/components/parameters/SecretId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateSecretRequest"
      responses:
        "200":
          description: Updated metadata
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SecretRef"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
  /credentials/retrieve:
    post:
      summary: Standard service-to-service credential retrieval
      operationId: retrieveCredentialMaterial
      description: >
        Requires a service JWT or approved OBO/user JWT with the expected
        audience, server-side caller classification, acting subject, intended
        use, and resource context. Authorization is evaluated before decrypt.
        This endpoint is not callable from browser UI code, session-only
        requests, or browser-accessible tokens.
      x-internal: true
      x-browser-access: forbidden
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RetrieveCredentialRequest"
      responses:
        "200":
          description: Credential material for an approved server-side consumer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RetrieveCredentialResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/Unavailable"
  /credentials/exchange:
    post:
      summary: Standard service-to-service provider credential exchange
      operationId: exchangeProviderCredential
      description: >
        Used by Dynamic Agents, MCP launchers, and approved internal services to
        obtain or inject a provider credential for the acting user and resource.
        This endpoint is not callable from browser UI code, session-only
        requests, or browser-accessible tokens.
      x-internal: true
      x-browser-access: forbidden
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ExchangeRequest"
      responses:
        "200":
          description: Provider credential response for server-side use
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExchangeResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/Unavailable"
  /credentials/inject/{provider_key}:
    get:
      summary: AgentGateway provider credential injector
      operationId: injectProviderCredentialHeaders
      description: >
        Future AgentGateway integration contract for resolving the calling user's
        connected provider token and returning it as upstream response headers.
        The deployed AgentGateway v0.12 path does not support backend
        response-header injection, so current Jira traffic uses the
        Dynamic Agents/Jira connector credential-exchange path instead.
        Browser-origin, session-cookie, wrong-audience, and non-AgentGateway
        caller requests are denied before any provider token refresh occurs.
      x-internal: true
      x-browser-access: forbidden
      parameters:
        - $ref: "#/components/parameters/ProviderKey"
      responses:
        "200":
          description: Provider credential was injected into response headers for AgentGateway
          headers:
            X-CAIPE-Provider-Token:
              description: Provider OAuth bearer token for the upstream MCP request
              schema:
                type: string
            X-CAIPE-Provider-Connection-Id:
              description: Provider connection id used for audit and diagnostics
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InjectResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/Unavailable"
  /credentials/oauth-connectors:
    get:
      summary: List OAuth connectors
      operationId: listOAuthConnectors
      responses:
        "200":
          description: Connector metadata with masked client secret state
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/OAuthConnector"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      summary: Create or import an OAuth connector
      operationId: createOAuthConnector
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateOAuthConnectorRequest"
      responses:
        "201":
          description: Connector metadata
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthConnector"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
  /credentials/oauth-connectors/{connector_id}:
    patch:
      summary: Test, enable, disable, rotate, or update an OAuth connector
      operationId: updateOAuthConnector
      parameters:
        - $ref: "#/components/parameters/ConnectorId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateOAuthConnectorRequest"
      responses:
        "200":
          description: Connector metadata
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthConnector"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
  /credentials/oauth/{provider_key}/connect:
    post:
      summary: Start provider OAuth consent
      operationId: startProviderConnection
      parameters:
        - $ref: "#/components/parameters/ProviderKey"
      responses:
        "200":
          description: Authorization redirect details
          content:
            application/json:
              schema:
                type: object
                required: [authorization_url, state_id]
                properties:
                  authorization_url:
                    type: string
                    format: uri
                  state_id:
                    type: string
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
  /credentials/oauth/{provider_key}/callback:
    get:
      summary: OAuth callback endpoint
      operationId: completeProviderConnection
      parameters:
        - $ref: "#/components/parameters/ProviderKey"
        - name: code
          in: query
          required: true
          schema:
            type: string
        - name: state
          in: query
          required: true
          schema:
            type: string
      responses:
        "302":
          description: Redirects to Connections & Secrets UI with non-secret status
        "400":
          $ref: "#/components/responses/BadRequest"
        "409":
          $ref: "#/components/responses/Conflict"
  /credentials/connections:
    get:
      summary: List provider connections for the caller
      operationId: listProviderConnections
      responses:
        "200":
          description: Provider connection metadata
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/ProviderConnection"
  /credentials/connections/{connection_id}:
    patch:
      summary: Disconnect, revoke, reconnect, or update sharing for a provider connection
      operationId: updateProviderConnection
      parameters:
        - $ref: "#/components/parameters/ConnectionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateProviderConnectionRequest"
      responses:
        "200":
          description: Provider connection metadata
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProviderConnection"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
  /credentials/migrations/preview:
    post:
      summary: Preview credential-shaped values in existing records
      operationId: previewCredentialMigration
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MigrationPreviewRequest"
      responses:
        "200":
          description: Non-secret migration candidates
          content:
            application/json:
              schema:
                type: object
                properties:
                  preview_id:
                    type: string
                  candidates:
                    type: array
                    items:
                      $ref: "#/components/schemas/MigrationCandidate"
        "403":
          $ref: "#/components/responses/Forbidden"
  /credentials/health:
    get:
      summary: Credential feature dependency health
      operationId: getCredentialHealth
      responses:
        "200":
          description: Non-secret health status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CredentialHealth"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  parameters:
    SecretId:
      name: secret_id
      in: path
      required: true
      schema:
        type: string
    ConnectorId:
      name: connector_id
      in: path
      required: true
      schema:
        type: string
    ProviderKey:
      name: provider_key
      in: path
      required: true
      schema:
        type: string
    ConnectionId:
      name: connection_id
      in: path
      required: true
      schema:
        type: string
  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Unauthorized:
      description: Missing, expired, or wrong-audience JWT
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Forbidden:
      description: Authenticated caller is not authorized
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    NotFound:
      description: Resource not found or not discoverable
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Conflict:
      description: State conflict such as revoked, reconnect required, scope required, or disabled connector
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    FeatureDisabled:
      description: Credential feature toggle is disabled
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Unavailable:
      description: Credential store, KMS, policy service, or provider dependency unavailable
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
  schemas:
    SecretRef:
      type: object
      required: [secret_id, display_name, kind, owner_type, owner_id, visibility, status, current_version]
      properties:
        secret_id:
          type: string
        display_name:
          type: string
        description:
          type: string
        kind:
          type: string
          enum: [byo_secret, connector_client_secret, provider_token_set, migration_candidate]
        owner_type:
          type: string
          enum: [user, team, service, system]
        owner_id:
          type: string
        visibility:
          type: string
          enum: [personal, team, team_shared, system]
        status:
          type: string
          enum: [active, rotating, revoked, deleted, drift_detected]
        current_version:
          type: integer
        masked:
          type: boolean
          const: true
    CreateSecretRequest:
      type: object
      required: [display_name, kind, owner_type, owner_id, secret_value]
      properties:
        display_name:
          type: string
        kind:
          type: string
          enum: [byo_secret, connector_client_secret]
        owner_type:
          type: string
          enum: [user, team, service, system]
        owner_id:
          type: string
        secret_value:
          type: string
          writeOnly: true
        tags:
          type: array
          items:
            type: string
    UpdateSecretRequest:
      type: object
      properties:
        display_name:
          type: string
        action:
          type: string
          enum: [update_metadata, rotate, share, revoke, delete]
        secret_value:
          type: string
          writeOnly: true
        target_team_id:
          type: string
    RetrieveCredentialRequest:
      type: object
      required: [secret_id, caller_type, intended_use, resource_context]
      properties:
        secret_id:
          type: string
        caller_type:
          type: string
          enum: [agentgateway, dynamic_agent, mcp_runtime, internal_service, credential_exchange]
        intended_use:
          type: string
          enum: [mcp_env, authorization_header, api_key, oauth_bearer, connector_refresh]
        acting_user_id:
          type: string
        service_id:
          type: string
        resource_context:
          $ref: "#/components/schemas/ResourceContext"
    RetrieveCredentialResponse:
      type: object
      required: [secret_id, version, credential_type, material]
      properties:
        secret_id:
          type: string
        version:
          type: integer
        credential_type:
          type: string
          enum: [opaque, api_key, bearer_token, client_secret, oauth_token_set]
        material:
          type: object
          additionalProperties: true
          writeOnly: true
    ExchangeRequest:
      type: object
      required: [provider_key, caller_type, intended_use, resource_context]
      properties:
        provider_key:
          type: string
        caller_type:
          type: string
          enum: [dynamic_agent, mcp_runtime, internal_service]
        provider_resource_id:
          type: string
        required_scopes:
          type: array
          items:
            type: string
        intended_use:
          type: string
          enum: [github_mcp, jira_mcp, confluence_mcp, internal_service]
        resource_context:
          $ref: "#/components/schemas/ResourceContext"
    ExchangeResponse:
      type: object
      required: [provider_key, credential_type, expires_at, material]
      properties:
        provider_key:
          type: string
        provider_resource_id:
          type: string
        credential_type:
          type: string
          enum: [oauth_bearer, oauth_token_set]
        expires_at:
          type: string
          format: date-time
        material:
          type: object
          additionalProperties: true
          writeOnly: true
    InjectResponse:
      type: object
      required: [ok, provider, provider_connection_id]
      properties:
        ok:
          type: boolean
          const: true
        provider:
          type: string
        provider_connection_id:
          type: string
        expires_in:
          type: integer
    OAuthConnector:
      type: object
      required: [connector_id, provider_key, display_name, type, status, authorization_url, token_url, client_id, client_secret_masked, requested_scopes]
      properties:
        connector_id:
          type: string
        provider_key:
          type: string
        display_name:
          type: string
        type:
          type: string
          enum: [built_in, custom_oauth]
        status:
          type: string
          enum: [draft, enabled, disabled, validation_failed, deleted]
        authorization_url:
          type: string
          format: uri
        token_url:
          type: string
          format: uri
        userinfo_url:
          type: string
          format: uri
        accessible_resources_url:
          type: string
          format: uri
        redirect_uri:
          type: string
          format: uri
        client_id:
          type: string
        client_secret_masked:
          type: boolean
          const: true
        requested_scopes:
          type: array
          items:
            type: string
        refresh_policy:
          type: string
          enum: [rotate_refresh_token, reuse_refresh_token, no_refresh, provider_default]
    CreateOAuthConnectorRequest:
      allOf:
        - $ref: "#/components/schemas/OAuthConnector"
        - type: object
          required: [client_secret]
          properties:
            client_secret:
              type: string
              writeOnly: true
    UpdateOAuthConnectorRequest:
      type: object
      properties:
        action:
          type: string
          enum: [test, enable, disable, rotate_client_secret, update_metadata, delete]
        client_secret:
          type: string
          writeOnly: true
        requested_scopes:
          type: array
          items:
            type: string
    ProviderConnection:
      type: object
      required: [connection_id, connector_id, provider_key, subject_user_id, state, granted_scopes]
      properties:
        connection_id:
          type: string
        connector_id:
          type: string
        provider_key:
          type: string
        subject_user_id:
          type: string
        share_scope:
          type: string
          enum: [personal, team_shared]
        provider_account_id:
          type: string
        provider_account_name:
          type: string
        provider_resource_id:
          type: string
        granted_scopes:
          type: array
          items:
            type: string
        state:
          type: string
          enum: [not_connected, pending_consent, active, refresh_required, reconsent_required, reconnect_required, revoked, failed]
        expires_at:
          type: string
          format: date-time
        masked:
          type: boolean
          const: true
    UpdateProviderConnectionRequest:
      type: object
      required: [action]
      properties:
        action:
          type: string
          enum: [disconnect, revoke, reconnect, share_with_team, remove_team_share]
        team_id:
          type: string
    MigrationPreviewRequest:
      type: object
      required: [source_types]
      properties:
        source_types:
          type: array
          items:
            type: string
            enum: [mcp_server_env, skill_hub_credentials_ref, catalog_api_key]
        source_ids:
          type: array
          items:
            type: string
    MigrationCandidate:
      type: object
      required: [source_type, source_id, candidate_field, risk, recommendation]
      properties:
        source_type:
          type: string
        source_id:
          type: string
        candidate_field:
          type: string
        risk:
          type: string
          enum: [low, medium, high]
        recommendation:
          type: string
          enum: [leave_static, migrate_to_secret_ref, manual_review, unsupported]
    ResourceContext:
      type: object
      required: [resource_type, resource_id]
      properties:
        resource_type:
          type: string
          enum: [dynamic_agent, mcp_server, tool, internal_service, oauth_connector]
        resource_id:
          type: string
        tool_name:
          type: string
        team_id:
          type: string
    CredentialHealth:
      type: object
      required: [feature_enabled, credential_store, key_wrapper, policy_service]
      properties:
        feature_enabled:
          type: boolean
        credential_store:
          type: string
          enum: [healthy, degraded, unavailable]
        key_wrapper:
          type: string
          enum: [healthy, degraded, unavailable]
        policy_service:
          type: string
          enum: [healthy, degraded, unavailable]
    ErrorResponse:
      type: object
      required: [error, reason_code, correlation_id]
      properties:
        error:
          type: string
        reason_code:
          type: string
        correlation_id:
          type: string
