openapi: 3.1.0
info:
  title: Render Service
  description: |
    HTML-to-PDF/PNG rendering service backed by a headless browser.
    Accepts a fully-formed HTML document and returns a rendered PDF or PNG
    depending on the `Accept` header.

    The service is stateless: each request contains everything needed to
    render. External resources referenced in the HTML (images, fonts, CSS)
    are fetched at render time, so keep them minimal and consider inlining
    assets as data URIs for determinism.

    `POST /render/svg`, `/render/mermaid` and `/render/graphviz` are the
    exceptions: they draw a source that may come from an untrusted author, and
    fetch nothing.
  version: 1.3.0

servers:
  - url: http://service-render:3000
    description: In-cluster service address

paths:
  /render:
    post:
      summary: Render HTML to PDF or PNG
      description: |
        Accepts an HTML document and returns the rendered output as binary.
        The response format is controlled by the `Accept` header:

        - `Accept: application/pdf` (default) — full paginated PDF
        - `Accept: image/png` — PNG snapshot of the first page

        The response body is the raw binary, not a JSON envelope.
      operationId: render
      parameters:
        - in: header
          name: Accept
          schema:
            type: string
            enum: [application/pdf, image/png]
            default: application/pdf
          description: |
            Desired output format. Defaults to `application/pdf`.
            Use `image/png` to get a PNG snapshot of the first page.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RenderRequest'
            examples:
              minimal:
                summary: Minimal PDF request
                value:
                  html: "<!DOCTYPE html><html><body><h1>Hello</h1></body></html>"
              transcript_pdf:
                summary: Transcript PDF with margins
                value:
                  html: "<!DOCTYPE html>..."
                  options:
                    format: A4
                    pdf:
                      printBackground: true
                      margin:
                        top: "12mm"
                        bottom: "12mm"
                        left: "10mm"
                        right: "10mm"
                  timeoutMs: 20000
              transcript_png:
                summary: First-page PNG thumbnail
                value:
                  html: "<!DOCTYPE html>..."
                  options:
                    format: A4
      responses:
        '200':
          description: Rendered successfully
          headers:
            Content-Length:
              description: Size of the output in bytes
              schema:
                type: integer
            Content-Type:
              description: MIME type of the output, matching the requested Accept format
              schema:
                type: string
          content:
            application/pdf:
              schema:
                type: string
                format: binary
            image/png:
              schema:
                type: string
                format: binary
        '400':
          description: Invalid request (missing HTML, malformed options, etc.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: HTML payload too large
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Rendering failed (browser crash, timeout, etc.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: Service unavailable (browser unhealthy or queue full)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /render/svg:
    post:
      summary: Render an SVG to PNG
      description: |
        Draws an SVG as a PNG `width` pixels wide, as tall as the SVG's own
        aspect ratio (its `viewBox`, or its `width`/`height`) makes it, on a
        white background. An SVG with neither is drawn 150 pixels tall.

        Safe for an SVG from an untrusted author: it is drawn as an image, so
        no script in it runs and no external resource it names (image,
        stylesheet, font) is fetched; the page also runs with JavaScript off
        and every request aborted. Text uses the image's system fonts.

        The response body is the raw PNG, not a JSON envelope.
      operationId: renderSvg
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SvgRenderRequest'
            examples:
              diagram:
                summary: A diagram 800 pixels wide
                value:
                  svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100"><rect width="200" height="100" fill="#4a90d9"/></svg>'
                  width: 800
      responses:
        '200':
          description: Rendered successfully
          headers:
            Content-Length:
              description: Size of the PNG in bytes
              schema:
                type: integer
          content:
            image/png:
              schema:
                type: string
                format: binary
        '400':
          description: Invalid request (`svg` missing, `width` not an integer from 1 to 4096)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: SVG over 2 MB (`svg_too_large`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: |
            The SVG could not be decoded (`invalid_svg`), or the PNG would be
            taller than 8192 pixels or larger than 16,777,216 pixels in all
            (`image_too_large`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Rendering failed (browser crash, timeout, etc.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: Service unavailable (browser unhealthy or queue full)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /render/mermaid:
    post:
      summary: Render a Mermaid diagram to PNG
      description: |
        Draws a Mermaid diagram (flowchart, sequence, state, ER, Gantt, ...)
        with a pinned Mermaid build loaded from local disk, never a CDN.

        Safe for a source from an untrusted author: Mermaid runs at
        `securityLevel: 'strict'` (no click handlers, sanitized labels) in a
        page whose Content-Security-Policy lets only the Mermaid bundle run,
        and every request the page makes is aborted, so nothing a label,
        link or directive names is fetched. Text uses the server's fonts.

        The PNG is `width` pixels wide when asked, and otherwise twice the
        diagram's own width (at most 4096), as tall as the diagram's aspect
        ratio makes it. It is drawn the same way as `/render/svg`, under the
        same pixel caps, and shares the render queue with `/render` and
        `/render/svg`.

        The response body is the raw PNG, not a JSON envelope.
      operationId: renderMermaid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MermaidRenderRequest'
            examples:
              diagram:
                summary: A flowchart 800 pixels wide
                value:
                  source: "flowchart LR\n  A[Start] --> B{Ok?}\n  B -->|yes| C[Done]"
                  width: 800
                  theme: neutral
      responses:
        '200':
          description: Rendered successfully
          headers:
            Content-Length:
              description: Size of the PNG in bytes
              schema:
                type: integer
          content:
            image/png:
              schema:
                type: string
                format: binary
        '400':
          description: Invalid request (`source` missing, `width` not an integer from 1 to 4096, or `theme` not one of `default`, `neutral`, `dark`, `forest`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: Source over 50 KB (`source_too_large`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: |
            The source has an error (`invalid_mermaid`; `message` is Mermaid's own,
            for example "Parse error on line 3: ..."), or the PNG would be taller than 8192 pixels
            or larger than 16,777,216 pixels in all (`image_too_large`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Rendering failed (browser crash, timeout, etc.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: Service unavailable (browser unhealthy or queue full)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /render/graphviz:
    post:
      summary: Render a Graphviz diagram to PNG
      description: |
        Draws a Graphviz (DOT) graph with a pinned Graphviz WebAssembly build
        (`@viz-js/viz`) loaded from local disk, in the same locked-down page
        as Mermaid: only the bundle runs and every request is aborted.
        Graphviz runs no script, and a file or URL an `image`, `href` or `URL`
        attribute names is never read or fetched.

        The PNG is `width` pixels wide when asked, and otherwise twice the
        diagram's own width (at most 4096), as tall as the diagram's aspect
        ratio makes it. It is drawn the same way as `/render/svg`, under the
        same pixel caps, and shares the render queue with `/render` and
        `/render/svg`.

        The response body is the raw PNG, not a JSON envelope.
      operationId: renderGraphviz
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GraphvizRenderRequest'
            examples:
              diagram:
                summary: A dependency graph 600 pixels wide
                value:
                  source: "digraph { api -> core; agent -> core }"
                  width: 600
                  engine: dot
      responses:
        '200':
          description: Rendered successfully
          headers:
            Content-Length:
              description: Size of the PNG in bytes
              schema:
                type: integer
          content:
            image/png:
              schema:
                type: string
                format: binary
        '400':
          description: Invalid request (`source` missing, `width` not an integer from 1 to 4096, or `engine` not one of `dot`, `neato`, `fdp`, `sfdp`, `circo`, `twopi`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: Source over 50 KB (`source_too_large`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: |
            The source has an error (`invalid_graphviz`; `message` is Graphviz's own,
            for example "syntax error in line 1 near '}'"), or the PNG would be taller than 8192 pixels
            or larger than 16,777,216 pixels in all (`image_too_large`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Rendering failed (browser crash, timeout, etc.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: Service unavailable (browser unhealthy or queue full)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /healthz:
    get:
      summary: Service health check
      description: |
        Returns 200 if the service is alive and the browser instance is
        responsive. Used by orchestrators and clients with circuit breakers.
      operationId: healthCheck
      responses:
        '200':
          description: Service is healthy
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Health'
        '503':
          description: Service is unhealthy (browser dead, etc.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Health'

components:
  schemas:
    RenderRequest:
      type: object
      required:
        - html
      properties:
        html:
          type: string
          description: Complete HTML document to render. Must include doctype and html tags.
          minLength: 1
          maxLength: 10485760  # 10 MB cap; tune to your needs
          example: "<!DOCTYPE html><html><body>...</body></html>"
        options:
          $ref: '#/components/schemas/RenderOptions'
        timeoutMs:
          type: integer
          description: |
            Per-request render timeout in milliseconds. Caps total time
            spent on this render (page load + output generation). Server
            enforces an upper bound regardless of client value.
          minimum: 1000
          maximum: 120000
          default: 30000

    RenderOptions:
      type: object
      description: |
        Rendering options. `format` and `landscape` are shared across output
        types. Use the `pdf` sub-object for PDF-only options and `png` for
        PNG-only options. Type-specific fields take precedence over the
        shared fields when both apply.
      properties:
        format:
          type: string
          description: Page size preset. Overridden by explicit width/height in the type-specific sub-object.
          enum: [A4, A3, A5, Letter, Legal, Tabloid]
          default: A4
        landscape:
          type: boolean
          description: Render in landscape orientation.
          default: false
        pdf:
          $ref: '#/components/schemas/PdfOptions'
        png:
          $ref: '#/components/schemas/PngOptions'

    PdfOptions:
      type: object
      description: PDF-specific options. All fields optional.
      properties:
        width:
          type: string
          description: Page width with unit (e.g. "8.5in", "210mm"). Overrides format if set with height.
          pattern: '^[0-9]+(\.[0-9]+)?(px|in|cm|mm)$'
        height:
          type: string
          description: Page height with unit. Overrides format if set with width.
          pattern: '^[0-9]+(\.[0-9]+)?(px|in|cm|mm)$'
        printBackground:
          type: boolean
          description: |
            Print CSS backgrounds and colors. Required for designs that
            use background colors on bubbles, headers, etc.
          default: true
        margin:
          $ref: '#/components/schemas/Margin'
        scale:
          type: number
          description: Scale of the rendered content. 1.0 = 100%.
          minimum: 0.1
          maximum: 2.0
          default: 1.0
        preferCSSPageSize:
          type: boolean
          description: |
            If true, CSS @page rules in the HTML take precedence over
            format/width/height options.
          default: false

    PngOptions:
      type: object
      description: PNG-specific options. All fields optional.
      properties:
        width:
          type: integer
          description: |
            Viewport width in CSS pixels. Overrides format if set with height.
            At 96 dpi, A4 width ≈ 794 px and Letter width = 816 px.
          minimum: 1
        height:
          type: integer
          description: |
            Viewport height in CSS pixels, defining the first-page crop boundary.
            Overrides format if set with width.
          minimum: 1

    Margin:
      type: object
      description: Page margins. Each side optional; omitted sides default to "0".
      properties:
        top:
          type: string
          pattern: '^[0-9]+(\.[0-9]+)?(px|in|cm|mm)$'
          example: "12mm"
        bottom:
          type: string
          pattern: '^[0-9]+(\.[0-9]+)?(px|in|cm|mm)$'
          example: "12mm"
        left:
          type: string
          pattern: '^[0-9]+(\.[0-9]+)?(px|in|cm|mm)$'
          example: "10mm"
        right:
          type: string
          pattern: '^[0-9]+(\.[0-9]+)?(px|in|cm|mm)$'
          example: "10mm"

    SvgRenderRequest:
      type: object
      required:
        - svg
        - width
      properties:
        svg:
          type: string
          description: The SVG document. At most 2 MB (UTF-8).
          minLength: 1
        width:
          type: integer
          description: The PNG's width in pixels.
          minimum: 1
          maximum: 4096
        timeoutMs:
          type: integer
          description: Render timeout in milliseconds, capped at 120000.
          default: 30000
          minimum: 1
          maximum: 120000

    MermaidRenderRequest:
      type: object
      required:
        - source
      properties:
        source:
          type: string
          description: The Mermaid source. At most 50 KB (UTF-8).
          minLength: 1
        width:
          type: integer
          description: The PNG's width in pixels. Omitted, twice the diagram's own width, at most 4096.
          minimum: 1
          maximum: 4096
        theme:
          type: string
          description: Mermaid's theme. `dark` is drawn on its own dark background.
          enum: [default, neutral, dark, forest]
          default: default
        timeoutMs:
          type: integer
          description: Render timeout in milliseconds, capped at 120000.
          default: 30000
          minimum: 1
          maximum: 120000

    GraphvizRenderRequest:
      type: object
      required:
        - source
      properties:
        source:
          type: string
          description: The DOT source. At most 50 KB (UTF-8).
          minLength: 1
        width:
          type: integer
          description: The PNG's width in pixels. Omitted, twice the graph's own width, at most 4096.
          minimum: 1
          maximum: 4096
        engine:
          type: string
          description: The Graphviz layout engine.
          enum: [dot, neato, fdp, sfdp, circo, twopi]
          default: dot
        timeoutMs:
          type: integer
          description: Render timeout in milliseconds, capped at 120000.
          default: 30000
          minimum: 1
          maximum: 120000

    Error:
      type: object
      required:
        - error
        - message
      properties:
        error:
          type: string
          description: Stable, machine-readable error code.
          enum:
            - invalid_request
            - html_too_large
            - svg_too_large
            - source_too_large
            - invalid_svg
            - invalid_mermaid
            - invalid_graphviz
            - image_too_large
            - render_timeout
            - render_failed
            - browser_unavailable
            - queue_full
        message:
          type: string
          description: Human-readable description. May change between versions; do not match on this.
          example: "HTML field is required"
        details:
          type: object
          description: Optional structured context (validation errors, etc.). Shape depends on error code.
          additionalProperties: true

    Health:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          enum: [ok, degraded, unhealthy]
        browser:
          type: object
          properties:
            connected:
              type: boolean
            uptimeMs:
              type: integer
              description: Time since the browser instance was launched.
        queue:
          type: object
          properties:
            depth:
              type: integer
              description: Number of renders waiting for a worker.
            inFlight:
              type: integer
              description: Number of renders currently executing.
