openapi: 3.1.0
info:
  title: SwarmCast Portal API
  version: 0.3.0
  description: |
    Portal and control-plane API used by the SwarmCast frontend.

    Public subset of the portal API contract published for agents and external
    integrations.

    It covers the public catalog surface plus authenticated session and
    publisher workflows. Admin-only operations are intentionally omitted from
    this published contract.
servers:
  - url: http://localhost:3001
    description: Local backend

tags:
  - name: Session
  - name: Plans
  - name: Streams
  - name: Publisher

paths:
  /session:
    get:
      tags: [Session]
      operationId: getSession
      summary: Get current session state
      responses:
        "200":
          description: Current session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionResponse"
  /session/magic-link/request:
    post:
      tags: [Session]
      operationId: requestMagicLink
      summary: Request an email magic link
      description: Magic-link requests are rate-limited per source IP and email identifier.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MagicLinkRequest"
      responses:
        "200":
          description: Magic link accepted for delivery
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MagicLinkRequestResponse"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "400":
          $ref: "#/components/responses/BadRequest"
  /session/magic-link/complete:
    get:
      tags: [Session]
      operationId: completeMagicLink
      summary: Complete magic-link sign-in callback
      parameters:
        - in: query
          name: token
          schema:
            type: string
      responses:
        "303":
          description: Redirect back to the app after completing or rejecting authentication
  /session/google/start:
    get:
      tags: [Session]
      operationId: startGoogleAuth
      summary: Start Google sign-in
      parameters:
        - in: query
          name: mode
          schema:
            type: string
            enum: [login, signup]
        - in: query
          name: next
          schema:
            type: string
      responses:
        "303":
          description: Redirect to Google or to the local test callback entry point when test auth is enabled
  /session/google/callback:
    get:
      tags: [Session]
      operationId: completeGoogleAuth
      summary: Complete Google sign-in callback
      parameters:
        - in: query
          name: code
          schema:
            type: string
        - in: query
          name: state
          schema:
            type: string
        - in: query
          name: error
          schema:
            type: string
      responses:
        "303":
          description: Redirect back to the app after completing or rejecting authentication
  /session/logout:
    post:
      tags: [Session]
      operationId: logout
      summary: End the current session
      responses:
        "204":
          description: Session ended
  /me:
    get:
      tags: [Session]
      operationId: getCurrentUser
      summary: Get current signed-in user
      responses:
        "200":
          description: Current user
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CurrentUser"
        "401":
          $ref: "#/components/responses/Unauthorized"
    patch:
      tags: [Session]
      operationId: updateCurrentUser
      summary: Update current user account settings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateCurrentUserRequest"
      responses:
        "200":
          description: Updated current user
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CurrentUser"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /me/agent-tokens:
    get:
      tags: [Session]
      operationId: listAgentTokens
      summary: List agent bearer tokens for the current browser-authenticated account
      responses:
        "200":
          description: Current agent tokens
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListAgentTokensResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      tags: [Session]
      operationId: createAgentToken
      summary: Create a new agent bearer token for bots and automation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateAgentTokenRequest"
      responses:
        "201":
          description: Created agent token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateAgentTokenResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /me/agent-tokens/{tokenId}:
    delete:
      tags: [Session]
      operationId: revokeAgentToken
      summary: Revoke an agent bearer token
      parameters:
        - in: path
          name: tokenId
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Token revoked
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /streams:
    get:
      tags: [Streams]
      operationId: listStreams
      summary: List and search streams
      description: Public catalog reads are rate-limited per authenticated user or source IP.
      parameters:
        - in: query
          name: q
          schema:
            type: string
        - in: query
          name: category
          schema:
            type: string
        - in: query
          name: visibility
          schema:
            $ref: "#/components/schemas/StreamVisibility"
        - in: query
          name: sort
          schema:
            $ref: "#/components/schemas/StreamSort"
      responses:
        "200":
          description: Streams list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListStreamsResponse"
        "429":
          $ref: "#/components/responses/TooManyRequests"
  /streams/{streamId}:
    get:
      tags: [Streams]
      operationId: getStream
      summary: Get stream detail
      description: Public catalog reads are rate-limited per authenticated user or source IP.
      parameters:
        - $ref: "#/components/parameters/StreamId"
      responses:
        "200":
          description: Stream detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StreamDetail"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "404":
          $ref: "#/components/responses/NotFound"
  /streams/{streamId}/replay:
    get:
      tags: [Streams]
      operationId: getReplayPreview
      summary: Fetch replay preview for a stream
      description: |
        Returns explicit replay preview events for readable streams.
        Disabled streams reject replay for normal subscribers and keep it only for privileged reference access.
        Replay preview is rate-limited more tightly than catalog reads.
      parameters:
        - $ref: "#/components/parameters/StreamId"
        - in: query
          name: last
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 25
        - in: query
          name: since
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: Replay preview events
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReplayPreview"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "404":
          $ref: "#/components/responses/NotFound"
  /streams/{streamId}/subscription:
    put:
      tags: [Streams]
      operationId: upsertViewerSubscription
      summary: Create or update viewer subscription settings
      description: |
        Accepts either an authenticated browser session or an agent bearer token.
        Live SSE is subscription-backed.
        Disabled streams reject new subscriptions.
        Deprecated streams reject new subscriptions and should be migrated to their successor.
        Subscription writes are rate-limited per authenticated subscriber.
      parameters:
        - $ref: "#/components/parameters/StreamId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpsertViewerSubscriptionRequest"
      responses:
        "200":
          description: Active subscription
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ViewerSubscription"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/BadRequest"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags: [Streams]
      operationId: deleteViewerSubscription
      summary: Cancel viewer subscription
      description: |
        Accepts either an authenticated browser session or an agent bearer token.
        Subscription writes are rate-limited per authenticated subscriber.
      parameters:
        - $ref: "#/components/parameters/StreamId"
      responses:
        "204":
          description: Subscription removed
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "404":
          $ref: "#/components/responses/NotFound"
  /streams/{streamId}/events:
    get:
      tags: [Streams]
      operationId: subscribeToStream
      summary: Subscribe to live stream events over SSE
      description: |
        Server-Sent Events endpoint used for live stream delivery.
        Accepts either an authenticated browser session or an agent bearer token.
        Requires an authenticated active subscription for the same stream unless the caller is using privileged reference access.
        Disabled streams reject live delivery for normal subscribers.
        Live connection attempts are rate-limited per authenticated subscriber.
      parameters:
        - $ref: "#/components/parameters/StreamId"
        - in: query
          name: throttleMs
          schema:
            type: integer
            minimum: 0
        - in: query
          name: replayLast
          schema:
            type: integer
            minimum: 0
        - in: query
          name: since
          schema:
            type: string
            format: date-time
        - in: query
          name: deliveryMode
          schema:
            $ref: "#/components/schemas/DeliveryMode"
      responses:
        "200":
          description: text/event-stream response
          content:
            text/event-stream:
              schema:
                type: string
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/BadRequest"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      tags: [Publisher]
      operationId: publishStreamEvent
      summary: Publish an event into a stream
      description: |
        Accepts either a session cookie or Authorization Bearer token.
        Event publishing is rate-limited both per authenticated publisher account and per authenticated publisher account plus stream.
      parameters:
        - $ref: "#/components/parameters/StreamId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PublishEventRequest"
      responses:
        "201":
          description: Event accepted and appended to replay
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublishEventResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "404":
          $ref: "#/components/responses/NotFound"
  /streams/{streamId}/heartbeat:
    post:
      tags: [Publisher]
      operationId: publishStreamHeartbeat
      summary: Publish a publisher heartbeat for a stream
      description: |
        Accepts either a session cookie or Authorization Bearer token.
        Heartbeats refresh publisher presence only. They do not append replay events, consume stream sequence numbers, or change stream business-event metrics.
        Heartbeat mode becomes visible on stream detail and publisher home after the first accepted heartbeat.
        Publisher heartbeat writes are rate-limited separately per authenticated publisher account and stream.
      parameters:
        - $ref: "#/components/parameters/StreamId"
      responses:
        "200":
          description: Heartbeat accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublishHeartbeatResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "404":
          $ref: "#/components/responses/NotFound"
  /publisher/profile:
    get:
      tags: [Publisher]
      operationId: getPublisherProfile
      summary: Get publisher profile for the signed-in account
      description: Accepts either a session cookie or Authorization Bearer token.
      responses:
        "200":
          description: Publisher profile
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublisherProfile"
        "401":
          $ref: "#/components/responses/Unauthorized"
    put:
      tags: [Publisher]
      operationId: updatePublisherProfile
      summary: Create or update publisher profile
      description: |
        Accepts either a session cookie or Authorization Bearer token.
        Publishing is limited to approved publisher accounts during the current alpha release.
        Publisher control writes are rate-limited per approved publisher account.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdatePublisherProfileRequest"
      responses:
        "200":
          description: Updated publisher profile
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublisherProfile"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
  /publisher/home:
    get:
      tags: [Publisher]
      operationId: getPublisherHome
      summary: Get operational status for the signed-in publisher
      description: Accepts either a session cookie or Authorization Bearer token and includes publisher approval state in the action list.
      responses:
        "200":
          description: Publisher operational home payload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublisherHomeResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /publisher/streams:
    get:
      tags: [Publisher]
      operationId: listPublisherStreams
      summary: List streams owned by the signed-in publisher
      description: Accepts either a session cookie or Authorization Bearer token.
      responses:
        "200":
          description: Publisher streams
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListPublisherStreamsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [Publisher]
      operationId: createPublisherStream
      summary: Create a publisher-managed stream
      description: |
        Accepts either a session cookie or Authorization Bearer token.
        Stream creation requires approved publisher access.
        Publisher control writes are rate-limited per approved publisher account.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PublisherStreamInput"
      responses:
        "201":
          description: Created publisher stream
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublisherStreamRecord"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
  /publisher/streams/{streamId}:
    get:
      tags: [Publisher]
      operationId: getPublisherStream
      summary: Get a publisher-managed stream for editing
      description: Accepts either a session cookie or Authorization Bearer token.
      parameters:
        - $ref: "#/components/parameters/StreamId"
      responses:
        "200":
          description: Publisher stream record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublisherStreamRecord"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags: [Publisher]
      operationId: updatePublisherStream
      summary: Update a publisher-managed stream
      description: |
        Accepts either a session cookie or Authorization Bearer token.
        Publisher control writes are rate-limited per authenticated publisher.
      parameters:
        - $ref: "#/components/parameters/StreamId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PublisherStreamInput"
      responses:
        "200":
          description: Updated publisher stream
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublisherStreamRecord"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "404":
          $ref: "#/components/responses/NotFound"
components:
  parameters:
    StreamId:
      in: path
      name: streamId
      required: true
      description: Stream identifier or slug
      schema:
        type: string

  responses:
    BadRequest:
      description: Request validation failed
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Unauthorized:
      description: Authentication required
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Forbidden:
      description: Authenticated but missing permissions
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    TooManyRequests:
      description: Rate limit exceeded. Respect Retry-After before retrying.
      headers:
        Retry-After:
          description: Seconds until the next retry is allowed.
          schema:
            type: integer
        X-RateLimit-Limit:
          description: Limit applied to the current policy window.
          schema:
            type: integer
        X-RateLimit-Remaining:
          description: Remaining requests in the current policy window.
          schema:
            type: integer
        X-RateLimit-Reset:
          description: Seconds until the current policy window resets.
          schema:
            type: integer
        X-RateLimit-Policy:
          description: Applied policy identifier plus quota metadata.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"

  schemas:
    ErrorResponse:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
        message:
          type: string
        details:
          type: object
          additionalProperties: true

    UserRole:
      type: string
      enum: [user, admin]
    AccountStatus:
      type: string
      enum: [active, suspended]
    PublisherReviewStatus:
      type: string
      enum: [none, pending, verified]
    StreamVisibility:
      type: string
      enum: [public, private, team]
    DeliveryMode:
      type: string
      enum: [raw, compact, summary]
    PricingModel:
      type: string
      enum: [free, credit-tier, private-fee]
    PublisherType:
      type: string
      enum: [first-party, community, private]
    StreamSort:
      type: string
      enum: [popular, trending, newest, reliable]
      default: popular
    StreamModerationStatus:
      type: string
      enum: [active, disabled]

    SessionResponse:
      type: object
      required: [isAuthenticated]
      properties:
        isAuthenticated:
          type: boolean
        user:
          $ref: "#/components/schemas/CurrentUser"

    MagicLinkRequest:
      type: object
      required: [email]
      properties:
        email:
          type: string
          format: email
        mode:
          type: string
          enum: [login, signup]
        next:
          type: string

    MagicLinkRequestResponse:
      type: object
      required: [email, expiresAt]
      properties:
        email:
          type: string
          format: email
        expiresAt:
          type: string
          format: date-time
        testToken:
          type: string

    UpdateCurrentUserRequest:
      type: object
      properties:
        displayName:
          type: string
          minLength: 1
          maxLength: 48

    AgentToken:
      type: object
      required: [id, label, tokenPrefix, createdAt, expiresAt]
      properties:
        id:
          type: string
        label:
          type: string
        tokenPrefix:
          type: string
        createdAt:
          type: string
          format: date-time
        lastUsedAt:
          type: string
          format: date-time
        expiresAt:
          type: string
          format: date-time
        revokedAt:
          type: string
          format: date-time

    ListAgentTokensResponse:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/AgentToken"

    CreateAgentTokenRequest:
      type: object
      required: [label]
      properties:
        label:
          type: string
          minLength: 1
          maxLength: 120
        expiresInDays:
          type: integer
          minimum: 1
          maximum: 365

    CreateAgentTokenResponse:
      type: object
      required: [token, accessToken]
      properties:
        token:
          $ref: "#/components/schemas/AgentToken"
        accessToken:
          type: string

    CurrentUser:
      type: object
      required:
        - id
        - handle
        - displayName
        - email
        - workspaceName
        - planCode
        - creditsIncluded
        - creditsUsed
        - creditsRemaining
        - activeLiveConnections
        - liveConnectionsLimit
        - roles
        - accountStatus
        - publisherStatus
      properties:
        id:
          type: string
        handle:
          type: string
        displayName:
          type: string
        email:
          type: string
          format: email
        workspaceName:
          type: string
        planCode:
          type: string
          enum: [free, pro, power]
        creditsIncluded:
          type: integer
        creditsUsed:
          type: integer
        creditsRemaining:
          type: integer
        activeLiveConnections:
          type: integer
        liveConnectionsLimit:
          type: integer
        roles:
          type: array
          items:
            $ref: "#/components/schemas/UserRole"
        accountStatus:
          $ref: "#/components/schemas/AccountStatus"
        publisherStatus:
          $ref: "#/components/schemas/PublisherReviewStatus"

    Publisher:
      type: object
      required: [id, name, verified, type]
      properties:
        id:
          type: string
        name:
          type: string
        verified:
          type: boolean
        type:
          $ref: "#/components/schemas/PublisherType"

    StreamSummary:
      type: object
      required:
        - id
        - slug
        - name
        - shortDescription
        - category
        - visibility
        - pricingModel
        - creditCost
        - priceLabel
        - subscriberCount
        - availability7d
        - messagesPerMinute
        - lastEventAt
        - rating
        - viewerSubscribed
        - previewMessages
        - publisher
        - tags
        - lifecycle
        - moderation
      properties:
        id:
          type: string
        slug:
          type: string
        name:
          type: string
        shortDescription:
          type: string
        category:
          type: string
        visibility:
          $ref: "#/components/schemas/StreamVisibility"
        pricingModel:
          $ref: "#/components/schemas/PricingModel"
        creditCost:
          type: integer
        priceLabel:
          type: string
        subscriberCount:
          type: integer
        availability7d:
          type: number
          format: float
        averagePublishIntervalMs:
          type: integer
        messagesPerMinute:
          type: number
          format: float
        lastEventAt:
          type: string
          format: date-time
        rating:
          type: number
          format: float
        viewerSubscribed:
          type: boolean
        previewMessages:
          type: array
          items:
            type: string
        publisher:
          $ref: "#/components/schemas/Publisher"
        tags:
          type: array
          items:
            type: string
        lifecycle:
          $ref: "#/components/schemas/StreamLifecycle"
        moderation:
          $ref: "#/components/schemas/StreamModerationState"

    StreamLifecycleStatus:
      type: string
      enum: [active, deprecated]

    StreamSuccessor:
      type: object
      required: [id, slug, name]
      properties:
        id:
          type: string
        slug:
          type: string
        name:
          type: string

    StreamLifecycle:
      type: object
      required: [status]
      properties:
        status:
          $ref: "#/components/schemas/StreamLifecycleStatus"
        deprecatedAt:
          type: string
          format: date-time
        successor:
          $ref: "#/components/schemas/StreamSuccessor"

    StreamLifecycleInput:
      type: object
      properties:
        status:
          $ref: "#/components/schemas/StreamLifecycleStatus"
        successorStreamId:
          type: string

    ReplayPreviewEvent:
      type: object
      required: [eventId, sequence, type, timestamp, payloadPreview, payload]
      properties:
        eventId:
          type: string
        sequence:
          type: integer
        type:
          type: string
        timestamp:
          type: string
          format: date-time
        confidence:
          type: number
          format: float
        payloadPreview:
          type: string
        payload:
          type: object
          additionalProperties: true
        display:
          $ref: "#/components/schemas/EventDisplay"

    ReplayPreview:
      type: object
      required: [streamId, events]
      properties:
        streamId:
          type: string
        events:
          type: array
          items:
            $ref: "#/components/schemas/ReplayPreviewEvent"

    StreamDetail:
      allOf:
        - $ref: "#/components/schemas/StreamSummary"
        - type: object
          required:
            - longDescription
            - retentionLabel
            - schemaVersion
            - payloadSchema
            - eventTypes
            - presentationMode
            - presentationConfig
            - lineage
            - whySubscribe
            - sampleEvent
            - defaultSubscriptionConfig
            - streamTrustScore
            - publisherTrustScore
            - protectionPolicy
            - licensedReuseAllowed
            - revenueShareEligible
            - publisherPresence
          properties:
            longDescription:
              type: string
            schemaHint:
              type: string
            schemaVersion:
              type: string
            payloadSchema:
              type: object
              additionalProperties: true
            eventTypes:
              type: array
              items:
                type: string
            presentationMode:
              $ref: "#/components/schemas/PresentationMode"
            presentationConfig:
              $ref: "#/components/schemas/PresentationConfig"
            retentionLabel:
              type: string
            streamTrustScore:
              type: number
              format: float
            publisherTrustScore:
              type: number
              format: float
            sampleEvent:
              type: object
              additionalProperties: true
            whySubscribe:
              type: array
              items:
                type: string
            protectionPolicy:
              type: array
              items:
                type: string
            licensedReuseAllowed:
              type: boolean
            revenueShareEligible:
              type: boolean
            lineage:
              type: array
              items:
                $ref: "#/components/schemas/LineageNode"
            defaultSubscriptionConfig:
              $ref: "#/components/schemas/SubscriptionConfig"
            publisherPresence:
              $ref: "#/components/schemas/PublisherPresence"

    LineageNode:
      type: object
      required: [streamId, streamName, relationship]
      properties:
        streamId:
          type: string
        streamName:
          type: string
        relationship:
          type: string
          enum: [source, derived-from]

    SubscriptionConfig:
      type: object
      required:
        [
          deliveryMode,
          replayLast,
          throttleMs,
          filterTags,
          minConfidence,
          dedupWindowMs,
        ]
      properties:
        deliveryMode:
          $ref: "#/components/schemas/DeliveryMode"
        replayLast:
          type: integer
          minimum: 0
        replaySince:
          type: string
          format: date-time
        throttleMs:
          type: integer
          minimum: 0
        filterTags:
          type: array
          items:
            type: string
        minConfidence:
          type: number
          format: float
        dedupWindowMs:
          type: integer
          minimum: 0

    PresentationMode:
      type: string
      enum: [text_line, headline_with_meta]

    PresentationConfig:
      type: object
      properties:
        lineField:
          type: string
        titleField:
          type: string
        bodyField:
          type: string
        badgeField:
          type: string
        metaFields:
          type: array
          items:
            type: string

    EventDisplayMeta:
      type: object
      required: [label, value]
      properties:
        label:
          type: string
        value:
          type: string

    EventDisplay:
      type: object
      properties:
        title:
          type: string
        body:
          type: string
        badge:
          type: string
        meta:
          type: array
          items:
            $ref: "#/components/schemas/EventDisplayMeta"

    UpsertViewerSubscriptionRequest:
      type: object
      properties:
        deliveryMode:
          $ref: "#/components/schemas/DeliveryMode"
        replayLast:
          type: integer
          minimum: 0
        replaySince:
          type: string
          format: date-time
        throttleMs:
          type: integer
          minimum: 0
        filterTags:
          type: array
          items:
            type: string
        minConfidence:
          type: number
          format: float
        dedupWindowMs:
          type: integer
          minimum: 0

    ViewerSubscription:
      allOf:
        - $ref: "#/components/schemas/SubscriptionConfig"
        - type: object
          required: [streamId, subscribedAt]
          properties:
            streamId:
              type: string
            subscribedAt:
              type: string
              format: date-time

    PublisherPresenceMode:
      type: string
      enum: [events-only, heartbeat]

    PublisherPresenceStatus:
      type: string
      enum: [unsupported, healthy, stale]

    PublisherPresence:
      type: object
      required: [mode, status]
      properties:
        mode:
          $ref: "#/components/schemas/PublisherPresenceMode"
        status:
          $ref: "#/components/schemas/PublisherPresenceStatus"
        lastBusinessEventAt:
          type: string
          format: date-time
        lastHeartbeatAt:
          type: string
          format: date-time
        lastPublisherSeenAt:
          type: string
          format: date-time
        heartbeatExpectedEveryMinutes:
          type: integer
        staleAfterMinutes:
          type: integer

    ListStreamsResponse:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/StreamSummary"

    PublisherProfile:
      type: object
      required: [name, legalName, billingEmail, country, website, saved]
      properties:
        name:
          type: string
          maxLength: 48
        legalName:
          type: string
          maxLength: 80
        billingEmail:
          type: string
          format: email
          maxLength: 80
        country:
          type: string
          maxLength: 56
        website:
          type: string
          maxLength: 80
        saved:
          type: boolean

    UpdatePublisherProfileRequest:
      type: object
      properties:
        name:
          type: string
          maxLength: 48
        legalName:
          type: string
          maxLength: 80
        billingEmail:
          type: string
          format: email
          maxLength: 80
        country:
          type: string
          maxLength: 56
        website:
          type: string
          maxLength: 80

    PublisherStreamInput:
      type: object
      required:
        - name
        - shortDescription
        - longDescription
        - category
        - creditCost
        - samplePreview
        - schemaVersion
        - payloadSchema
        - eventTypes
        - presentationMode
        - presentationConfig
        - sampleEvent
        - upstreamStreamIds
        - attributionNote
      properties:
        name:
          type: string
          maxLength: 72
        slug:
          type: string
          maxLength: 48
        shortDescription:
          type: string
          maxLength: 140
        longDescription:
          type: string
        category:
          type: string
        creditCost:
          type: integer
          minimum: 0
        samplePreview:
          type: string
        schemaVersion:
          type: string
        payloadSchema:
          type: object
          additionalProperties: true
        eventTypes:
          type: array
          items:
            type: string
        presentationMode:
          $ref: "#/components/schemas/PresentationMode"
        presentationConfig:
          $ref: "#/components/schemas/PresentationConfig"
        sampleEvent:
          type: object
          additionalProperties: true
        upstreamStreamIds:
          type: array
          items:
            type: string
        attributionNote:
          type: string
        lifecycle:
          $ref: "#/components/schemas/StreamLifecycleInput"

    PublisherStreamMeta:
      type: object
      required: [activeSince, publishedEvents]
      properties:
        activeSince:
          type: string
          format: date-time
        publishedEvents:
          type: integer

    PublisherStreamRecord:
      type: object
      required:
        [stream, replay, meta, attributionNote, publishEndpoint, publishedAt]
      properties:
        stream:
          $ref: "#/components/schemas/StreamDetail"
        replay:
          $ref: "#/components/schemas/ReplayPreview"
        meta:
          $ref: "#/components/schemas/PublisherStreamMeta"
        attributionNote:
          type: string
        publishEndpoint:
          type: string
        publishedAt:
          type: string
          format: date-time

    ListPublisherStreamsResponse:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/PublisherStreamRecord"


    PublisherHomeFreshnessStatus:
      type: string
      enum: [healthy, attention, idle]

    PublisherHomeActionSeverity:
      type: string
      enum: [info, warning, critical]

    PublisherHomeAction:
      type: object
      required: [code, severity, title, description]
      properties:
        code:
          type: string
        severity:
          $ref: '#/components/schemas/PublisherHomeActionSeverity'
        title:
          type: string
        description:
          type: string
        streamId:
          type: string
        relatedStreamId:
          type: string

    PublisherHomeUpstreamState:
      type: object
      required: [id, slug, name, lastEventAt, lifecycle, moderation, needsAttention]
      properties:
        id:
          type: string
        slug:
          type: string
        name:
          type: string
        lastEventAt:
          type: string
          format: date-time
        lifecycle:
          $ref: '#/components/schemas/StreamLifecycle'
        moderation:
          $ref: '#/components/schemas/StreamModerationState'
        needsAttention:
          type: boolean

    PublisherHomeStreamState:
      type: object
      required:
        [
          stream,
          activeSince,
          publishedEvents,
          publishEndpoint,
          freshnessStatus,
          publisherPresence,
          needsAttention,
          upstreams,
          actions,
        ]
      properties:
        stream:
          $ref: '#/components/schemas/StreamSummary'
        activeSince:
          type: string
          format: date-time
        publishedEvents:
          type: integer
        publishEndpoint:
          type: string
        freshnessStatus:
          $ref: '#/components/schemas/PublisherHomeFreshnessStatus'
        publisherPresence:
          $ref: '#/components/schemas/PublisherPresence'
        needsAttention:
          type: boolean
        lastEventId:
          type: string
        lastEventType:
          type: string
        lastEventAt:
          type: string
          format: date-time
        upstreams:
          type: array
          items:
            $ref: '#/components/schemas/PublisherHomeUpstreamState'
        actions:
          type: array
          items:
            $ref: '#/components/schemas/PublisherHomeAction'

    PublisherHomeSummary:
      type: object
      required:
        [
          totalStreams,
          activeStreams,
          deprecatedStreams,
          disabledStreams,
          attentionStreams,
          totalSubscribers,
          totalPublishedEvents,
          actionItems,
        ]
      properties:
        totalStreams:
          type: integer
        activeStreams:
          type: integer
        deprecatedStreams:
          type: integer
        disabledStreams:
          type: integer
        attentionStreams:
          type: integer
        totalSubscribers:
          type: integer
        totalPublishedEvents:
          type: integer
        actionItems:
          type: integer

    PublisherHomeResponse:
      type: object
      required: [generatedAt, account, profile, summary, streams, actions]
      properties:
        generatedAt:
          type: string
          format: date-time
        account:
          $ref: '#/components/schemas/CurrentUser'
        profile:
          $ref: '#/components/schemas/PublisherProfile'
        summary:
          $ref: '#/components/schemas/PublisherHomeSummary'
        streams:
          type: array
          items:
            $ref: '#/components/schemas/PublisherHomeStreamState'
        actions:
          type: array
          items:
            $ref: '#/components/schemas/PublisherHomeAction'

    PublishEventRequest:
      type: object
      required: [type, payload]
      properties:
        eventId:
          type: string
        type:
          type: string
        timestamp:
          type: string
          format: date-time
        confidence:
          type: number
          format: float
        payloadPreview:
          type: string
        payload:
          type: object
          additionalProperties: true

    PublishEventResponse:
      type: object
      required: [streamId, acceptedAt, sequence, event]
      properties:
        streamId:
          type: string
        acceptedAt:
          type: string
          format: date-time
        sequence:
          type: integer
        event:
          $ref: "#/components/schemas/ReplayPreviewEvent"

    PublishHeartbeatResponse:
      type: object
      required: [streamId, acceptedAt, publisherPresence]
      properties:
        streamId:
          type: string
        acceptedAt:
          type: string
          format: date-time
        publisherPresence:
          $ref: "#/components/schemas/PublisherPresence"

    StreamModerationState:
      type: object
      required: [status]
      properties:
        status:
          $ref: "#/components/schemas/StreamModerationStatus"
        disabledReason:
          type: string
        updatedAt:
          type: string
          format: date-time
