openapi: 3.0.3
info:
  title: SOAT API Keys API
  version: 1.0.0
  description: API for managing API keys (optionally project-scoped) with optional policy attachments
  contact:
    name: SOAT Team
    url: https://github.com/ttoss/soat
servers:
  - url: '{baseUrl}'
    description: Base URL of your SOAT deployment (e.g. https://your-soat.com or http://localhost:5047)
    variables:
      baseUrl:
        description: The base URL of your SOAT deployment
        default: http://localhost:5047
tags:
  - name: API Keys
    description: Manage API keys
security:
  - bearerAuth: []
paths:
  /api/v1/api-keys:
    get:
      tags:
        - API Keys
      summary: List API keys
      description: >
        Lists API keys accessible to the caller.
        - JWT admin: returns all API keys.
        - JWT regular user: returns only the user's own API keys.
        - Project-scoped credential (API key or OAuth token): returns only API keys scoped to that project.
      operationId: listApiKeys
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of API keys
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ApiKeyRecord'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
    post:
      tags:
        - API Keys
      summary: Create an API key
      description: >
        Creates a new API key for the authenticated user.
        - `project_id` is optional. When set, the key is scoped to that single project. When omitted or null, the key is **unscoped** and spans every project its owner can reach.
        - If `policy_ids` is provided, the key's effective permissions are the intersection of the user's policies and the key's policies.
        - Otherwise the key inherits the user's permissions (confined to the key's project when scoped).
        - When the request is authenticated with a **project-scoped credential**, the new key is confined to that same project: omitting `project_id` defaults to it, naming a different project returns `403 API_KEY_PROJECT_SCOPE`, and `project_id: null` (an unscoped key) is likewise refused. Minting an unscoped key requires an unscoped credential.
      operationId: createApiKey
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  description: Key name for identification
                  example: CI/CD Pipeline
                project_id:
                  type: string
                  nullable: true
                  description: Optional project ID to scope the key to. Omit or set null to create an unscoped key that spans projects.
                  example: proj_V1StGXR8Z5jdHi6B
                  x-soat-ref: projects
                policy_ids:
                  x-soat-ref: policies
                  type: array
                  items:
                    type: string
                  description: Optional list of policy IDs to attach. Key permissions become the intersection of user policies and these policies.
                  example: ['pol_V1StGXR8Z5jdHi6B']
      responses:
        '201':
          description: API key created successfully. The raw key value is only returned once.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyCreated'
        '400':
          description: Bad request (missing name, invalid project or policy IDs)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden (a project-scoped credential named a different project, or asked for an unscoped key)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/api-keys/{api_key_id}:
    get:
      tags:
        - API Keys
      summary: Get an API key
      description: Returns details of an API key. Only the owner or an admin can access it, and a project-scoped credential can only reach keys in its own project.
      operationId: getApiKey
      parameters:
        - name: api_key_id
          in: path
          required: true
          description: API key public ID (key_ prefix)
          schema:
            type: string
            example: key_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: API key details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyRecord'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden (not the key owner or admin, or the credential is scoped to a different project)
        '404':
          description: API key not found
    put:
      tags:
        - API Keys
      summary: Update an API key
      description: Updates an API key's name, project scope, or policies. The project scope can be changed to another project, set (scoping a previously unscoped key), or cleared with null (unscoping the key). Only the owner or an admin can update it. A project-scoped credential can only update keys in its own project, and cannot move a key to another project or unscope it.
      operationId: updateApiKey
      parameters:
        - name: api_key_id
          in: path
          required: true
          description: API key public ID (key_ prefix)
          schema:
            type: string
            example: key_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  example: Updated Key Name
                project_id:
                  x-soat-ref: projects
                  type: string
                  nullable: true
                  description: Re-scope the key to a different project, or set null to clear the scope (unscoped key). Omit to leave the scope unchanged.
                  example: proj_V1StGXR8Z5jdHi6B
                policy_ids:
                  x-soat-ref: policies
                  type: array
                  items:
                    type: string
                  description: Replace the key's policy list (empty array removes all)
                  example: ['pol_V1StGXR8Z5jdHi6B']
      responses:
        '200':
          description: API key updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyRecord'
        '400':
          description: Bad request (invalid project or policy IDs)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden (not the key owner or admin, or the credential is scoped to a different project)
        '404':
          description: API key not found
    delete:
      tags:
        - API Keys
      summary: Delete an API key
      description: Deletes an API key. Only the owner or an admin can delete it, and a project-scoped credential can only delete keys in its own project.
      operationId: deleteApiKey
      parameters:
        - name: api_key_id
          in: path
          required: true
          description: API key public ID (key_ prefix)
          schema:
            type: string
            example: key_V1StGXR8Z5jdHi6B
      responses:
        '204':
          description: API key deleted successfully
        '401':
          description: Unauthorized
        '403':
          description: Forbidden (not the key owner or admin, or the credential is scoped to a different project)
        '404':
          description: API key not found
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: JWT token or sk_ api key
  schemas:
    ApiKeyRecord:
      type: object
      properties:
        id:
          type: string
          description: Public API key ID (key_ prefix)
          example: key_V1StGXR8Z5jdHi6B
        name:
          type: string
          example: CI/CD Pipeline
        key_prefix:
          type: string
          description: First 8 characters of the raw key for identification
          example: sk_a1b2c3
        user_id:
          x-soat-ref: users
          type: string
          description: Owner user public ID
          example: user_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          nullable: true
          description: The project this key is scoped to, or null for an unscoped key that spans projects
          example: proj_V1StGXR8Z5jdHi6B
        policy_ids:
          x-soat-ref: policies
          type: array
          items:
            type: string
          description: Public IDs of policies attached to this key
          example: ['pol_V1StGXR8Z5jdHi6B']
        created_at:
          type: string
          format: date-time
          example: '2024-01-01T00:00:00.000Z'
        updated_at:
          type: string
          format: date-time
          example: '2024-01-01T00:00:00.000Z'
    ApiKeyCreated:
      allOf:
        - $ref: '#/components/schemas/ApiKeyRecord'
        - type: object
          properties:
            key:
              type: string
              description: The raw API key value (only returned once at creation). Use as Bearer token.
              example: sk_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4
    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          description: >-
            Structured error. Every error response uses this shape — 401, 403
            and the 500 catch-all included — so `code` can be read without
            first checking the type of `error`.
          required:
            - code
            - message
            - hint
            - docs_url
          properties:
            code:
              type: string
              description: A key from the server's ERROR_CODES registry.
              example: RESOURCE_NOT_FOUND
            message:
              type: string
              example: 'Resource not found'
            hint:
              type: string
              description: >-
                What to do about this error. Resolved per code, so a caller that
                has never seen the code before can act on the response without
                leaving it.
              example: >-
                Check the id, and check that the credential can see the project
                that owns the resource.
            docs_url:
              type: string
              format: uri
              description: The reference-page anchor documenting this code.
              example: >-
                https://soat.ttoss.dev/docs/error-codes#resource_not_found
            meta:
              type: object
              description: Optional structured context for the error.
