openapi: 3.1.0
info:
  title: SeaMeet Media/STT Worker API
  version: 0.4.0
  description: |
    Media/STT Worker-only contract derived from docs/api/openapi.yaml.
servers:
  - url: https://seameet-stt-proxy.seameet.workers.dev
    description: PROD -- Cloudflare Worker
  - url: https://seameet-stt-proxy-dev.seameet.workers.dev
    description: DEV -- Cloudflare Worker
security:
  - supabaseJwt: []
components:
  securitySchemes:
    supabaseJwt:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >
        Advanced first-party app session auth. Normal public API clients should
        use X-Api-Key instead.
    apiKey:
      type: apiKey
      in: header
      name: X-Api-Key
      description: >
        Personal API key for machine/agent access (create at
        https://app.seameet.ai/account). Format `smk_<40 hex>`. This is the
        only header normal API clients need. Scopes: read-only keys may call
        entitlement, account-state, usage, delta, get-sections, get-asset,
        download-url(s), and share-list; write scope unlocks the mutation ops.
        Key management ops (api-key-*) always require a user JWT.
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string }
        detail: { type: string }
        reason: { type: string }
    QuotaExceeded:
      type: object
      description: Returned with HTTP 413 when an upload would exceed the storage quota.
      required: [error, usedBytes, quotaBytes, remainingBytes]
      properties:
        error: { const: quota_exceeded }
        usedBytes: { type: integer }
        quotaBytes: { type: integer }
        remainingBytes: { type: integer }
    Asset:
      type: object
      description: >
        Manifest row (also readable via PostgREST /rest/v1/assets under RLS).
        delta additionally decorates rows with `shared`, `capped`, and -- for
        shared rows -- `ownerDisplayName`.
      required: [id, uid, kind, client_ref, display_name, r2_key, size_bytes,
                 duration_ms, has_video, hash, parent_asset_id, rel_path,
                 link_access, upload_state, trashed_at, trash_expires_at,
                 deleted_at, created_at, updated_at]
      properties:
        id: { type: string, format: uuid }
        uid: { type: string, format: uuid, description: Owner uid }
        kind: { type: string, enum: [audio, video, audio_for_stt, screenshot, bundle, note, wiki] }
        client_ref: { type: string, description: Sanitized-basename sync identity }
        display_name: { type: [string, 'null'] }
        r2_key: { type: [string, 'null'] }
        size_bytes: { type: integer }
        duration_ms: { type: [integer, 'null'] }
        has_video: { type: boolean }
        hash: { type: [string, 'null'], description: sha256 hex }
        parent_asset_id: { type: [string, 'null'], description: audio_for_stt -> its source recording }
        rel_path: { type: [string, 'null'], description: workspace-relative path (note/wiki) }
        link_access: { type: string, enum: [none, viewer] }
        upload_state: { type: string, enum: [pending, uploading, complete] }
        trashed_at: { type: [string, 'null'], format: date-time }
        trash_expires_at: { type: [string, 'null'], format: date-time }
        deleted_at: { type: [string, 'null'], format: date-time }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        shared: { type: boolean, description: 'delta only: true when the row reaches the caller via a grant, not ownership' }
        capped: { type: boolean, description: 'delta only: server applied the reveal cap and content beyond it is hidden' }
        ownerDisplayName: { type: [string, 'null'], description: 'delta only, shared rows: owner display name for "Shared by ..."' }
    SectionVersion:
      type: object
      description: One per-section text row (transcript, summary, notes, ...).
      required: [section, version, content, device_id, updated_at]
      properties:
        asset_id: { type: string, format: uuid, description: Present in delta responses; absent in get-sections. }
        section: { type: string }
        version: { type: integer }
        content: { type: string, description: May be truncated at the reveal cap for non-Pro viewers. }
        device_id: { type: [string, 'null'] }
        updated_at: { type: string, format: date-time }
    AccountBlock:
      type: object
      description: >
        Allowance/meter block consumed by the client walls and meters
        (composeAccount in supabase/functions/_shared/allowance.ts).
      required: [linked, revealCapMs, freeTranscriptionMsLeft, freeOcrLeft,
                 importSecondsThisPeriod, importCapSeconds, ocrImportThisPeriod,
                 ocrCaptureThisPeriod, ocrImportCap, currentPeriodEnd, storageQuotaBytes]
      properties:
        linked: { type: boolean, description: false for anonymous (not-yet-linked) users }
        revealCapMs:
          type: integer
          description: >
            Client-side VIEW mask: transcript content past this offset is
            blurred for free viewers. Sync Pro gets Number.MAX_SAFE_INTEGER
            as a "no mask" sentinel.
        freeTranscriptionMsLeft: { type: integer, description: Remaining ms of the free-tier lifetime generation pool }
        freeOcrLeft: { type: integer }
        importSecondsThisPeriod: { type: integer }
        importCapSeconds: { type: integer }
        ocrImportThisPeriod: { type: integer }
        ocrCaptureThisPeriod: { type: integer }
        ocrImportCap: { type: integer }
        currentPeriodEnd: { type: [string, 'null'], format: date-time }
        storageQuotaBytes: { type: integer }
    Entitlement:
      type: object
      required: [entitled, reason, plan, account]
      properties:
        entitled: { type: boolean }
        reason: { type: string, enum: [ok, no_plan, expired, disabled] }
        plan: { type: string }
        account: { $ref: '#/components/schemas/AccountBlock' }
    AssetMeta:
      type: object
      description: canRead-gated viewer metadata DTO returned by get-asset.
      required: [id, displayName, ownerDisplayName, durationMs, kind, linkAccess, shared, isOwner]
      properties:
        id: { type: string, format: uuid }
        displayName: { type: string }
        ownerDisplayName: { type: string }
        durationMs: { type: [integer, 'null'] }
        kind: { type: string }
        linkAccess: { type: string, enum: [none, viewer] }
        shared: { type: boolean, description: Caller already holds a durable grant (grantee/saver). Always false for the owner. }
        isOwner: { type: boolean }
    ShareEntry:
      type: object
      description: One access-list row returned by share-list.
      required: [id, granteeUid, granteeEmail, granteeName, role, createdAt, resolvedAt, viaLink]
      properties:
        id: { type: string, format: uuid }
        granteeUid: { type: [string, 'null'], format: uuid }
        granteeEmail: { type: [string, 'null'], description: 'Resolved account email, or the pending invite email.' }
        granteeName: { type: [string, 'null'] }
        role: { type: string, enum: [viewer], description: 'v1: roles reserved but always viewer' }
        createdAt: { type: string, format: date-time }
        resolvedAt: { type: [string, 'null'], format: date-time }
        viaLink: { type: boolean, description: 'Grant was self-saved via the share link, not owner-created.' }
paths:
  /v1/file:
    post:
      operationId: fileStt
      tags: [Media/STT Worker]
      summary: Managed file transcription against the synced copy (Worker)
      security:
        - supabaseJwt: []
      servers:
        - url: https://seameet-stt-proxy.seameet.workers.dev
          description: PROD -- Cloudflare Worker
        - url: https://seameet-stt-proxy-dev.seameet.workers.dev
          description: DEV -- Cloudflare Worker
      x-codeSamples:
        - lang: cURL
          label: Transcribe a synced file
          source: |
            curl -sS "https://seameet-stt-proxy.seameet.workers.dev/v1/file" \
              -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"assetId":"00000000-0000-4000-8000-000000000000","language":"en"}'
      description: >
        Prefers the kind=audio_for_stt child for video assets. 409
        not_synced when the asset isn't complete in R2 -- upload first via
        the sync ops. Soniox/Deepgram chosen by server-side routing.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [assetId]
              properties:
                assetId: { type: string, format: uuid }
                language: { type: string, description: ISO-639-1 hint }
            examples:
              transcribeEnglish:
                summary: Transcribe an English recording
                value:
                  assetId: 00000000-0000-4000-8000-000000000000
                  language: en
      responses:
        '200': { description: '{segments: TranscriptSegment[], provider, durationMs}' }
        '409': { description: not_synced }
  /v1/gemini-push:
    post:
      operationId: geminiPush
      tags: [Media/STT Worker]
      summary: Server-side R2 -> Gemini File API push (kills the second client upload for deep analysis)
      security:
        - supabaseJwt: []
      servers:
        - url: https://seameet-stt-proxy.seameet.workers.dev
          description: PROD -- Cloudflare Worker
        - url: https://seameet-stt-proxy-dev.seameet.workers.dev
          description: DEV -- Cloudflare Worker
      requestBody:
        content:
          application/json:
            schema: { type: object, required: [assetId], properties: { assetId: { type: string, format: uuid } } }
      responses:
        '200': { description: '{fileUri, fileName, mimeType, sizeBytes}' }
        '409': { description: not_synced }
        '413': { description: 'too_large -- over the push ceiling; client falls back to direct upload' }
        '504': { description: push_timeout -- client falls back to direct upload }
  /v1/health:
    get:
      operationId: health
      tags: [Media/STT Worker]
      summary: Live STT session counts per provider (unauthenticated, counts only)
      servers:
        - url: https://seameet-stt-proxy.seameet.workers.dev
          description: PROD -- Cloudflare Worker
        - url: https://seameet-stt-proxy-dev.seameet.workers.dev
          description: DEV -- Cloudflare Worker
      security: []
      responses:
        '200': { description: '{sessions: {provider: count}}' }

# ---------------------------------------------------------------------------
# Outbound webhooks (OpenAPI 3.1 `webhooks:`). These describe the POSTs SeaMeet
# sends TO an endpoint you registered via webhook-create -- your server is the
# receiver. Register/list/update/delete the endpoints via the /sync-api#webhook-*
# operations above.
#
# Every delivery carries these headers:
#   X-SeaMeet-Event:     the event name (recording.synced | ai.ready)
#   X-SeaMeet-Delivery:  a unique uuid per delivery attempt
#   X-SeaMeet-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "{t}.{rawBody}")>
# Verify by recomputing v1 over `${t}.${rawBody}` with your endpoint secret and
# rejecting a stale t (recommended tolerance: 5 minutes). Respond 2xx to ack;
# non-2xx / timeout counts as a failure (endpoint auto-disables after 20 in a row).
# ---------------------------------------------------------------------------
