openapi: 3.1.0
info:
  title: n8n-flow-auditor
  description: |
    Deterministic n8n workflow validator on Cloudflare Workers, exposed over MCP and REST.
    Ports `@n8n/workflow-sdk` to plain JS with a multi-version catalog (66 types / 245 versions)
    and 21 validators. Zero LLM invocation — designed as ground truth for AI agents.

    Service URL: https://n8n-auditor.automators.work
  version: 0.30.0
  license:
    name: Proprietary
servers:
  - url: https://n8n-auditor.automators.work
    description: Production
tags:
  - name: health
    description: Worker status and metadata
  - name: validation
    description: Workflow validation
  - name: n8n
    description: Integration with the user's n8n instance
  - name: mcp
    description: Model Context Protocol (JSON-RPC 2.0 over Streamable HTTP)
paths:
  /health:
    get:
      tags: [health]
      summary: Worker status
      description: Returns version, catalog size, and runtime configuration.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Health'
              example:
                ok: true
                service: n8n-flow-auditor
                version: v21-sticky-analyzer
                catalogTypes: 66
                catalogVersions: 245
                validators: 21
                mcpEndpoint: /mcp
                mcpTools: 5
                cacheVersion: v21
                maxBodyBytes: 1048576
                n8nTimeoutMs: 10000
                authRequired: false
                authProviders: []
                rateLimit: { enabled: true, limit: 60, period: 60, impl: in-memory-per-isolate }
                llm: false

  /validate:
    post:
      tags: [validation]
      summary: Validate an inline workflow
      description: |
        Validates the workflow JSON against the 21 validators and the multi-version catalog.
        Zero LLM invocation. Deterministic — same input → same output.
        Cached by input hash (10-min TTL).
      security:
        - apiKey: []
        - {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValidateRequest'
      responses:
        '200':
          description: Validation report
          headers:
            x-cache:
              schema: { type: string, enum: [hit, miss] }
              description: Indicates whether the response came from the edge cache
            x-rate-limit-remaining:
              schema: { type: string }
              description: Requests remaining in the current window
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationReport'
            text/markdown:
              schema:
                type: string
                description: Markdown report (when format=markdown)
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'

  /validate/by-id:
    post:
      tags: [validation, n8n]
      summary: Fetch + validate by ID
      description: |
        Downloads the workflow from the n8n configured on the worker (env N8N_BASE_URL/N8N_API_KEY)
        and validates it. **Not cached** — the remote workflow may change between calls.
      security:
        - apiKey: []
        - {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [workflowId]
              properties:
                workflowId: { type: string, description: Workflow ID in n8n }
                format: { type: string, enum: [json, markdown], default: json }
                options: { $ref: '#/components/schemas/ValidationOptions' }
      responses:
        '200':
          description: Validation report
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationReport'
        '404':
          description: Workflow does not exist in the configured n8n
        '502':
          description: n8n is unreachable
        '503':
          description: n8n is not configured on the worker (missing env vars)
        '504':
          description: Timeout while fetching from the user's n8n (>10s)

  /n8n/workflows:
    get:
      tags: [n8n]
      summary: List workflows from n8n
      description: Pulls the list of workflows from the n8n configured on the worker.
      security:
        - apiKey: []
        - {}
      parameters:
        - in: query
          name: limit
          schema: { type: integer, default: 50, minimum: 1, maximum: 250 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  workflows:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        name: { type: string }
                        active: { type: boolean }
                        tags: { type: array, items: { type: string } }
                        updatedAt: { type: string, format: date-time }
        '503':
          description: n8n is not configured on the worker

  /mcp:
    post:
      tags: [mcp]
      summary: MCP endpoint (Model Context Protocol)
      description: |
        Streamable HTTP transport, JSON-RPC 2.0, protocol `2025-06-18`.

        Supported methods: `initialize`, `tools/list`, `tools/call`, `ping`.

        Tools (5): `validate_workflow`, `validate_workflow_by_id`, `list_known_node_types`,
        `get_node_schema`, `analyze_sticky_notes`.

        The MCP client discovers the OAuth flow automatically via
        `WWW-Authenticate` + `/.well-known/oauth-protected-resource` when it receives 401.
      security:
        - apiKey: []
        - cfAccess: []
        - {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/JsonRpcRequest'
            examples:
              initialize:
                summary: Handshake
                value:
                  jsonrpc: '2.0'
                  id: 1
                  method: initialize
                  params:
                    protocolVersion: '2025-06-18'
                    capabilities: {}
                    clientInfo: { name: my-client, version: '1.0' }
              toolsList:
                summary: List tools
                value:
                  jsonrpc: '2.0'
                  id: 2
                  method: tools/list
              validateWorkflow:
                summary: Validate inline workflow
                value:
                  jsonrpc: '2.0'
                  id: 3
                  method: tools/call
                  params:
                    name: validate_workflow
                    arguments:
                      workflow:
                        name: example
                        nodes: []
                        connections: {}
              analyzeStickyNotes:
                summary: Analyze sticky notes
                value:
                  jsonrpc: '2.0'
                  id: 4
                  method: tools/call
                  params:
                    name: analyze_sticky_notes
                    arguments:
                      workflow:
                        name: example
                        nodes: []
                        connections: {}
      responses:
        '200':
          description: JSON-RPC response
          headers:
            x-mcp-cache:
              schema: { type: string, enum: [hit, miss] }
            x-mcp-auth:
              schema: { type: string, enum: [api_key, cf_access] }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JsonRpcResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: Bearer of the admin API_KEY (configured as a worker secret).
    cfAccess:
      type: apiKey
      in: header
      name: Cf-Access-Jwt-Assertion
      description: |
        JWT signed by Cloudflare Access. Legacy — for deployments with CF Zero Trust.
        The primary auth method is self-hosted OAuth 2.1 (see `/.well-known/oauth-authorization-server`).

  schemas:
    Health:
      type: object
      properties:
        ok: { type: boolean }
        service: { type: string }
        version: { type: string, description: Worker version (e.g. v30-custom-domain) }
        catalogTypes: { type: integer }
        catalogVersions: { type: integer }
        validators: { type: integer }
        mcpEndpoint: { type: string }
        mcpTools: { type: integer }
        cacheVersion: { type: string }
        maxBodyBytes: { type: integer }
        n8nTimeoutMs: { type: integer }
        authRequired: { type: boolean }
        authProviders:
          type: array
          items: { type: string, enum: [api_key, cf_access] }
        rateLimit:
          oneOf:
            - { type: object, properties: { enabled: { const: false } } }
            - type: object
              properties:
                enabled: { const: true }
                limit: { type: integer }
                period: { type: integer }
                impl: { type: string }
        llm: { type: boolean, description: Always false; this service never invokes any LLM }

    ValidateRequest:
      type: object
      required: [workflow]
      properties:
        workflow:
          type: object
          description: WorkflowJSON exported from n8n (max 50 nodes).
          properties:
            name: { type: string }
            nodes: { type: array, items: { type: object } }
            connections: { type: object }
        options: { $ref: '#/components/schemas/ValidationOptions' }
        format: { type: string, enum: [json, markdown], default: json }

    ValidationOptions:
      type: object
      properties:
        allowNoTrigger: { type: boolean, default: false, description: Suppresses the MISSING_TRIGGER warning }
        allowDisconnectedNodes: { type: boolean, default: false, description: Suppresses DISCONNECTED_NODE warnings }

    ValidationReport:
      type: object
      properties:
        workflowName: { type: string }
        generatedAt: { type: string, format: date-time }
        valid: { type: boolean }
        errors:
          type: array
          items: { $ref: '#/components/schemas/ValidationIssue' }
        warnings:
          type: array
          items: { $ref: '#/components/schemas/ValidationIssue' }

    ValidationIssue:
      type: object
      required: [code, severity, message]
      properties:
        code:
          type: string
          enum:
            - NO_NODES
            - MISSING_TRIGGER
            - DISCONNECTED_NODE
            - INVALID_CONNECTION
            - MERGE_SINGLE_INPUT
            - SUBNODE_NOT_CONNECTED
            - SET_CREDENTIAL_FIELD
            - FILTER_MISSING_OPTIONS
            - FILTER_MISSING_CONDITIONS
            - FILTER_MISSING_COMBINATOR
            - SWITCH_WRONG_RULES_KEY
            - HARDCODED_CREDENTIALS
            - AGENT_STATIC_PROMPT
            - AGENT_NO_SYSTEM_MESSAGE
            - INVALID_DATE_METHOD
            - MISSING_EXPRESSION_PREFIX
            - INVALID_EXPRESSION_PATH
            - PARTIAL_EXPRESSION_PATH
            - FROM_AI_IN_NON_TOOL
            - TOOL_NO_PARAMETERS
            - INVALID_INPUT_INDEX
            - MISSING_REQUIRED_INPUT
            - UNSUPPORTED_SUBNODE_INPUT
            - MISSING_PARAMETER
            - INVALID_PARAMETER
            - MAX_NODES_EXCEEDED
        severity: { type: string, enum: [error, warning] }
        message: { type: string }
        nodeName: { type: string, description: Name of the associated node }
        parameterPath: { type: string, description: Parameter path (e.g. "options.systemMessage") }

    JsonRpcRequest:
      type: object
      required: [jsonrpc, method]
      properties:
        jsonrpc: { type: string, const: '2.0' }
        id: { oneOf: [{ type: integer }, { type: string }, { type: 'null' }] }
        method: { type: string, enum: [initialize, tools/list, tools/call, ping, notifications/initialized, notifications/cancelled] }
        params: { type: object }

    JsonRpcResponse:
      type: object
      required: [jsonrpc, id]
      properties:
        jsonrpc: { type: string, const: '2.0' }
        id: { oneOf: [{ type: integer }, { type: string }, { type: 'null' }] }
        result: { type: object }
        error:
          type: object
          properties:
            code: { type: integer }
            message: { type: string }
            data: { type: object }

    ErrorResponse:
      type: object
      properties:
        error: { type: string }
        detail: { type: string }
        message: { type: string }

  responses:
    BadRequest:
      description: Invalid body (malformed JSON or missing fields)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    Unauthorized:
      description: Auth required but missing or invalid
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
      headers:
        WWW-Authenticate:
          schema: { type: string }
    PayloadTooLarge:
      description: Body exceeds 1 MB
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    RateLimited:
      description: Exceeded the requests-per-window limit
      headers:
        Retry-After:
          schema: { type: integer, description: Seconds until the window resets }
        x-rate-limit-key:
          schema: { type: string }
        x-rate-limit-limit:
          schema: { type: integer }
        x-rate-limit-remaining:
          schema: { type: integer }
        x-rate-limit-reset:
          schema: { type: integer, description: Unix timestamp of the reset }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
