openapi: 3.1.0
info:
  title: Alpha Compose API
  version: 1.0.0
  description: |
    Create transparent ProRes 4444 telemetry overlays or bounded composites for
    short videos. The Compose UI can export a Project JSON workflow envelope,
    but this API accepts only the nested `RenderRequest`. Successful H.264 jobs
    can later open owner-bound completed-output playback sessions. Browser
    sessions require `X-CSRF-Token` on writes; personal bearer tokens do not.
    Google Drive delivery uses only the `drive.file` scope. The Compose UI
    uploads local source videos resumably from the browser directly to Google
    Drive, never through this API. The API validates the resulting Drive object
    before playback and applies processing admission only to selected source
    work, not the whole uploaded file.
  contact:
    email: me@jamiep.org
  license:
    name: All rights reserved
    url: https://alphacompose.com/terms
servers:
  - url: https://alphacompose.com
    description: Production
security:
  - bearerAuth: []
  - sessionCookie: []
tags:
  - name: Rendering
    description: Preview creation and one-second synchronous overlay diagnostics.
  - name: Derivative preparation
    description: Owner-bound production private-video-preview preparation.
  - name: Jobs
    description: Durable render submission and lifecycle.
  - name: Tokens
    description: Personal API token management.
  - name: Administration
    description: Owner- and admin-managed membership management.
  - name: Browser authorization
    description: Atomic Google identity and restricted Drive authorization, plus Drive Picker browser access.
paths:
  /health:
    get:
      operationId: getHealth
      summary: Check service health.
      security: []
      responses:
        "200":
          description: Service is ready.
          content:
            application/json:
              schema:
                type: object
                required: [status]
                properties:
                  status:
                    const: ok
  /v1/homepage/videos:
    get:
      operationId: getHomepageVideos
      summary: Read bounded public homepage playlist metadata.
      description: >-
        Returns the owner-ordered public Alpha Compose YouTube playlist from a
        15-minute in-memory cache. A warm cache may be returned with `stale:
        true` after a temporary upstream failure. The server-only API key and
        raw upstream errors are never returned.
      security: []
      parameters:
        - in: header
          name: If-None-Match
          required: false
          schema: {type: string}
      responses:
        "200":
          description: Bounded public video metadata.
          headers:
            Cache-Control: {schema: {type: string}}
            ETag: {schema: {type: string}}
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [videos, stale]
                properties:
                  stale: {type: boolean}
                  videos:
                    type: array
                    maxItems: 200
                    items:
                      type: object
                      additionalProperties: false
                      required: [id, title, summary, duration, durationIso, position, thumbnailUrl, watchUrl]
                      properties:
                        id: {type: string, pattern: '^[A-Za-z0-9_-]{6,64}$'}
                        title: {type: string, maxLength: 200}
                        summary: {type: string, maxLength: 160}
                        duration: {type: string, maxLength: 10}
                        durationIso: {type: string, maxLength: 64}
                        position: {type: integer, minimum: 0, maximum: 1000000}
                        thumbnailUrl: {type: string, format: uri}
                        watchUrl: {type: string, format: uri}
        "304":
          description: Cached representation is unchanged.
        "503":
          description: Metadata is temporarily unavailable and this instance has no successful cached result.
  /v1/product-updates/signup:
    post:
      operationId: signupForProductUpdates
      summary: Send an anonymous product-updates signup.
      description: Validates a short-lived signed homepage nonce and the submitted email, then sends one idempotent plain-text notification through Resend without retaining the email in Alpha Compose storage, logs, or analytics.
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              additionalProperties: false
              required: [proof, email]
              properties:
                proof: {type: string, description: Short-lived signed homepage nonce.}
                email: {type: string, format: email, maxLength: 254}
      responses:
        "200":
          description: Notification accepted and homepage success feedback rendered.
          content:
            text/html:
              schema: {type: string}
        "400":
          description: Nonce or email validation failed and homepage retry feedback rendered with a fresh nonce.
          content:
            text/html:
              schema: {type: string}
        "502":
          description: Notification delivery failed safely and homepage retry feedback rendered.
          content:
            text/html:
              schema: {type: string}
        "503":
          description: Notification provider is temporarily unavailable and homepage retry feedback rendered.
          content:
            text/html:
              schema: {type: string}
  /v1/auth/login/start:
    get:
      operationId: startGoogleLogin
      tags: [Browser authorization]
      summary: Start combined Google identity and Drive authorization.
      description: Starts a PKCE-protected offline flow for exactly `openid email profile drive.file` and stores signed OAuth state in the Firebase-compatible `__session` HttpOnly cookie. Routine login does not force consent. A signed recovery marker created after an unusable grant enables consent recovery.
      security: []
      parameters:
        - name: recovery
          in: query
          required: false
          description: Requests recovery consent only when accompanied by the server-issued signed recovery cookie.
          schema: {type: boolean, default: false}
      responses:
        "302":
          description: Redirect to Google OAuth authorization.
          headers:
            Location:
              description: Google OAuth authorization URL requesting exactly `openid email profile drive.file`, PKCE, and offline access.
              schema: {type: string, format: uri}
            Set-Cookie:
              description: Signed Firebase-compatible `__session` cookie containing short-lived OAuth state.
              schema: {type: string}
  /v1/auth/login/callback:
    get:
      operationId: finishGoogleLogin
      tags: [Browser authorization]
      summary: Finish combined Google identity and Drive authorization.
      description: Validates signed state and PKCE, verifies the Google identity and exact scopes, transactionally creates and subject-binds an active member on first sign-in, rejects suspended memberships and mismatched subjects, obtains or validates an encrypted refresh token, reuses or creates the output folder, persists the grant, and issues the browser session last.
      security: []
      parameters:
        - $ref: "#/components/parameters/OAuthCode"
        - $ref: "#/components/parameters/OAuthState"
      responses:
        "302":
          description: Redirect to the signed-in homepage.
          headers:
            Location:
              schema: {type: string, const: /}
            Set-Cookie:
              description: Signed Firebase-compatible `__session` cookie.
              schema: {type: string}
        "400":
          description: OAuth state, code, or Drive scope validation failed.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Error"}
        "401": {$ref: "#/components/responses/AuthenticationRequired"}
        "403": {$ref: "#/components/responses/AccountSuspended"}
        "502":
          description: Google OAuth or Drive is temporarily unavailable.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Error"}
        "503":
          description: KMS or grant persistence is temporarily unavailable.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Error"}
  /v1/auth/logout:
    post:
      operationId: logoutBrowserSession
      tags: [Browser authorization]
      summary: End the current browser session.
      description: Verifies the signed session-bound CSRF token, expires the Firebase-compatible `__session` cookie, and redirects to the signed-out homepage.
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              additionalProperties: false
              required: [csrf]
              properties:
                csrf: {type: string, description: Signed CSRF token bound to the current session subject.}
      responses:
        "302":
          description: Browser session ended and redirected to the signed-out homepage.
          headers:
            Location:
              schema: {type: string, const: /}
            Set-Cookie:
              description: Expired Firebase-compatible `__session` and legacy `agg_session` cookies.
              schema: {type: string}
        "401": {$ref: "#/components/responses/AuthenticationRequired"}
        "403": {$ref: "#/components/responses/CsrfRequired"}
  /v1/drive/picker:
    get:
      operationId: showGoogleDrivePicker
      tags: [Browser authorization]
      summary: Open Google Drive Picker for the signed-in user.
      description: |
        Refreshes the browser session's restricted `drive.file` grant and
        returns a no-store HTML Picker page. The normal list view includes My
        Drive and files shared with the user; a separate list view enables
        Shared Drives. Results contain only supported video MIME types, while
        non-selectable folders remain visible for navigation. Google Picker's
        opaque Upload tab is not used. The Compose page instead uploads local
        videos resumably and directly from the browser to Google Drive, then
        validates the created Drive object through the normal selected-source
        flow. Source bytes never pass through Alpha Compose, and no Alpha
        Compose whole-file upload size limit is applied. Selecting or creating
        a file grants per-file access and does not broaden the OAuth scope.
      security:
        - sessionCookie: []
      responses:
        "200":
          description: Google Drive Picker HTML.
          content:
            text/html:
              schema: {type: string}
        "401": {$ref: "#/components/responses/AuthenticationRequired"}
        "503":
          description: Picker API key or application ID is not configured.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Error"}
  /v1/drive/picker/diagnostic:
    post:
      operationId: reportGoogleDrivePickerDiagnostic
      tags: [Browser authorization]
      summary: Report a privacy-safe Google Drive selection or upload state.
      description: Records bounded Picker and direct-upload status categories without tokens, account values, filenames, file IDs, or resumable session URLs.
      security:
        - sessionCookie: []
      parameters:
        - $ref: "#/components/parameters/CsrfToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phase, view, listState]
              properties:
                phase: {type: string, enum: [opened, loaded, empty, selected, cancelled, error]}
                view: {type: string, enum: [drive, upload, unknown]}
                listState:
                  type: string
                  enum: [unknown, empty, selected, unsupported, storage, authorization, rate, transient, network, policy, cancelled]
      responses:
        "200":
          description: Diagnostic accepted.
        "400":
          description: Diagnostic payload is invalid.
        "401": {$ref: "#/components/responses/AuthenticationRequired"}
        "403": {$ref: "#/components/responses/CsrfRequired"}
  /v1/drive/recording-clock-inspections:
    post:
      operationId: inspectDriveRecordingClock
      tags: [Browser authorization]
      summary: Inspect bounded source ranges for advisory recording-clock candidates.
      description: |
        Revalidates the selected source and reads at most two 256 KiB ranges,
        each with a three-second Drive request timeout. Source bytes are not
        persisted. Drive createdTime is neither requested nor used. Returned
        movie, track, or container candidates are advisory; the browser still
        requires an editable start and a confirmed IANA video timezone.
      security:
        - sessionCookie: []
      parameters:
        - $ref: "#/components/parameters/CsrfToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [fileId]
              properties:
                fileId: {type: string, minLength: 1, maxLength: 256}
      responses:
        "200":
          description: Bounded inspection completed or fell back to manual entry.
          content:
            application/json:
              schema:
                type: object
                required: [fileName, status, candidates, ambiguous, limits]
                properties:
                  fileName: {type: string}
                  status: {type: string, enum: [candidate, manual]}
                  candidates:
                    type: array
                    items:
                      type: object
                      required: [source, kind, value]
                      properties:
                        source: {type: string, enum: [movie, track, container]}
                        kind: {type: string, enum: [explicit-offset, local-date-time]}
                        value: {type: string}
                  recommendedIndex: {type: [integer, "null"], minimum: 0}
                  ambiguous: {type: boolean}
                  durationSeconds: {type: [number, "null"], minimum: 0}
                  limits:
                    type: object
                    required: [maxBytes, maxRanges, timeoutMillis]
                    properties:
                      maxBytes: {type: integer, const: 524288}
                      maxRanges: {type: integer, const: 2}
                      timeoutMillis: {type: integer, const: 3000}
        "400":
          description: The selected source payload or authoritative metadata is invalid.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Error"}
        "401": {$ref: "#/components/responses/AuthenticationRequired"}
        "403": {$ref: "#/components/responses/CsrfRequired"}
  /v1/drive/playback-analyses:
    post:
      operationId: inspectDrivePlayback
      tags: [Browser authorization]
      summary: Inspect one selected Drive source for browser playback evidence.
      description: |
        Revalidates authoritative Drive metadata, probes the original selected
        source through a bounded read-only proxy, and returns normalized
        container, video, and optional audio evidence for browser capability
        checks. No playback session or cookie is created.
      security:
        - sessionCookie: []
      parameters:
        - $ref: "#/components/parameters/CsrfToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [fileId]
              properties:
                fileId: {type: string, minLength: 1, maxLength: 256}
      responses:
        "200":
          description: Normalized browser playback evidence for the selected source.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [fileName, evidence]
                properties:
                  fileName: {type: string}
                  evidence:
                    type: object
                    additionalProperties: false
                    required: [container, video]
                    properties:
                      container:
                        type: object
                        additionalProperties: false
                        required: [format]
                        properties:
                          format: {type: string, enum: [mp4, mov, unknown]}
                          majorBrand: {type: string}
                      video:
                        type: object
                        additionalProperties: false
                        required: [codec, codecTag]
                        properties:
                          codec: {type: string}
                          codecTag: {type: string}
                          profile: {type: string}
                          pixelFormat: {type: string}
                      audio:
                        type: object
                        additionalProperties: false
                        required: [codec]
                        properties:
                          codec: {type: string}
        "400":
          description: Selected Drive metadata is invalid.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Error"}
        "401": {$ref: "#/components/responses/AuthenticationRequired"}
        "403": {$ref: "#/components/responses/CsrfRequired"}
        "503":
          description: Restricted Drive access or source metadata is temporarily unavailable.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Error"}
  /v1/drive/playback-sessions:
    post:
      operationId: createDrivePlaybackSession
      tags: [Browser authorization]
      summary: Prepare one selected Drive source for private browser playback.
      description: |
        Revalidates authoritative Drive metadata, preserves the original source
        MIME type, and returns an opaque same-origin URL. The Drive file ID and
        access token never enter that URL. One-hour, owner- and URL-bound
        playback authority is signed into the Firebase-compatible `__session`
        HttpOnly cookie while preserving the authenticated browser session.
      security:
        - sessionCookie: []
      parameters:
        - $ref: "#/components/parameters/CsrfToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [fileId]
              properties:
                fileId: {type: string, minLength: 1, maxLength: 256}
      responses:
        "201":
          description: Browser playback session created.
          headers:
            Set-Cookie:
              description: Signed Firebase-compatible `__session` cookie containing the existing session and short-lived playback authority.
              schema: {type: string}
            Cache-Control:
              schema: {const: no-store}
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [playbackUrl, contentType, size]
                properties:
                  playbackUrl:
                    type: string
                    pattern: "^/v1/drive/playback/[0-9a-f-]{36}$"
                  contentType: {type: string}
                  size: {type: integer, minimum: 1}
        "400":
          description: Selected Drive metadata is invalid.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Error"}
        "401": {$ref: "#/components/responses/AuthenticationRequired"}
        "403": {$ref: "#/components/responses/CsrfRequired"}
        "503":
          description: Restricted Drive access or source metadata is temporarily unavailable.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Error"}
  /v1/drive/playback/{playbackId}:
    get:
      operationId: streamDrivePlayback
      tags: [Browser authorization]
      summary: Stream an owned selected Drive source with browser Range semantics.
      description: |
        Rechecks the active member session and signed owner/path binding on
        every request. One single `Range` is accepted. Open-ended and suffix
        ranges are capped at 8 MiB, proxied directly from Drive, and never
        persisted. Drive must return truthful 206 `Content-Range` and
        `Content-Length` headers. The response is always private and no-store.
      security:
        - sessionCookie: []
      parameters:
        - name: playbackId
          in: path
          required: true
          schema: {type: string, format: uuid}
        - name: Range
          in: header
          required: false
          description: One byte range, including bounded, open-ended, or suffix form.
          schema:
            type: string
            examples: ["bytes=0-1048575", "bytes=1048576-", "bytes=-1048576"]
      responses:
        "200":
          description: Complete source returned when Range is omitted.
          headers:
            Accept-Ranges:
              schema: {const: bytes}
            Cache-Control:
              schema: {const: no-store}
          content:
            video/mp4:
              schema: {type: string, contentEncoding: binary}
            video/quicktime:
              schema: {type: string, contentEncoding: binary}
        "206":
          description: Requested single byte range.
          headers:
            Accept-Ranges:
              schema: {const: bytes}
            Content-Range:
              schema: {type: string, example: "bytes 0-1048575/7340032"}
            Cache-Control:
              schema: {const: no-store}
          content:
            video/mp4:
              schema: {type: string, contentEncoding: binary}
            video/quicktime:
              schema: {type: string, contentEncoding: binary}
        "401":
          description: Authentication or owner-bound playback authority is invalid or expired.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Error"}
        "416":
          description: Range is malformed, multiple, empty, or outside the selected source.
          headers:
            Accept-Ranges:
              schema: {const: bytes}
            Content-Range:
              schema: {type: string, example: "bytes */7340032"}
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Error"
                  - type: object
                    properties:
                      error: {const: playback_range_not_satisfiable}
        "502":
          description: Drive returned an invalid or untruthful ranged response.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Error"}
  /v1/preview:
    post:
      operationId: createPreview
      tags: [Rendering]
      summary: Start an asynchronous key-moment gallery preview.
      description: |
        Starts a bounded asynchronous operation that selects the first and final
        rendered video frames, trace boundaries, configured timer boundaries,
        and prominent extrema from every rendered trace. Candidates map to real
        output frames before coincident labels and assets are deduplicated.
        Selected-source previews batch-decode each unique output frame once and
        return only the exact composited Final image reference. Fitted unoverlaid
        Source images are not generated or retained. When a
        selected video ends before every requested moment, a successful partial
        gallery returns each available Final image plus bounded warning
        counts and recovery guidance. Zero available moments fail with the same
        structured source-duration diagnosis. This is
        a breaking replacement for the former synchronous `image/png` midpoint response.
        Clients must poll `statusUrl` and consume the structured result after
        `state` becomes `succeeded`.
      parameters:
        - $ref: "#/components/parameters/OptionalIdempotencyKey"
        - $ref: "#/components/parameters/CsrfToken"
      requestBody:
        $ref: "#/components/requestBodies/RenderRequest"
      responses:
        "202":
          description: Preview operation accepted.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/PreviewOperation"}
        "400": {$ref: "#/components/responses/InvalidRequest"}
        "401": {$ref: "#/components/responses/AuthenticationRequired"}
        "403": {$ref: "#/components/responses/CsrfRequired"}
        "413": {$ref: "#/components/responses/PayloadTooLarge"}
  /v1/previews/{previewId}:
    get:
      operationId: getPreview
      tags: [Rendering]
      summary: Get an owned preview operation and its completed manifest.
      description: Preview operations and assets expire 24 hours after creation. A selected-source partial result remains succeeded, exposes every available Final image, and reports bounded warning counts. Zero available moments are a failed operation with an actionable source-duration reason.
      parameters:
        - name: previewId
          in: path
          required: true
          schema: {type: string, format: uuid}
      responses:
        "200":
          description: Current operation state, progress, safe failure, or completed gallery.
          headers:
            Cache-Control:
              schema: {const: no-store}
          content:
            application/json:
              schema: {$ref: "#/components/schemas/PreviewOperation"}
        "401": {$ref: "#/components/responses/AuthenticationRequired"}
        "404": {$ref: "#/components/responses/NotFound"}
  /v1/previews/{previewId}/images/{assetId}/{size}:
    get:
      operationId: getPreviewImage
      tags: [Rendering]
      summary: Get an owned preview thumbnail or full-size PNG.
      description: |
        Returns one bounded private image. URLs contain only opaque operation and
        asset IDs. Responses are never shared or cached and expire with the
        operation after 24 hours.
      parameters:
        - name: previewId
          in: path
          required: true
          schema: {type: string, format: uuid}
        - name: assetId
          in: path
          required: true
          schema: {type: string, pattern: "^[A-Za-z0-9_-]{1,64}$"}
        - name: size
          in: path
          required: true
          schema: {type: string, enum: [thumbnail, full]}
      responses:
        "200":
          description: PNG thumbnail up to 4 MiB or full image up to 32 MiB.
          headers:
            Cache-Control:
              schema: {const: no-store}
            X-Content-Type-Options:
              schema: {const: nosniff}
          content:
            image/png:
              schema: {type: string, contentEncoding: binary}
        "401": {$ref: "#/components/responses/AuthenticationRequired"}
        "404": {$ref: "#/components/responses/NotFound"}
  /v1/overlay:
    post:
      operationId: createOverlay
      tags: [Rendering]
      summary: Render an overlay synchronously.
      description: |
        Renders only one-second overlay diagnostics. Longer valid requests are
        rejected before encoder startup and must use POST /v1/jobs. The route
        remains same-origin through Firebase Hosting and does not support CORS
        or the direct Cloud Run origin as a public API contract.
      parameters:
        - $ref: "#/components/parameters/CsrfToken"
      requestBody:
        $ref: "#/components/requestBodies/RenderRequest"
      responses:
        "200":
          description: Seekable ProRes 4444 MOV with transparent video and heartbeat audio.
          headers:
            Cache-Control:
              schema: {const: no-store}
          content:
            video/quicktime:
              schema: {type: string, contentEncoding: binary}
        "400": {$ref: "#/components/responses/InvalidRequest"}
        "401": {$ref: "#/components/responses/AuthenticationRequired"}
        "413": {$ref: "#/components/responses/PayloadTooLarge"}
        "422": {$ref: "#/components/responses/SynchronousOverlayDurationExceeded"}
        "504":
          description: >-
            Firebase Hosting did not receive an accepted one-second diagnostic
            response within 60 seconds.
  /v1/jobs:
    post:
      operationId: submitJob
      tags: [Jobs]
      summary: Submit an idempotent durable render.
      description: Preview is optional. Submission validates the request, applies admission, capacity, budget, confirmation, and idempotency safeguards, then preflights the requester's restricted Drive grant because every durable output is delivered to Drive. Personal tokens without a usable grant receive `drive_grant_required` before job creation.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/CsrfToken"
      requestBody:
        $ref: "#/components/requestBodies/RenderRequest"
      responses:
        "200":
          description: Existing job returned for an idempotent replay.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Job"}
        "202":
          description: Job accepted.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Job"}
        "400": {$ref: "#/components/responses/InvalidRequest"}
        "401": {$ref: "#/components/responses/AuthenticationRequired"}
        "409":
          description: The idempotency key conflicts with a different request.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Error"}
        "429": {$ref: "#/components/responses/AdmissionRejected"}
        "503": {$ref: "#/components/responses/CapacityUnavailable"}
  /v1/derivative-preparations:
    post:
      operationId: submitDerivativePreparation
      tags: [Derivative preparation]
      summary: Prepare an owned Drive video for private browser preview.
      description: Browser-supported sources remain on direct private Drive range passthrough and must not use this operation. Browser-incompatible but renderable sources require this explicit preparation request.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/CsrfToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [fileId]
              properties:
                fileId: {type: string, minLength: 1, maxLength: 256}
      responses:
        "200":
          description: Exact idempotent replay or eligible private cache hit.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema: {$ref: "#/components/schemas/DerivativePreparation"}
        "202":
          description: Preparation attempt admitted and queued.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema: {$ref: "#/components/schemas/DerivativePreparation"}
        "400": {$ref: "#/components/responses/InvalidRequest"}
        "401": {$ref: "#/components/responses/AuthenticationRequired"}
        "403": {$ref: "#/components/responses/CsrfRequired"}
        "409": {$ref: "#/components/responses/Conflict"}
        "429": {$ref: "#/components/responses/AdmissionRejected"}
        "503": {$ref: "#/components/responses/CapacityUnavailable"}
  /v1/derivative-preparations/{preparationId}:
    parameters:
      - $ref: "#/components/parameters/DerivativePreparationId"
    get:
      operationId: getDerivativePreparation
      tags: [Derivative preparation]
      summary: Get an owned private-preview preparation.
      responses:
        "200":
          description: Owned preparation state.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema: {$ref: "#/components/schemas/DerivativePreparation"}
        "401": {$ref: "#/components/responses/AuthenticationRequired"}
        "404": {$ref: "#/components/responses/NotFound"}
  /v1/derivative-preparations/{preparationId}/cancel:
    parameters:
      - $ref: "#/components/parameters/DerivativePreparationId"
    post:
      operationId: cancelDerivativePreparation
      tags: [Derivative preparation]
      summary: Cancel an owned queued or running preparation.
      parameters:
        - $ref: "#/components/parameters/CsrfToken"
      responses:
        "200":
          description: Cancellation was durably recorded.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema: {$ref: "#/components/schemas/DerivativePreparation"}
        "401": {$ref: "#/components/responses/AuthenticationRequired"}
        "403": {$ref: "#/components/responses/CsrfRequired"}
        "404": {$ref: "#/components/responses/NotFound"}
        "409": {$ref: "#/components/responses/Conflict"}
  /v1/derivative-preparations/{preparationId}/retry:
    parameters:
      - $ref: "#/components/parameters/DerivativePreparationId"
    post:
      operationId: retryDerivativePreparation
      tags: [Derivative preparation]
      summary: Retry an owned failed or cancelled preparation.
      parameters:
        - $ref: "#/components/parameters/CsrfToken"
      responses:
        "202":
          description: Next exact attempt admitted and queued.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema: {$ref: "#/components/schemas/DerivativePreparation"}
        "401": {$ref: "#/components/responses/AuthenticationRequired"}
        "403": {$ref: "#/components/responses/CsrfRequired"}
        "404": {$ref: "#/components/responses/NotFound"}
        "409": {$ref: "#/components/responses/Conflict"}
        "429": {$ref: "#/components/responses/AdmissionRejected"}
  /v1/derivative-preparations/{preparationId}/playback-sessions:
    parameters:
      - $ref: "#/components/parameters/DerivativePreparationId"
    post:
      operationId: createDerivativePlaybackSession
      tags: [Derivative preparation]
      summary: Mint private playback authority for one completed preparation.
      description: |
        Revalidates the active session, membership generation, owner,
        production environment, succeeded state, immutable asset generation,
        fixed media profile, and asset expiry. The response contains only an
        opaque same-origin URL. Object keys, source identity, account identity,
        storage URLs, and signed authority remain outside URLs and
        JavaScript-visible state. Authority is signed into the HttpOnly
        `__session` cookie and expires after at most one hour.
      parameters:
        - $ref: "#/components/parameters/CsrfToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
      responses:
        "201":
          description: Owner-bound derivative playback session created.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
            Set-Cookie:
              description: Signed Firebase-compatible `__session` cookie containing the existing session and short-lived playback authority.
              schema: {type: string}
            Cache-Control:
              schema: {const: no-store}
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [playbackUrl, contentType, size]
                properties:
                  playbackUrl:
                    type: string
                    pattern: "^/v1/derivative-preparations/[0-9a-f-]{36}/playback/[0-9a-f-]{36}$"
                  contentType: {const: video/mp4}
                  size: {type: integer, minimum: 1}
        "400": {$ref: "#/components/responses/InvalidRequest"}
        "401": {$ref: "#/components/responses/AuthenticationRequired"}
        "403": {$ref: "#/components/responses/CsrfRequired"}
        "404":
          description: The preparation or immutable private asset is unavailable.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Error"
                  - type: object
                    properties:
                      error: {const: derivative_playback_unavailable}
  /v1/derivative-preparations/{preparationId}/playback/{playbackId}:
    parameters:
      - $ref: "#/components/parameters/DerivativePreparationId"
    get:
      operationId: streamDerivativePlayback
      tags: [Derivative preparation]
      summary: Stream one completed private-preview asset with bounded ranges.
      description: |
        Every request revalidates the active member session, owner,
        preparation, production environment, succeeded state, immutable asset
        identity and generation, fixed profile, path binding, purpose, and
        expiry. Storage remains private. A missing Range requests the initial
        bounded segment; bounded, open-ended, and suffix forms are capped at
        8 MiB. Every successful response is a truthful 206 and is no-store.
      parameters:
        - name: playbackId
          in: path
          required: true
          schema: {type: string, format: uuid}
        - name: Range
          in: header
          required: false
          description: One byte range, including bounded, open-ended, or suffix form.
          schema:
            type: string
            examples: ["bytes=0-1048575", "bytes=1048576-", "bytes=-1048576"]
      responses:
        "206":
          description: Initial or requested bounded byte range.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
            Accept-Ranges:
              schema: {const: bytes}
            Content-Range:
              schema: {type: string, example: "bytes 0-1048575/7340032"}
            Content-Length:
              schema: {type: integer, minimum: 1, maximum: 8388608}
            Cache-Control:
              schema: {const: no-store}
            X-Content-Type-Options:
              schema: {const: nosniff}
          content:
            video/mp4:
              schema: {type: string, contentEncoding: binary}
        "401": {$ref: "#/components/responses/AuthenticationRequired"}
        "404":
          description: The preparation or immutable private asset is unavailable.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Error"
                  - type: object
                    properties:
                      error: {const: derivative_playback_unavailable}
        "416":
          description: Range is malformed, multiple, empty, or outside the asset.
          headers:
            Accept-Ranges:
              schema: {const: bytes}
            Content-Range:
              schema: {type: string, example: "bytes */7340032"}
            Cache-Control:
              schema: {const: no-store}
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Error"
                  - type: object
                    properties:
                      error: {const: playback_range_not_satisfiable}
        "502":
          description: Private storage returned a missing or untruthful bounded range.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Error"
                  - type: object
                    properties:
                      error: {const: derivative_playback_unavailable}
  /v1/jobs/{jobId}:
    parameters:
      - $ref: "#/components/parameters/JobId"
    get:
      operationId: getJob
      tags: [Jobs]
      summary: Get an owned render job.
      responses:
        "200":
          description: Owned job state.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Job"}
        "404": {$ref: "#/components/responses/NotFound"}
  /v1/jobs/{jobId}/cancel:
    parameters:
      - $ref: "#/components/parameters/JobId"
    post:
      operationId: cancelJob
      tags: [Jobs]
      summary: Cancel an owned render job.
      parameters:
        - $ref: "#/components/parameters/CsrfToken"
      responses:
        "200":
          description: Cancellation was durably recorded, or the job was already cancelled.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Job"}
        "202":
          description: Cloud Run acknowledged cancellation, but terminal persistence did not complete. The returned Job remains cancellation-requested until reconciliation completes and releases its exact lease within the next five-minute Scheduler run. Genuine remote cancellation failures are not acknowledged by this response.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Job"}
        "404": {$ref: "#/components/responses/NotFound"}
        "409": {$ref: "#/components/responses/Conflict"}
  /v1/jobs/{jobId}/retry:
    parameters:
      - $ref: "#/components/parameters/JobId"
    post:
      operationId: retryJob
      tags: [Jobs]
      summary: Retry an owned failed or cancelled render job.
      description: Preflights the requester's Drive delivery grant before queuing another attempt.
      parameters:
        - $ref: "#/components/parameters/CsrfToken"
      responses:
        "202":
          description: A failed or cancelled job was queued as its next attempt.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Job"}
        "401": {$ref: "#/components/responses/AuthenticationRequired"}
        "404": {$ref: "#/components/responses/NotFound"}
        "409": {$ref: "#/components/responses/Conflict"}
        "429": {$ref: "#/components/responses/AdmissionRejected"}
  /v1/jobs/{jobId}/playback-sessions:
    parameters:
      - $ref: "#/components/parameters/JobId"
    post:
      operationId: createCompletedOutputPlaybackSession
      tags: [Jobs]
      summary: Prepare one owned succeeded H.264 output for private browser playback.
      description: |
        Resolves the delivered Drive file through the owner-verified durable
        job, revalidates authoritative Drive metadata, and returns an opaque
        same-origin URL. The Drive file ID, access token, and signed authority
        never enter that URL. One-hour, owner-, job-, and URL-bound playback
        authority is signed into the Firebase-compatible `__session` HttpOnly
        cookie while preserving the authenticated browser session.
      parameters:
        - $ref: "#/components/parameters/CsrfToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
      responses:
        "201":
          description: Completed-output playback session created.
          headers:
            Set-Cookie:
              description: Signed Firebase-compatible `__session` cookie containing the existing session and short-lived playback authority.
              schema: {type: string}
            Cache-Control:
              schema: {const: no-store}
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [playbackUrl, contentType, size]
                properties:
                  playbackUrl:
                    type: string
                    pattern: "^/v1/jobs/[0-9a-f-]{36}/playback/[0-9a-f-]{36}$"
                  contentType: {const: video/mp4}
                  size: {type: integer, minimum: 1}
        "400": {$ref: "#/components/responses/InvalidRequest"}
        "401": {$ref: "#/components/responses/AuthenticationRequired"}
        "403": {$ref: "#/components/responses/CsrfRequired"}
        "404": {$ref: "#/components/responses/NotFound"}
        "415":
          description: The delivered output is not available for browser playback.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Error"
                  - type: object
                    properties:
                      error: {const: browser_playback_not_supported}
        "503":
          description: Restricted Drive access or output metadata is temporarily unavailable.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Error"}
  /v1/jobs/{jobId}/playback/{playbackId}:
    parameters:
      - $ref: "#/components/parameters/JobId"
    get:
      operationId: streamCompletedOutputPlayback
      tags: [Jobs]
      summary: Stream an owned succeeded H.264 output with browser Range semantics.
      description: |
        Rechecks the active member session plus signed owner, job, and path
        binding on every request. One single `Range` is accepted. Open-ended
        and suffix ranges are capped at 8 MiB, proxied directly from Drive,
        and never persisted. Drive must return truthful 206 `Content-Range`
        and `Content-Length` headers. The response is always private and
        no-store.
      parameters:
        - name: playbackId
          in: path
          required: true
          schema: {type: string, format: uuid}
        - name: Range
          in: header
          required: false
          description: One byte range, including bounded, open-ended, or suffix form.
          schema:
            type: string
            examples: ["bytes=0-1048575", "bytes=1048576-", "bytes=-1048576"]
      responses:
        "200":
          description: Complete MP4 returned when Range is omitted.
          headers:
            Accept-Ranges:
              schema: {const: bytes}
            Cache-Control:
              schema: {const: no-store}
          content:
            video/mp4:
              schema: {type: string, contentEncoding: binary}
        "206":
          description: Requested single byte range.
          headers:
            Accept-Ranges:
              schema: {const: bytes}
            Content-Range:
              schema: {type: string, example: "bytes 0-1048575/7340032"}
            Cache-Control:
              schema: {const: no-store}
          content:
            video/mp4:
              schema: {type: string, contentEncoding: binary}
        "401": {$ref: "#/components/responses/AuthenticationRequired"}
        "404": {$ref: "#/components/responses/NotFound"}
        "416":
          description: Range is malformed, multiple, empty, or outside the completed output.
          headers:
            Accept-Ranges:
              schema: {const: bytes}
            Content-Range:
              schema: {type: string, example: "bytes */7340032"}
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Error"
                  - type: object
                    properties:
                      error: {const: playback_range_not_satisfiable}
        "502":
          description: Drive returned an invalid or untruthful ranged response.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Error"
                  - type: object
                    properties:
                      error: {const: drive_playback_unavailable}
  /v1/uploads:
    post:
      operationId: createUpload
      tags: [Jobs]
      summary: Create a 15-minute signed request upload.
      parameters:
        - $ref: "#/components/parameters/CsrfToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [contentType, contentLength]
              properties:
                contentType: {const: application/json}
                contentLength: {type: integer, minimum: 1, maximum: 10551296}
      responses:
        "201":
          description: Signed upload command.
          content:
            application/json:
              schema:
                type: object
                required: [id, method, contentType, contentLength, expiresAt, uploadUrl]
                properties:
                  id: {type: string, format: uuid}
                  method: {const: PUT}
                  contentType: {const: application/json}
                  contentLength: {type: integer}
                  expiresAt: {type: string, format: date-time}
                  uploadUrl: {type: string, format: uri}
        "400": {$ref: "#/components/responses/InvalidRequest"}
  /v1/tokens:
    get:
      operationId: listTokens
      tags: [Tokens]
      summary: List personal token metadata.
      security:
        - sessionCookie: []
      responses:
        "200":
          description: Token metadata; secrets and hashes are never returned.
          content:
            application/json:
              schema:
                type: array
                items: {$ref: "#/components/schemas/Token"}
    post:
      operationId: createToken
      tags: [Tokens]
      summary: Create a personal API token.
      security:
        - sessionCookie: []
      parameters:
        - $ref: "#/components/parameters/CsrfToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [name]
              properties:
                name: {type: string, minLength: 1, maxLength: 80}
      responses:
        "201":
          description: Token metadata and the one-time token secret.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Token"
                  - type: object
                    required: [token]
                    properties:
                      token: {type: string, writeOnly: true}
        "400": {$ref: "#/components/responses/InvalidRequest"}
  /v1/tokens/{tokenId}/revoke:
    parameters:
      - $ref: "#/components/parameters/TokenId"
    post:
      operationId: revokeToken
      tags: [Tokens]
      summary: Revoke a personal API token.
      security:
        - sessionCookie: []
      parameters:
        - $ref: "#/components/parameters/CsrfToken"
      responses:
        "200":
          description: Revoked token metadata.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Token"}
        "404": {$ref: "#/components/responses/NotFound"}
  /v1/admin/members:
    get:
      operationId: listMembers
      tags: [Administration]
      summary: List administrator-managed memberships.
      security:
        - sessionCookie: []
      responses:
        "200":
          description: Administrator-managed memberships.
          content:
            application/json:
              schema:
                type: array
                items: {$ref: "#/components/schemas/Member"}
        "403": {$ref: "#/components/responses/AdminRequired"}
    post:
      operationId: addMember
      tags: [Administration]
      summary: Add or reactivate a member.
      security:
        - sessionCookie: []
      parameters:
        - $ref: "#/components/parameters/CsrfToken"
      requestBody:
        $ref: "#/components/requestBodies/MemberEmail"
      responses:
        "201":
          description: Membership added or reactivated with a new generation.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Member"}
        "403": {$ref: "#/components/responses/AdminRequired"}
  /v1/admin/members/revoke:
    post:
      operationId: revokeMember
      tags: [Administration]
      summary: Revoke a member and every access path.
      security:
        - sessionCookie: []
      parameters:
        - $ref: "#/components/parameters/CsrfToken"
      requestBody:
        $ref: "#/components/requestBodies/MemberEmail"
      responses:
        "200":
          description: Membership, sessions, tokens, Drive grant, and active work revoked.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Member"}
        "403": {$ref: "#/components/responses/AdminRequired"}
components:
  headers:
    RequestId:
      description: Opaque production-scoped UUID correlating the private-preview lifecycle without source or account identity.
      schema: {type: string, format: uuid}
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Alpha Compose personal token
    sessionCookie:
      type: apiKey
      in: cookie
      name: __session
  parameters:
    CsrfToken:
      name: X-CSRF-Token
      in: header
      required: false
      description: Required for writes authenticated by session cookie; omit for bearer authentication.
      schema: {type: string}
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: {type: string, minLength: 1, maxLength: 128}
    OptionalIdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Replays with the same owner, key, and exact request return the same Preview operation.
      schema: {type: string, minLength: 1, maxLength: 128}
    JobId:
      name: jobId
      in: path
      required: true
      schema: {type: string, format: uuid}
    DerivativePreparationId:
      name: preparationId
      in: path
      required: true
      schema: {type: string, format: uuid}
    TokenId:
      name: tokenId
      in: path
      required: true
      schema: {type: string, format: uuid}
    OAuthCode:
      name: code
      in: query
      required: true
      description: One-time Google OAuth authorization code.
      schema: {type: string, minLength: 1}
    OAuthState:
      name: state
      in: query
      required: true
      description: OAuth state value that must match signed state in the `__session` cookie.
      schema: {type: string, minLength: 1}
  requestBodies:
    RenderRequest:
      required: true
      description: Nested API request body only. The outer Compose Project JSON envelope is not accepted here.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/RenderRequest"}
    MemberEmail:
      required: true
      content:
        application/json:
          schema:
            type: object
            additionalProperties: false
            required: [email]
            properties:
              email: {type: string, format: email, maxLength: 254}
  responses:
    InvalidRequest:
      description: |
        Invalid request. Telemetry contract failures use the stable classified
        shape and emit a privacy-safe `request_failed` event with the same
        failure code, category, request ID, retryability, field, and bounded
        line when known. Never contains telemetry values, filenames, Drive IDs,
        account data, request bodies, or signed URLs.
      content:
        application/json:
          schema:
            anyOf:
              - {$ref: "#/components/schemas/Error"}
              - {$ref: "#/components/schemas/TelemetryContractError"}
    AuthenticationRequired:
      description: Authentication or a valid Drive grant is required.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    CsrfRequired:
      description: Browser CSRF token is missing or invalid.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    PayloadTooLarge:
      description: Request exceeded a configured byte limit.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    SynchronousOverlayDurationExceeded:
      description: The valid section exceeds the one-second synchronous diagnostic contract.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/SynchronousOverlayDurationExceeded"}
    Conflict:
      description: Idempotency conflict or invalid state transition.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    AdmissionRejected:
      description: Daily or monthly admission limit reached.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    CapacityUnavailable:
      description: Render capacity is full or a transaction should be retried.
      headers:
        Retry-After:
          schema: {type: integer, minimum: 1}
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    NotFound:
      description: The owned resource does not exist.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    AdminRequired:
      description: Owner or admin session required.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    NotAllowlisted:
      description: Live membership validation failed for an existing credential.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    AccountSuspended:
      description: The verified account could not use an active membership. No session or Drive grant was created.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
  schemas:
    RenderRequest:
      type: object
      additionalProperties: false
      description: Common render configuration accepted by preview, overlay, and durable-job endpoints. Source-video composition options require sourceVideo. Without sourceVideo, the output is a transparent ProRes 4444 overlay.
      required:
        - telemetryFormat
        - telemetry
        - preset
        - displayTimeZone
        - synchronizationMode
        - sectionStartAt
        - sectionEndAt
      properties:
        telemetryFormat:
          type: string
          enum: [polar-csv, garmin-fit, oxiwear-hr-csv]
          description: Heart-rate input encoding. polar-csv and oxiwear-hr-csv use CSV text; garmin-fit uses base64-encoded FIT content.
        telemetry:
          type: string
          description: |
            CSV text or base64-encoded Garmin FIT content. Timestamped Polar
            CSV requires one of `timestamp`, `date/time`, or `datetime` and one
            of `heart_rate`, `heart rate`, `heart rate (bpm)`, `HR`, or
            `HR (bpm)`. OxiWear heart-rate CSV requires `reading_time` and
            `pulse_rate`. CSV is limited to 10 MiB of UTF-8 text; Garmin FIT
            base64 is limited to 13,981,016 characters (a 10 MiB binary input).
        preset:
          type: string
          enum: [1080p25, 2.7k25]
          description: 1080p25 is 1920×1080 at 25 fps for up to 8 minutes; 2.7k25 is 2704×1520 at 25 fps for up to 4 minutes.
        displayTimeZone:
          type: string
          minLength: 1
          description: Required IANA timezone identifier, such as Europe/Warsaw or UTC, used only for the rendered 24-hour local clock. Missing, blank, offset-only, and unknown values are invalid. This field does not reinterpret any input timestamp and has no fallback.
        synchronizationMode:
          type: string
          enum: [shared-clock, manual-anchor]
          description: Required explicit clock relationship. shared-clock aligns activity timestamps directly to sectionStartAt and forbids synchronization anchors. manual-anchor requires both synchronization anchors and maps the selected output through their offset. For a source video, the Compose UI derives the recording start and equal API anchors from a selected elapsed source position plus its matching activity-data instant.
        futureTraceOpacityPercent:
          type: number
          minimum: 0
          maximum: 100
          default: 25
          description: Opacity percentage for the not-yet-reached heart-rate trace. Zero hides it; 100 makes it fully opaque.
        transparentAlphaBits:
          type: integer
          enum: [8, 16]
          default: 16
          description: ProRes 4444 alpha-plane precision. Only valid without sourceVideo. Omission preserves the existing 16-bit-alpha output.
        telemetrySyncAt:
          allOf: [{$ref: "#/components/schemas/Instant"}]
          description: Activity-data timestamp at the synchronization moment.
        cameraSyncAt:
          allOf: [{$ref: "#/components/schemas/Instant"}]
          description: API camera-clock anchor corresponding to telemetrySyncAt. It may be before, within, or after the selected output interval. For a source video with different clocks, the Compose UI does not request this absolute camera value; it derives recordingStartAt from the selected elapsed source position and submits the activity-data instant here.
        sectionStartAt:
          allOf: [{$ref: "#/components/schemas/Instant"}]
          description: Start of the rendered camera section. With sourceVideo, the offset from recordingStartAt must be a nonnegative whole 25 fps frame count.
        sectionEndAt:
          allOf: [{$ref: "#/components/schemas/Instant"}]
          description: End of the rendered camera section; duration must be a whole 25 fps frame count (40 ms steps), remain within the preset maximum, and not exceed known source duration.
        spo2:
          type: object
          additionalProperties: false
          required: [format, telemetry]
          properties:
            format:
              const: oxiwear-spo2-csv
              description: OxiWear SpO₂ CSV format.
            telemetry:
              type: string
              maxLength: 10485760
              description: OxiWear SpO₂ CSV text, limited to 10 MiB of UTF-8 text.
          description: Optional OxiWear SpO₂ series; samples must cover the requested section.
        timer:
          type: object
          additionalProperties: false
          required: [startAt, endAt]
          properties:
            startAt:
              allOf: [{$ref: "#/components/schemas/Instant"}]
              description: Timer start, inclusive, within the requested section.
            endAt:
              allOf: [{$ref: "#/components/schemas/Instant"}]
              description: Timer end, exclusive, after startAt and no later than sectionEndAt.
          description: |
            Optional elapsed timer interval. The rendered readout counts down
            negatively before startAt, shows elapsed timer time from startAt,
            and at and after endAt shows the fixed timer duration plus elapsed
            time since the end. When present, this timer replaces section
            elapsed in the readout and makes x-axis values relative to startAt.
            Axis ticks remain aligned to visible five-second timer multiples.
        watermark:
          type: object
          additionalProperties: false
          required: [contentBase64]
          properties:
            contentBase64:
              type: string
              contentEncoding: base64
              maxLength: 2796204
              description: PNG up to 2 MiB and 1024×1024 pixels.
          description: Optional PNG watermark.
        sourceVideo:
          type: object
          additionalProperties: false
          required: [fileId, recordingStartAt, timeZone]
          properties:
            fileId: {type: string, minLength: 1}
            recordingStartAt:
              type: string
              format: date-time
              description: Source recording start instant, user-confirmed for shared clocks or derived by the Compose UI from an elapsed source-frame match for different clocks.
            timeZone:
              type: string
              description: IANA timezone for the source video clock, user-confirmed for shared clocks or selected for the derived synced recording-time display. Fixed offsets are invalid.
            name: {type: string, description: Ignored client metadata.}
            mimeType: {type: string, description: Ignored client metadata.}
          description: |
            One Drive video selected by ID from My Drive, a file shared with
            the user, or a supported Shared Drive. Server metadata is
            authoritative. Supported MIME types are video/mp4,
            video/quicktime, video/webm, video/mpeg, video/ogg,
            video/x-msvideo, and video/x-matroska. The source must satisfy the
            current server-enforced size bound, be downloadable, decodable, not
            trashed, and be long enough for the selected source-relative output
            range. Source trim is derived from sectionStartAt minus
            recordingStartAt, must be nonnegative,
            and is applied identically to video and source audio. Folders,
            shortcuts, and Google Workspace documents are rejected.
        outputFormat:
          type: string
          enum: [h264-mp4, prores-422-mov]
          default: h264-mp4
          description: Composited output container. Only valid with sourceVideo.
        fitMode:
          type: string
          enum: [letterbox, pillarbox, crop]
          default: letterbox
          description: How the source video fits the render canvas. Only valid with sourceVideo.
        audioMode:
          type: string
          enum: [source+heartbeat, source-only, heartbeat-only]
          default: source+heartbeat
          description: Audio tracks for composited output. Only valid with sourceVideo.
      allOf:
        - oneOf:
            - title: Shared clock
              properties:
                synchronizationMode:
                  const: shared-clock
              required: [synchronizationMode]
              not:
                anyOf:
                  - properties:
                      telemetrySyncAt: {}
                    required: [telemetrySyncAt]
                  - properties:
                      cameraSyncAt: {}
                    required: [cameraSyncAt]
            - title: Manual anchor
              properties:
                synchronizationMode:
                  const: manual-anchor
              required: [telemetrySyncAt, cameraSyncAt]
        - if:
            properties:
              telemetryFormat: {const: garmin-fit}
          then:
            properties:
              telemetry: {maxLength: 13981016}
        - if:
            properties:
              telemetryFormat: {enum: [polar-csv, oxiwear-hr-csv]}
          then:
            properties:
              telemetry: {maxLength: 10485760}
    Instant:
      type: string
      format: date-time
      description: ISO-8601 instant with `Z` or an explicit UTC offset.
    TelemetryContractError:
      type: object
      additionalProperties: false
      description: Stable, non-retryable telemetry validation feedback. Never contains telemetry values, filenames, Drive IDs, account data, request bodies, or signed URLs.
      required: [error, category, failureCode, requestId, retryable, field]
      properties:
        error: {const: invalid_request}
        category: {const: request_contract}
        failureCode:
          type: string
          enum:
            - unsupported_telemetry_columns
            - malformed_telemetry_row
            - heart_rate_out_of_range
            - telemetry_value_out_of_range
            - unordered_telemetry
            - insufficient_telemetry_coverage
            - telemetry_too_large
            - telemetry_sample_limit_exceeded
            - unsupported_telemetry_format
            - invalid_telemetry
        requestId: {type: string, format: uuid}
        retryable: {const: false}
        field:
          type: string
          enum: [telemetry, telemetryFormat, spo2, spo2.telemetry]
        line: {type: integer, minimum: 1, maximum: 1000000000000}
        expectedSchema: {$ref: "#/components/schemas/TelemetryExpectedSchema"}
        documentationPath:
          const: "/openapi.yaml#/components/schemas/RenderRequest"
    TelemetryExpectedSchema:
      type: object
      additionalProperties: false
      required: [timestampColumns, valueColumns]
      properties:
        timestampColumns:
          type: array
          minItems: 1
          maxItems: 3
          items:
            type: string
            enum: [timestamp, date/time, datetime, reading_time]
        valueColumns:
          type: array
          minItems: 1
          maxItems: 5
          items:
            type: string
            enum: [heart_rate, heart rate, heart rate (bpm), HR, HR (bpm), pulse_rate, spo2]
    PreviewOperation:
      type: object
      additionalProperties: false
      required: [id, operationKind, state, progressPercent, statusUrl, resultUrl, createdAt, updatedAt, expiresAt]
      properties:
        id: {type: string, format: uuid}
        operationKind: {const: key-moment-gallery}
        state:
          type: string
          enum: [queued, launching, running, cancellation-requested, succeeded, failed, cancelled]
        progressPercent: {type: integer, minimum: 0, maximum: 100}
        statusUrl: {type: string, pattern: "^/v1/previews/"}
        resultUrl: {type: string, pattern: "^/v1/previews/"}
        createdAt: {type: string, format: date-time}
        updatedAt: {type: string, format: date-time}
        expiresAt:
          type: string
          format: date-time
          description: Operation and image retention boundary, 24 hours after creation.
        result: {$ref: "#/components/schemas/PreviewGallery"}
        error: {$ref: "#/components/schemas/PreviewOperationError"}
    PreviewOperationError:
      type: object
      additionalProperties: false
      required: [code, retryable]
      properties:
        code: {type: string, maxLength: 128}
        category:
          type: string
          enum: [request_contract, drive_source_metadata, drive_source_content, preview_rendering]
        requestId: {type: string, format: uuid}
        retryable: {type: boolean}
        stage: {type: string, maxLength: 128}
        reason:
          type: string
          enum: [preview_decode_failed, source_stream_failed, source_duration_too_short]
        requestedMomentCount: {type: integer, minimum: 1, maximum: 32}
        generatedMomentCount: {type: integer, minimum: 0, maximum: 32}
        omittedMomentCount: {type: integer, minimum: 1, maximum: 32}
        requestedDurationSeconds: {type: integer, minimum: 1, maximum: 480}
        status: {type: integer, minimum: 100, maximum: 599}
        elapsedMs: {type: integer, minimum: 0, maximum: 1000000000000}
        timeoutMs: {type: integer, minimum: 0, maximum: 1000000000000}
    PreviewGallery:
      type: object
      additionalProperties: false
      required: [version, mode, sections, assets]
      properties:
        version: {const: 2}
        mode: {type: string, enum: [overlay, final]}
        sections:
          type: array
          items: {$ref: "#/components/schemas/PreviewTraceSection"}
        assets:
          type: array
          description: One entry per unique output frame, shared across trace sections.
          items: {$ref: "#/components/schemas/PreviewFrameAsset"}
        warnings:
          type: array
          maxItems: 1
          description: Bounded diagnostics for a successful partial selected-source gallery.
          items: {$ref: "#/components/schemas/PreviewWarning"}
      allOf:
        - if:
            properties:
              mode: {const: overlay}
            required: [mode]
          then:
            properties:
              assets:
                items: {$ref: "#/components/schemas/PreviewOverlayFrameAsset"}
        - if:
            properties:
              mode: {const: final}
            required: [mode]
          then:
            properties:
              assets:
                items: {$ref: "#/components/schemas/PreviewFinalFrameAsset"}
    PreviewWarning:
      type: object
      additionalProperties: false
      required: [reason, requestId, requestedMomentCount, generatedMomentCount, omittedMomentCount, requestedDurationSeconds, retryable]
      properties:
        reason: {const: source_duration_too_short}
        requestId: {type: string, format: uuid}
        requestedMomentCount: {type: integer, minimum: 2, maximum: 32}
        generatedMomentCount: {type: integer, minimum: 1, maximum: 31}
        omittedMomentCount: {type: integer, minimum: 1, maximum: 31}
        requestedDurationSeconds: {type: integer, minimum: 1, maximum: 480}
        retryable: {const: false}
    PreviewTraceSection:
      type: object
      additionalProperties: false
      required: [id, name, unit, moments]
      properties:
        id: {type: string}
        name: {type: string}
        unit: {type: string}
        moments:
          type: array
          items: {$ref: "#/components/schemas/PreviewMoment"}
    PreviewMoment:
      type: object
      additionalProperties: false
      required: [frameIndex, elapsedSeconds, elapsed, labels, eventLabel, value, title, frameRef]
      properties:
        frameIndex: {type: integer, minimum: 0}
        elapsedSeconds: {type: number, minimum: 0}
        elapsed: {type: string, pattern: "^[0-9]+:[0-5][0-9]\\.[0-9]{3}$"}
        labels:
          type: array
          minItems: 1
          items: {type: string}
        eventLabel: {type: string}
        value: {type: number}
        title: {type: string}
        frameRef: {type: string, pattern: "^[A-Za-z0-9_-]{1,64}$"}
    PreviewFrameAsset:
      oneOf:
        - $ref: "#/components/schemas/PreviewOverlayFrameAsset"
        - $ref: "#/components/schemas/PreviewFinalFrameAsset"
      discriminator:
        propertyName: kind
        mapping:
          overlay: "#/components/schemas/PreviewOverlayFrameAsset"
          final: "#/components/schemas/PreviewFinalFrameAsset"
    PreviewOverlayFrameAsset:
      type: object
      additionalProperties: false
      required: [id, frameIndex, kind, image]
      properties:
        id: {type: string, pattern: "^[A-Za-z0-9_-]{1,64}$"}
        frameIndex: {type: integer, minimum: 0}
        kind: {const: overlay}
        image: {$ref: "#/components/schemas/PreviewImageReference"}
      examples:
        - id: a000
          frameIndex: 0
          kind: overlay
          image:
            thumbnailUrl: /v1/previews/00000000-0000-0000-0000-000000000061/images/a000-overlay/thumbnail
            fullUrl: /v1/previews/00000000-0000-0000-0000-000000000061/images/a000-overlay/full
    PreviewFinalFrameAsset:
      type: object
      additionalProperties: false
      required: [id, frameIndex, kind, image]
      properties:
        id: {type: string, pattern: "^[A-Za-z0-9_-]{1,64}$"}
        frameIndex: {type: integer, minimum: 0}
        kind: {const: final}
        image: {$ref: "#/components/schemas/PreviewImageReference"}
      examples:
        - id: a000
          frameIndex: 0
          kind: final
          image:
            thumbnailUrl: /v1/previews/00000000-0000-0000-0000-000000000061/images/a000-final/thumbnail
            fullUrl: /v1/previews/00000000-0000-0000-0000-000000000061/images/a000-final/full
    PreviewImageReference:
      type: object
      additionalProperties: false
      required: [thumbnailUrl, fullUrl]
      properties:
        thumbnailUrl: {type: string, pattern: "^/v1/previews/"}
        fullUrl: {type: string, pattern: "^/v1/previews/"}
    Job:
      type: object
      required: [id, state, attempt, statusUrl, cancelUrl, retryUrl]
      properties:
        id: {type: string, format: uuid}
        state:
          type: string
          enum: [queued, launching, running, cancellation-requested, succeeded, failed, cancelled]
        attempt: {type: integer, minimum: 1}
        statusUrl: {type: string}
        cancelUrl: {type: string}
        retryUrl: {type: string}
        failureCode:
          type: string
          enum:
            - worker_failed
            - request_load_failed
            - request_prepare_failed
            - source_metadata_failed
            - source_download_failed
            - source_content_failed
            - drive_grant_required
            - overlay_render_failed
            - composition_encode_failed
            - composition_timeout
            - artifact_upload_failed
            - drive_delivery_failed
            - completion_persistence_failed
            - output_too_large
            - launch_failed
            - stale_lease
        stage:
          type: string
          enum: [request_load, request_prepare, source_content, overlay_render, composition_encode, artifact_upload, drive_delivery, completion_persistence]
        reason:
          type: string
          enum: [preview_decode_failed, source_stream_failed]
        status:
          type: integer
          minimum: 100
          maximum: 599
          description: Upstream numeric status when safely available.
        retryable: {type: boolean}
        elapsedMs: {type: integer, minimum: 0}
        output:
          type: object
          properties:
            objectName: {type: string}
            sha256: {type: string, pattern: "^[0-9a-f]{64}$"}
            contentType: {type: string, enum: [video/quicktime, video/mp4]}
            driveFileId: {type: string}
            driveWebViewLink: {type: string, format: uri}
    DerivativePreparation:
      type: object
      additionalProperties: false
      required: [id, state, attempt, profileVersion, statusUrl, cancelUrl, retryUrl]
      properties:
        id: {type: string, format: uuid}
        state:
          type: string
          enum: [queued, running, cancellation-requested, succeeded, failed, cancelled, expired, revoked]
        attempt: {type: integer, minimum: 1}
        profileVersion: {const: h264-aac-1080p25-v1}
        assetId: {type: string, format: uuid}
        expiresAt: {type: string, format: date-time}
        failureCode: {type: string, maxLength: 64}
        retryable: {type: boolean}
        statusUrl: {type: string, pattern: "^/v1/derivative-preparations/"}
        cancelUrl: {type: string, pattern: "^/v1/derivative-preparations/"}
        retryUrl: {type: string, pattern: "^/v1/derivative-preparations/"}
    Token:
      type: object
      required: [id, name, createdAt, revoked]
      properties:
        id: {type: string, format: uuid}
        name: {type: string}
        createdAt: {type: string, format: date-time}
        revoked: {type: boolean}
    Member:
      type: object
      required: [email, role, status]
      properties:
        email: {type: string, format: email}
        role: {type: string, enum: [owner, admin, member]}
        status: {type: string, enum: [active, revoked]}
        membershipVersion: {type: string, format: uuid}
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
          enum:
            - invalid_request
            - payload_too_large
            - authentication_required
            - not_allowlisted
            - account_suspended
            - invalid_oauth_state
            - invalid_oauth_code
            - invalid_drive_scopes
            - csrf_required
            - drive_grant_required
            - oauth_exchange_failed
            - drive_unavailable
            - kms_unavailable
            - grant_persistence_failed
            - picker_not_configured
            - invalid_playback_source
            - browser_playback_not_supported
            - playback_session_invalid
            - playback_range_not_satisfiable
            - drive_playback_unavailable
            - compositing_requires_durable_job
            - synchronous_overlay_duration_exceeded
            - invalid_source_video
            - drive_source_unavailable
            - invalid_idempotency_key
            - idempotency_conflict
            - derivative_source_not_renderable
            - source_duration_exceeded
            - source_size_exceeded
            - derivative_user_job_active
            - derivative_project_backlog_exhausted
            - derivative_daily_attempt_limit_exhausted
            - derivative_user_budget_exhausted
            - derivative_pool_budget_exhausted
            - derivative_preparation_not_found
            - derivative_playback_unavailable
            - invalid_derivative_transition
            - daily_submission_limit_exhausted
            - monthly_budget_exhausted
            - capacity_exhausted
            - transaction_contention
            - job_not_found
            - preview_not_found
            - preview_image_not_found
            - invalid_job_transition
            - token_not_found
            - admin_required
        retryable: {type: boolean}
        field:
          type: string
          enum: [displayTimeZone, futureTraceOpacityPercent, synchronizationMode, telemetrySyncAt, cameraSyncAt]
        guidance:
          type: string
          enum:
            - Choose synchronizationMode as shared-clock or manual-anchor.
            - Omit telemetrySyncAt and cameraSyncAt when synchronizationMode is shared-clock.
            - Provide telemetrySyncAt and cameraSyncAt when synchronizationMode is manual-anchor.
            - Provide telemetrySyncAt and cameraSyncAt as ISO-8601 instants with Z or an explicit UTC offset.
        recoveryPath: {type: string, const: "/v1/auth/login/start?recovery=true"}
    SynchronousOverlayDurationExceeded:
      type: object
      additionalProperties: false
      required: [error, requestId, maxDurationSeconds, durableJobsPath]
      properties:
        error: {const: synchronous_overlay_duration_exceeded}
        requestId: {type: string, format: uuid}
        maxDurationSeconds: {const: 1}
        durableJobsPath: {const: /v1/jobs}
