openapi: 3.0.3
info:
  title: LightFile Proxy キャッシュクリアAPI
  version: 1.0.0
  description: |
    外部のシステムから LightFile Proxy のキャッシュクリアを依頼し、その結果を確認する API。
    管理画面の「キャッシュ操作」と同じ指定ができ、CloudFront のキャッシュ削除も連携して実行できる。

    基本の流れ: `POST` でジョブを作り、返ってきた `id` を保存し、その `id` で `GET` して
    `status` が `completed` か `failed` になるまで 15〜30 秒おきに確認する。

    APIキーは管理画面の「設定 › APIキー」で発行する（表示は発行時の1回だけ）。
    ブラウザからの呼び出し（CORS）には対応していない。サーバーから呼び出すこと。

    利用者向けの手引き: https://manage.lightfile-proxy.net/docs/cache-clear-api
servers:
  - url: https://manage.lightfile-proxy.net/api/v1
security:
  - apiKey: []

paths:
  /clusters/{clusterId}/cache-clear-jobs:
    parameters:
      - $ref: '#/components/parameters/clusterId'
    post:
      operationId: createCacheClearJob
      summary: キャッシュクリアを依頼する
      description: |
        LightFile Proxy のキャッシュクリアのジョブを作る。`cloudFront` を指定すると、完了後に CloudFront の
        キャッシュ削除も依頼する。必要な権限は `cache-clear:create`。

        - `paths` は前方一致。末尾の `*` は取り除く
        - CloudFront のパスは各パスの末尾に `*` を付けた前方一致になる（ファイルを指定しても `/img/a.jpg*`）
        - CloudFront と連携するとき、`paths` は15件まで（CloudFront はワイルドカードの削除を
          1ディストリビューションあたり同時に15件までしか受け付けない）
        - 同じクラスタで終わっていないキャッシュクリアが5件あると 429
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: |
            同じ値での再送は新しいジョブを作らず、最初のジョブを 200 で返す。内容が違えば 409。
            APIキーごとに別の空間。UUID を推奨。1〜255 文字の表示可能な ASCII。
          schema:
            type: string
            maxLength: 255
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCacheClearJobRequest'
            example:
              origin: https://origin.example.com/
              paths: ['/images/campaign/', '/css/']
              cloudFront:
                distributionId: E2ABCDEF123456
      responses:
        '202':
          description: ジョブを作った
          headers:
            Location:
              description: 作ったジョブのURL
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CacheClearJob'
        '200':
          description: 同じ Idempotency-Key での再送。最初に作ったジョブを返す
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CacheClearJob'
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '409': { $ref: '#/components/responses/Error' }
        '422': { $ref: '#/components/responses/Error' }
        '429':
          description: 終わっていないキャッシュクリアが多い
          headers:
            Retry-After:
              description: 再送までに待つ秒数
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500': { $ref: '#/components/responses/Error' }
    get:
      operationId: listCacheClearJobs
      summary: キャッシュクリアの一覧
      description: |
        新しい順に返す。管理画面や簡易サイト運用画面から依頼したものも含む。
        一覧では CloudFront に問い合わせ直さない。必要な権限は `cache-clear:read`。
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: cursor
          in: query
          description: 前の応答の nextCursor をそのまま指定する
          schema:
            type: string
        - name: requestedBy
          in: query
          description: self を指定すると、このAPIキーで依頼したものだけに絞る
          schema:
            type: string
            enum: [self]
      responses:
        '200':
          description: 一覧
          content:
            application/json:
              schema:
                type: object
                required: [jobs, nextCursor]
                properties:
                  jobs:
                    type: array
                    items:
                      $ref: '#/components/schemas/CacheClearJob'
                  nextCursor:
                    type: string
                    nullable: true
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }

  /clusters/{clusterId}/cache-clear-jobs/{jobId}:
    parameters:
      - $ref: '#/components/parameters/clusterId'
      - name: jobId
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getCacheClearJob
      summary: キャッシュクリアの状態
      description: |
        CloudFront が処理中なら、最後の確認から30秒以上たっていればその場で確認し直してから返す。
        必要な権限は `cache-clear:read`。
      responses:
        '200':
          description: ジョブ
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CacheClearJob'
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }

  /clusters/{clusterId}/origins:
    parameters:
      - $ref: '#/components/parameters/clusterId'
    get:
      operationId: listOrigins
      summary: origin に指定できるオリジン
      responses:
        '200':
          description: オリジンの一覧
          content:
            application/json:
              schema:
                type: object
                required: [origins]
                properties:
                  origins:
                    type: array
                    items:
                      type: object
                      required: [id, name, url]
                      properties:
                        id: { type: string }
                        name: { type: string }
                        url: { type: string }
        '401': { $ref: '#/components/responses/Error' }

  /clusters/{clusterId}/cloudfront-distributions:
    parameters:
      - $ref: '#/components/parameters/clusterId'
    get:
      operationId: listCloudFrontDistributions
      summary: cloudFront.distributionId に指定できるディストリビューション
      description: 管理画面が CloudFront から読み込んだ一覧を返す。
      responses:
        '200':
          description: ディストリビューションの一覧
          content:
            application/json:
              schema:
                type: object
                required: [distributions]
                properties:
                  distributions:
                    type: array
                    items:
                      type: object
                      required: [distributionId, name]
                      properties:
                        distributionId: { type: string }
                        name: { type: string }
        '401': { $ref: '#/components/responses/Error' }

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: '管理画面で発行する APIキー（lfpk_ で始まる）。Authorization: Bearer lfpk_...'

  parameters:
    clusterId:
      name: clusterId
      in: path
      required: true
      description: 管理画面に表示されるクラスタID
      schema:
        type: string

  responses:
    Error:
      description: エラー
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

  schemas:
    CreateCacheClearJobRequest:
      type: object
      required: [paths]
      properties:
        origin:
          type: string
          description: |
            オリジンのURL（管理画面に表示されるもの）かオリジンのID。指定すると paths をオリジンの
            ルートからのパスとして扱い、URLを渡してもホスト部分は無視する。省くと paths にオリジンの完全なURLが要る
        paths:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: string
            maxLength: 2048
          description: キャッシュクリアする条件（前方一致）
        cloudFront:
          type: object
          nullable: true
          required: [distributionId]
          properties:
            distributionId:
              type: string
        maxEntries:
          type: integer
          minimum: 1
          nullable: true
          description: 該当するキャッシュがこの件数を超えたら、何も消さずに失敗させる。省くと上限なし

    CacheClearJob:
      type: object
      required: [id, status, createdAt, updatedAt, requestedBy, target, proxy, cloudFront]
      properties:
        id:
          type: string
        status:
          type: string
          enum: [queued, clearing, invalidating, completed, failed]
          description: |
            全体の状態。completed か failed になったら確認をやめる。
            cloudFront.status が untracked のときも completed になるが、CloudFront 側の完了は保証しない
        createdAt: { type: string, format: date-time, nullable: true }
        updatedAt: { type: string, format: date-time, nullable: true }
        requestedBy:
          type: object
          required: [type]
          properties:
            type:
              type: string
              enum: [apiKey, console, siteOperation]
            apiKeyId: { type: string }
            name: { type: string }
        target:
          type: object
          required: [urlPrefixes, maxEntries]
          properties:
            urlPrefixes:
              type: array
              items: { type: string }
            maxEntries:
              type: integer
              nullable: true
        proxy:
          type: object
          required: [status, progress, message, startedAt, result]
          properties:
            status:
              type: string
              enum: [wait, working, done, fail]
            progress:
              type: number
            message:
              type: string
            startedAt: { type: string, format: date-time, nullable: true }
            result:
              type: object
              nullable: true
              required: [files, fileSize, byCacheType]
              properties:
                files: { type: integer }
                fileSize: { type: integer }
                byCacheType:
                  type: array
                  items:
                    type: object
                    properties:
                      cacheType: { type: string }
                      files: { type: integer }
                      fileSize: { type: integer }
                examples:
                  type: array
                  description: 削除したキャッシュの例（個別の取得でだけ返す）
                  items:
                    type: object
                    properties:
                      cacheType: { type: string }
                      url: { type: string }
                      fileSize: { type: integer }
        cloudFront:
          type: object
          nullable: true
          description: CloudFront と連携しないジョブでは null
          required: [status, distributionId, distributionName, paths, invalidationId, requestedAt, completedAt, message]
          properties:
            status:
              type: string
              enum: [pending, requesting, inProgress, completed, untracked, failed, skipped]
              description: |
                untracked は「依頼は受理されたが完了を確認できない」。登録したIAMロールに
                cloudfront:GetInvalidation が無いときなどに付く
            distributionId: { type: string }
            distributionName: { type: string }
            paths:
              type: array
              items: { type: string }
            invalidationId: { type: string, nullable: true }
            requestedAt: { type: string, format: date-time, nullable: true }
            completedAt: { type: string, format: date-time, nullable: true }
            message: { type: string, nullable: true }

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - invalid_request
                - unauthorized
                - api_key_expired
                - api_key_revoked
                - insufficient_scope
                - not_found
                - job_not_found
                - method_not_allowed
                - idempotency_conflict
                - origin_not_found
                - paths_outside_origin
                - distribution_not_found
                - too_many_running_jobs
                - internal
            message:
              type: string
            details:
              type: array
              items: { type: string }
