openapi: 3.1.0
info:
  title: CloudCruise Custom Secret Provider API
  version: 1.0.0
  summary: The API your password manager exposes so CloudCruise can resolve vault credentials from it at run time.
  description: |
    > **Stable.** The endpoints below will not change incompatibly within `v1`.
    > Additional endpoints and optional fields may be added in
    > backwards-compatible ways.

    Implement this API in front of your own password manager to use it with
    CloudCruise the same way as the built-in 1Password integration. You host
    the API; CloudCruise calls it.

    All endpoints are `POST` with a JSON body, including read operations. Item
    references travel in the body, never in the URL, so they are never altered
    by URL encoding and do not appear in access logs.

    | Endpoint | Required | Used for |
    | --- | --- | --- |
    | `POST /v1/capabilities` | yes | Connection test; reports whether you implement item listing |
    | `POST /v1/items/resolve` | yes | Fetching the secret values of one item |
    | `POST /v1/items/describe` | yes | Showing the bound item's name in the dashboard |
    | `POST /v1/items/list` | if `item_listing` | Letting users pick an item instead of typing its `ref` |

    ## Request authentication

    Every request is signed with HMAC-SHA256 using the connection's signing
    secret and carries the `X-CloudCruise-Timestamp`,
    `X-CloudCruise-Request-Id`, `X-CloudCruise-Connection-Id` and
    `X-CloudCruise-Signature` headers. The signed string, the verification
    rules, a Node.js example and a test vector are in the
    [Custom provider guide](https://docs.cloudcruise.com/integrations/custom-provider).

servers:
  - url: "{baseUrl}"
    description: The HTTPS base URL you configure on the CloudCruise connection.
    variables:
      baseUrl:
        default: https://pm.example.com/cloudcruise

security:
  - cloudcruiseSignature: []
    cloudcruiseTimestamp: []

paths:
  /v1/capabilities:
    post:
      operationId: getCapabilities
      summary: Report supported optional endpoints
      description: |
        Called when the connection is created and when a user clicks **Test
        connection**. CloudCruise stores the result on the connection, so
        after enabling an optional endpoint, click **Test connection** again.
        A `200` proves that your endpoint is reachable and that signature
        verification works.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ConnectionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CapabilitiesRequest"
      responses:
        "200":
          description: Capabilities of this endpoint.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Capabilities"
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/InvalidSignature" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "5XX": { $ref: "#/components/responses/ServerError" }

  /v1/items/resolve:
    post:
      operationId: resolveItem
      summary: Return the secret values of one item
      description: |
        Return the fields the item has. Omit a field (or return `null`) when
        the item does not have it; an empty string is treated the same as
        absent. A response with none of the three fields is treated as an
        error ("credential resolved to empty"), handled like the errors in
        the table above.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ConnectionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ItemRequest"
      responses:
        "200":
          description: The item's secret values.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResolvedItem"
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/InvalidSignature" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/ItemNotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "5XX": { $ref: "#/components/responses/ServerError" }

  /v1/items/describe:
    post:
      operationId: describeItem
      summary: Return an item's display metadata
      description: |
        Metadata only, never secret values. Used to show which item a
        credential is bound to, and to check a `ref` when a user binds it.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ConnectionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ItemRequest"
      responses:
        "200":
          description: The item's metadata.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ItemSummary"
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/InvalidSignature" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/ItemNotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "5XX": { $ref: "#/components/responses/ServerError" }

  /v1/items/list:
    post:
      operationId: listItems
      summary: List items the connection may access
      description: |
        Required when `item_listing` is `true`. Metadata only, never secret
        values. Return items in a stable order so pages neither skip nor
        repeat items.

        CloudCruise follows `next_cursor` until it is absent or `null`, for at
        most 20 pages (10,000 items), and searches the result itself.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ConnectionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ListRequest"
      responses:
        "200":
          description: One page of items.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ItemPage"
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/InvalidSignature" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "5XX": { $ref: "#/components/responses/ServerError" }

components:
  securitySchemes:
    cloudcruiseSignature:
      type: apiKey
      in: header
      name: X-CloudCruise-Signature
      description: |
        `v1=<hex HMAC-SHA256>`, possibly several comma-separated values during
        a secret rotation. See **Request authentication** above.
    cloudcruiseTimestamp:
      type: apiKey
      in: header
      name: X-CloudCruise-Timestamp
      description: Unix time in seconds, part of the signed string. Reject if more than 300 seconds off.

  parameters:
    RequestId:
      name: X-CloudCruise-Request-Id
      in: header
      required: true
      description: Unique per request; never reused, including across retries.
      schema:
        type: string
        format: uuid
    ConnectionId:
      name: X-CloudCruise-Connection-Id
      in: header
      required: true
      description: The CloudCruise connection id. Not secret.
      schema:
        type: string
        format: uuid

  schemas:
    Ref:
      type: string
      minLength: 1
      maxLength: 512
      pattern: "^[\\x20-\\x7E]+$"
      description: Your identifier for one item. Opaque to CloudCruise.
      examples:
        - prod/acme-portal/login

    CapabilitiesRequest:
      type: object
      description: Currently empty. Send `{}`; ignore unknown properties.
      additionalProperties: true

    Capabilities:
      type: object
      required: [item_listing, password_write]
      properties:
        item_listing:
          type: boolean
          description: "`true` if `POST /v1/items/list` is implemented. Without it, users type the `ref` when binding a credential."
        password_write:
          type: boolean
          description: "Reserved: return `false` in this version."
        item_search:
          type: boolean
          default: false
          description: "Reserved: return `false` in this version."
      examples:
        - item_listing: true
          password_write: false
          item_search: false

    ItemRequest:
      type: object
      required: [ref]
      properties:
        ref:
          $ref: "#/components/schemas/Ref"
      examples:
        - ref: prod/acme-portal/login

    ResolvedItem:
      type: object
      properties:
        user_name:
          type: [string, "null"]
          maxLength: 1024
        password:
          type: [string, "null"]
          maxLength: 4096
        totp_seed:
          type: [string, "null"]
          maxLength: 2048
          description: Base32 TOTP secret or `otpauth://totp/...` URI. Not a one-time code.
      examples:
        - user_name: automation@acme.com
          password: correct-horse-battery-staple
          totp_seed: JBSWY3DPEHPK3PXP

    ItemSummary:
      type: object
      required: [ref, title]
      properties:
        ref:
          $ref: "#/components/schemas/Ref"
        title:
          type: string
          minLength: 1
          maxLength: 200
          description: Display name of the item.
        group_id:
          type: [string, "null"]
          maxLength: 512
          description: Optional folder, vault or collection the item belongs to.
        group_name:
          type: [string, "null"]
          maxLength: 200
          description: Display name of the group, shown next to the item in the picker.
      examples:
        - ref: prod/acme-portal/login
          title: Acme Portal (prod)
          group_id: automation
          group_name: Automation

    ListRequest:
      type: object
      properties:
        cursor:
          type: [string, "null"]
          maxLength: 1024
          description: "`next_cursor` from the previous page; absent or `null` for the first page."
        limit:
          type: integer
          minimum: 1
          maximum: 500
          default: 500
          description: Maximum items to return. You may return fewer.

    ItemPage:
      type: object
      required: [items]
      properties:
        items:
          type: array
          maxItems: 500
          items:
            $ref: "#/components/schemas/ItemSummary"
        next_cursor:
          type: [string, "null"]
          maxLength: 1024
          description: Absent or `null` on the last page.

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              maxLength: 100
              description: |
                Known values: `invalid_request`, `invalid_signature`,
                `forbidden`, `item_not_found`, `rate_limited`,
                `internal_error`.
                CloudCruise handles an
                unknown code according to the HTTP status.
              examples: [item_not_found]
            message:
              type: string
              maxLength: 1000
              description: Shown to CloudCruise users. Never include secret values.

  responses:
    InvalidRequest:
      description: The request is malformed.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            { error: { code: invalid_request, message: "ref is required" } }
    InvalidSignature:
      description: Signature, timestamp or signing-secret check failed.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            {
              error:
                {
                  code: invalid_signature,
                  message: "Signature verification failed"
                }
            }
    Forbidden:
      description: The signature is valid, but this connection may not access the item.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            {
              error:
                {
                  code: forbidden,
                  message: "Connection has read-only access to this item"
                }
            }
    ItemNotFound:
      description: No item with this ref.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            {
              error:
                {
                  code: item_not_found,
                  message: "No item prod/acme-portal/login"
                }
            }
    RateLimited:
      description: Too many requests. CloudCruise retries, honouring `Retry-After` up to 10 seconds.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema: { type: integer, minimum: 0 }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    ServerError:
      description: Your service failed. CloudCruise retries, then fails the run as "secret provider unavailable".
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
