openapi: 3.1.0
info:
  title: Daeryeok Stickerboard API
  version: 1.0.0
  description: The public v1 contract behind the shared standalone stickerboard.
servers:
  - url: https://stickerboard.daeryeok.cafe
paths:
  /v1/health:
    get:
      operationId: stickerboardHealth
      summary: Check the shared board service
      responses:
        "200":
          description: Service is available
          content:
            application/json:
              schema:
                type: object
                required: [ok, service, version, protocol, upstream]
                properties:
                  ok: { type: boolean, const: true }
                  service: { type: string, const: daeryeok-stickerboard }
                  version: { type: string }
                  protocol: { type: string, const: daeryeok-stickerboard/v1 }
                  upstream:
                    type: object
                    required: [service, version]
                    properties:
                      service: { type: string }
                      version: { type: string }
  /v1/stickerboard:
    get:
      operationId: getStickerboard
      summary: Read the authoritative shared paper
      responses:
        "200":
          description: Current stickerboard snapshot
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StickerboardState" }
  /v1/stickerboard/operations:
    post:
      operationId: mutateStickerboard
      summary: Place, move, peel, or clear this device's stickers
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/StickerOperation" }
      responses:
        "200":
          description: Accepted operation and resulting authoritative state
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StickerboardState" }
        "400": { description: Invalid operation }
        "403": { description: Browser origin is not allowed }
        "429": { description: The board is rate limited }
  /v1/stickerboard/uploads:
    post:
      operationId: uploadSticker
      summary: Normalize an image and place it as a shared sticker
      parameters:
        - { name: cid, in: query, required: true, schema: { type: string, minLength: 8, maxLength: 64 } }
        - { name: x, in: query, required: true, schema: { type: number, minimum: 0, maximum: 1 } }
        - { name: y, in: query, required: true, schema: { type: number, minimum: 0, maximum: 1 } }
        - { name: r, in: query, required: true, schema: { type: number, minimum: -45, maximum: 45 } }
        - { name: g, in: query, required: true, schema: { type: integer, enum: [1, 2] } }
        - { name: removeBackground, in: query, required: false, schema: { type: boolean, default: false } }
      requestBody:
        required: true
        content:
          image/jpeg: { schema: { type: string, contentEncoding: binary, maxLength: 5242880 } }
          image/png: { schema: { type: string, contentEncoding: binary, maxLength: 5242880 } }
          image/webp: { schema: { type: string, contentEncoding: binary, maxLength: 5242880 } }
      responses:
        "201":
          description: Processed image was stored and placed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StickerboardState" }
        "413": { description: Source or processed image is too large }
        "415": { description: Unsupported or invalid image }
        "429": { description: The sticker press is rate limited }
  /v1/stickerboard/assets/{assetId}.webp:
    get:
      operationId: getStickerAsset
      summary: Read an image belonging to an active shared sticker
      parameters:
        - name: assetId
          in: path
          required: true
          schema: { type: string, pattern: "^[a-f0-9]{32}$" }
      responses:
        "200":
          description: Processed WebP sticker
          content:
            image/webp: { schema: { type: string, contentEncoding: binary } }
        "404": { description: No active sticker references this asset }
  /v1/stickerboard/socket:
    get:
      operationId: openStickerboardSocket
      summary: Upgrade to the read-only v1 stickerboard WebSocket
      description: Sends a hello snapshot followed by stickers messages. Clients write through the bounded HTTP endpoints.
      responses:
        "101": { description: WebSocket upgrade accepted }
        "426": { description: A WebSocket upgrade is required }
components:
  schemas:
    Sticker:
      type: object
      additionalProperties: false
      required: [id, t, v, x, y, r, g]
      properties:
        id: { type: integer, minimum: 1 }
        t: { type: string, enum: [e, t, w, i] }
        v: { type: string }
        x: { type: number, minimum: 0, maximum: 1 }
        y: { type: number, minimum: 0, maximum: 1 }
        r: { type: number, minimum: -45, maximum: 45 }
        g: { type: integer, enum: [1, 2] }
    StickerboardState:
      type: object
      required: [schemaVersion, protocol, stickers, stickersUpdatedAt]
      properties:
        schemaVersion: { type: integer, const: 1 }
        protocol: { type: string, const: daeryeok-stickerboard/v1 }
        stickers:
          type: array
          maxItems: 80
          items: { $ref: "#/components/schemas/Sticker" }
        stickersUpdatedAt: { type: integer, minimum: 0 }
        stickerId: { type: integer, minimum: 1 }
    StickerPlacement:
      type: object
      additionalProperties: false
      required: [t, v, x, y, r, g]
      properties:
        t: { type: string, enum: [e, t, w] }
        v: { type: string }
        x: { type: number, minimum: 0, maximum: 1 }
        y: { type: number, minimum: 0, maximum: 1 }
        r: { type: number, minimum: -45, maximum: 45 }
        g: { type: integer, enum: [1, 2] }
    StickerOperation:
      oneOf:
        - type: object
          additionalProperties: false
          required: [cid, op, item]
          properties:
            cid: { type: string, minLength: 8, maxLength: 64 }
            op: { type: string, const: add }
            item: { $ref: "#/components/schemas/StickerPlacement" }
        - type: object
          additionalProperties: false
          required: [cid, op, id, x, y, r, g]
          properties:
            cid: { type: string, minLength: 8, maxLength: 64 }
            op: { type: string, const: move }
            id: { type: integer, minimum: 1 }
            x: { type: number, minimum: 0, maximum: 1 }
            y: { type: number, minimum: 0, maximum: 1 }
            r: { type: number, minimum: -45, maximum: 45 }
            g: { type: integer, enum: [1, 2] }
        - type: object
          additionalProperties: false
          required: [cid, op, id]
          properties:
            cid: { type: string, minLength: 8, maxLength: 64 }
            op: { type: string, const: remove }
            id: { type: integer, minimum: 1 }
        - type: object
          additionalProperties: false
          required: [cid, op]
          properties:
            cid: { type: string, minLength: 8, maxLength: 64 }
            op: { type: string, const: clear }
