openapi: 3.0.3
info:
  title: md2json
  version: "0.1.0"
  description: |
    Markdown to structured JSON converter. One endpoint — send raw Markdown,
    get back typed JSON with frontmatter, GFM tables, tasklists, heading tree,
    sections, links, paragraphs, code blocks.

    Built on the `remark` family (MIT), runs on Cloudflare Workers (no cold starts,
    p95 under 150 ms). Stateless — no database, no upstream calls.
  contact:
    name: md2json
    url: https://md2json-prod.md2json-dev.workers.dev/
  license:
    name: MIT
servers:
  - url: https://md2json-prod.md2json-dev.workers.dev
    description: Production (RapidAPI-proxied)
  - url: https://md2json-prod.md2json-dev.workers.dev
    description: Direct Worker URL (bypassing RapidAPI — blocked in production by X-RapidAPI-Proxy-Secret)
paths:
  /v1/parse:
    post:
      summary: Convert Markdown to structured JSON
      description: |
        Send Markdown as the raw request body (`Content-Type: text/markdown`)
        or as JSON (`Content-Type: application/json`) with `{ "markdown": "...", "options": {...} }`.
        Returns a single envelope with every structural element split out.
      operationId: parseMarkdown
      security:
        - RapidAPIProxy: []
      requestBody:
        required: true
        content:
          text/markdown:
            schema:
              type: string
              description: Raw Markdown content.
              example: |
                ---
                title: Project Alpha
                status: shipping
                ---

                # Project Alpha

                - [x] Ship MVP
                - [ ] Polish docs
          application/json:
            schema:
              type: object
              required: [markdown]
              properties:
                markdown:
                  type: string
                  description: Raw Markdown content.
                  minLength: 1
                  maxLength: 262144
                options:
                  $ref: '#/components/schemas/ParseOptions'
      responses:
        '200':
          description: Parse succeeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParseResult'
        '400':
          $ref: '#/components/responses/InvalidInput'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /healthz:
    get:
      summary: Health check
      description: Public (no auth). Returns service version and current timestamp.
      operationId: healthz
      security: []
      responses:
        '200':
          description: Service is healthy.
          content:
            application/json:
              schema:
                type: object
                required: [ok, version, time]
                properties:
                  ok:
                    type: boolean
                    example: true
                  version:
                    type: string
                    example: "0.1.0"
                  time:
                    type: integer
                    format: int64
                    description: Current epoch milliseconds.
  /notify:
    post:
      summary: Drop an email for launch notification
      description: Public (no auth). Stores the email in a Cloudflare Workers KV namespace for one-time launch communication.
      operationId: notify
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                _subject:
                  type: string
                  description: Optional subject line (used for internal logging only).
      responses:
        '200':
          description: Email recorded.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
        '400':
          $ref: '#/components/responses/InvalidInput'
components:
  securitySchemes:
    RapidAPIProxy:
      type: apiKey
      in: header
      name: X-RapidAPI-Proxy-Secret
      description: RapidAPI injects this header after validating the subscriber's X-RapidAPI-Key. The Worker compares it to a secret set via `wrangler secret put`.
  schemas:
    ParseOptions:
      type: object
      properties:
        gfm:
          type: boolean
          default: true
          description: Enable GitHub-Flavored Markdown (tables, tasklists, strikethrough, autolinks).
        frontmatter:
          type: boolean
          default: true
          description: Parse YAML/TOML `---` metadata blocks via gray-matter.
        sections:
          type: boolean
          default: true
          description: Build hierarchy-aware `sections` tree from heading depth.
        includeAst:
          type: boolean
          default: false
          description: Include the full mdast `ast` field (Pro tier and above).
    ParseResult:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/ParseData'
    ParseData:
      type: object
      properties:
        title:
          type: string
          nullable: true
          description: First H1 text, or null.
        frontmatter:
          type: object
          nullable: true
          description: Parsed YAML/TOML metadata as a plain object.
        headings:
          type: array
          items:
            $ref: '#/components/schemas/ParsedHeading'
        sections:
          type: array
          description: Hierarchy-aware tree built from heading depth.
          items:
            $ref: '#/components/schemas/ParsedSection'
        tables:
          type: array
          items:
            $ref: '#/components/schemas/ParsedTable'
        tasklists:
          type: array
          items:
            $ref: '#/components/schemas/ParsedTasklist'
        lists:
          type: array
          items:
            $ref: '#/components/schemas/ParsedList'
        codeBlocks:
          type: array
          items:
            $ref: '#/components/schemas/ParsedCodeBlock'
        links:
          type: array
          items:
            $ref: '#/components/schemas/ParsedLink'
        paragraphs:
          type: array
          items:
            type: string
        ast:
          description: "Full mdast tree (only when `includeAst: true`)."
          nullable: true
    ParsedHeading:
      type: object
      required: [level, text, id]
      properties:
        level: { type: integer, minimum: 1, maximum: 6 }
        text: { type: string }
        id: { type: string, nullable: true, description: GitHub-style slug. }
    ParsedSection:
      type: object
      properties:
        heading:
          $ref: '#/components/schemas/ParsedHeading'
        children:
          type: array
          items:
            $ref: '#/components/schemas/ParsedSection'
        content:
          type: array
          description: Leaf mdast nodes (paragraphs, lists, tables, code) under this section.
          items: {}
    ParsedTable:
      type: object
      required: [headers, rows]
      properties:
        headers:
          type: array
          items: { type: string }
        rows:
          type: array
          items:
            type: object
            additionalProperties: { type: string }
    ParsedTasklist:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ParsedTaskItem'
    ParsedTaskItem:
      type: object
      required: [text, checked]
      properties:
        text: { type: string }
        checked: { type: boolean }
    ParsedList:
      type: object
      required: [ordered, items]
      properties:
        ordered: { type: boolean }
        items:
          type: array
          items: { type: string }
    ParsedCodeBlock:
      type: object
      required: [lang, value]
      properties:
        lang: { type: string, nullable: true }
        value: { type: string }
    ParsedLink:
      type: object
      required: [text, url]
      properties:
        text: { type: string }
        url: { type: string, format: uri }
    Error:
      type: object
      required: [error, message]
      properties:
        error:
          type: string
          enum: [invalid_input, oversized_input, unauthorized, unprocessable, rate_limited, internal]
        message: { type: string }
        retryAfter:
          type: integer
          description: Seconds to wait before retry (only present on `rate_limited`).
  responses:
    InvalidInput:
      description: Body empty, invalid JSON, non-UTF-8, oversized (>256 KB), or invalid notify email.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: Missing or wrong `X-RapidAPI-Proxy-Secret` (production only).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Per-key or per-IP rate limit exceeded.
      headers:
        Retry-After:
          schema: { type: integer }
          description: Seconds to wait.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Internal:
      description: Unhandled server error (details in Workers observability logs).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
