{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://raw.githubusercontent.com/lopatnov/conduit/main/schema/conduit.schema.json",
  "title": "Conduit configuration",
  "description": "Configuration schema for the Conduit reverse proxy. Accepts a full object, an array of sites, or a single-site object.",
  "oneOf": [
    { "$ref": "#/$defs/AppConfig" },
    { "type": "array", "items": { "$ref": "#/$defs/SiteConfig" } },
    { "$ref": "#/$defs/SiteConfig" }
  ],
  "$defs": {
    "AppConfig": {
      "type": "object",
      "required": ["sites"],
      "properties": {
        "version": { "type": "integer", "minimum": 1, "description": "Config schema version." },
        "global": { "$ref": "#/$defs/GlobalConfig" },
        "sites": { "type": "array", "items": { "$ref": "#/$defs/SiteConfig" } }
      }
    },
    "GlobalConfig": {
      "type": "object",
      "properties": {
        "workers":             { "type": "integer", "minimum": 1 },
        "backlog":             { "type": "integer", "minimum": 1 },
        "shutdownTimeoutSecs": { "type": "integer", "minimum": 0 },
        "admin": {
          "type": "object",
          "properties": {
            "bind":  { "type": "string", "examples": ["127.0.0.1:2019"] },
            "token": { "type": "string", "minLength": 16, "description": "Bearer token for Admin API auth. When set, all requests must include Authorization: Bearer <token>. Minimum 16 characters; 32+ recommended." }
          }
        },
        "otlp": {
          "type": "object",
          "required": ["endpoint"],
          "description": "OpenTelemetry OTLP tracing. Requires --features otlp.",
          "properties": {
            "endpoint":    { "type": "string", "format": "uri", "examples": ["http://localhost:4317", "https://api.honeycomb.io:443"] },
            "serviceName": { "type": "string", "default": "conduit" },
            "sampleRate":  { "type": "number", "minimum": 0, "maximum": 1, "default": 1.0 },
            "timeoutMs":   { "type": "integer", "minimum": 1, "default": 5000 }
          }
        }
      }
    },
    "SiteConfig": {
      "type": "object",
      "properties": {
        "host": { "type": "string", "description": "Virtual host name. '*' matches any host." },
        "port": { "type": "integer", "minimum": 1, "maximum": 65535 },
        "tls":  { "$ref": "#/$defs/TlsConfig" },
        "http2": {
          "type": "object",
          "properties": {
            "maxConcurrentStreams": { "type": "integer" },
            "initialWindowSize":   { "type": "integer" }
          }
        },
        "logging":        { "$ref": "#/$defs/LoggingConfig" },
        "compression":    { "$ref": "#/$defs/CompressionConfig" },
        "responseTime":   { "$ref": "#/$defs/BoolOrObject" },
        "securityHeaders":{ "$ref": "#/$defs/SecurityHeadersConfig" },
        "cors":           { "$ref": "#/$defs/CorsConfig" },
        "hotReload":      { "$ref": "#/$defs/HotReloadConfig" },
        "healthCheck":    { "$ref": "#/$defs/HealthCheckConfig" },
        "rateLimit": {
          "type": "object",
          "required": ["windowSecs", "limit"],
          "properties": {
            "windowSecs": { "type": "integer", "minimum": 1 },
            "limit":      { "type": "integer", "minimum": 1 },
            "algorithm":  { "type": "string", "enum": ["token-bucket"] },
            "keyBy":      { "type": "string", "enum": ["ip", "header"] },
            "skipPaths":  { "type": "array", "items": { "type": "string" } },
            "store": {
              "type": "string",
              "description": "Backend for the rate-limiter state. \"memory\" (default) uses an in-process DashMap; a Redis URL (e.g. \"redis://127.0.0.1:6379\") uses a Redis fixed-window counter with automatic fallback to memory on failure.",
              "examples": ["memory", "redis://127.0.0.1:6379"]
            }
          }
        },
        "basicAuth": {
          "type": "object",
          "required": ["users"],
          "properties": {
            "users":     { "type": "object", "additionalProperties": { "type": "string" } },
            "challenge": { "type": "boolean" },
            "realm":     { "type": "string" },
            "skipPaths": { "type": "array", "items": { "type": "string" } }
          }
        },
        "apiKey": {
          "type": "object",
          "required": ["keys"],
          "properties": {
            "keys":      { "type": "array", "items": { "type": "string" } },
            "header":    { "type": "string" },
            "skipPaths": { "type": "array", "items": { "type": "string" } }
          }
        },
        "ipFilter": {
          "type": "object",
          "properties": {
            "allow":      { "type": "array", "items": { "type": "string" } },
            "deny":       { "type": "array", "items": { "type": "string" } },
            "trustProxy": { "type": "boolean" }
          }
        },
        "limits": {
          "type": "object",
          "properties": {
            "maxBodyBytes":          { "type": "integer", "minimum": 0 },
            "maxHeaderBytes":        { "type": "integer", "minimum": 0 },
            "timeoutSecs":           { "type": "integer", "minimum": 0 },
            "maxInflightRequests":   { "type": "integer", "minimum": 1 },
            "maxBodyBufferBytes":    { "type": "integer", "minimum": 0 },
            "keepaliveRequestLimit": { "type": "integer", "minimum": 1 },
            "priorityThreshold":     { "type": "number", "minimum": 0, "maximum": 1,
                                       "description": "Fraction of maxInflightRequests at which low-priority routes (priority < 50) are shed (0.0–1.0). Default 0.8." },
            "maxRequestHeaders":     { "type": "integer", "minimum": 1,
                                       "description": "Max number of request headers. Requests exceeding this receive 431." },
            "maxConnectionsPerIp":   { "type": "integer", "minimum": 1,
                                       "description": "Max simultaneous in-flight requests from one client IP. Excess requests receive 429." },
            "minUploadRateBytesPerSec": { "type": "integer", "minimum": 1,
                                          "description": "Minimum upload rate in bytes per second. Clients uploading slower than this receive 408 Request Timeout (slow-loris upload defence)." }
          }
        },
        "headers":   { "type": "object", "additionalProperties": { "type": "string" } },
        "redirects": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["from", "to"],
            "properties": {
              "from":   { "type": "string" },
              "to":     { "type": "string" },
              "status": { "type": "integer", "enum": [301, 302, 307, 308] }
            }
          }
        },
        "static": { "$ref": "#/$defs/StaticConfig" },
        "staticOptions": {
          "type": "object",
          "properties": {
            "etag":          { "type": "boolean" },
            "lastModified":  { "type": "boolean" },
            "maxAge":        { "type": "string", "examples": ["1d", "30m", "1h"] },
            "index":         { "type": "array", "items": { "type": "string" } },
            "dotFiles":      { "type": "string", "enum": ["ignore", "allow", "deny"] },
            "preCompressed": { "type": "boolean" }
          }
        },
        "proxy":   { "$ref": "#/$defs/ProxyConfig" },
        "routes": {
          "type": "array",
          "description": "Explicit route table with match criteria. Matched in order; first match wins. Backward-compatible with top-level 'proxy' and 'static'.",
          "items": { "$ref": "#/$defs/RouteConfig" }
        },
        "upload": {
          "type": "object",
          "required": ["path", "dir"],
          "properties": {
            "path":               { "type": "string" },
            "dir":                { "type": "string" },
            "maxFileSizeBytes":   { "type": "integer", "minimum": 0 },
            "maxTotalSizeBytes":  { "type": "integer", "minimum": 0 },
            "maxFiles":           { "type": "integer", "minimum": 1 },
            "allowedMimeTypes":   { "type": "array", "items": { "type": "string" } },
            "fieldName":          { "type": "string" }
          }
        },
        "metrics": {
          "type": "object",
          "properties": {
            "path":  { "type": "string" },
            "token": { "type": "string" }
          }
        },
        "fallback": { "$ref": "#/$defs/FallbackConfig" },
        "consumers": { "$ref": "#/$defs/ConsumersConfig" },
        "outlierDetection": { "$ref": "#/$defs/OutlierDetectionConfig" },
        "maskErrors": { "type": "boolean", "description": "Replace upstream 5xx bodies with a generic JSON error." },
        "serverTiming": { "type": "boolean", "description": "Inject a W3C Server-Timing response header with total and upstream durations." },
        "allowDuplicateChunked": { "type": "boolean", "description": "Allow duplicate Transfer-Encoding: chunked headers from upstream (default: false, deduplicate)." },
        "faultInjection": { "$ref": "#/$defs/FaultInjectionConfig" },
        "jwtAuth": { "$ref": "#/$defs/JwtAuthConfig" },
        "forwardAuth": { "$ref": "#/$defs/ForwardAuthConfig" },
        "requestTransform": { "$ref": "#/$defs/HeaderTransformConfig" },
        "responseTransform": { "$ref": "#/$defs/HeaderTransformConfig" },
        "middleware": {
          "type": "array",
          "description": "Ordered middleware chain executed after built-in filters.",
          "items": {
            "type": "object",
            "required": ["type"],
            "properties": {
              "type": {
                "type": "string",
                "enum": ["script", "ipFilter", "rateLimit", "auth", "headers"],
                "description": "Middleware type. Use \"script\" for Rhai scripts (Phase 4)."
              },
              "config": {
                "description": "Type-specific configuration object (used by built-in types)."
              },
              "path": {
                "type": "string",
                "description": "Path to the Rhai script file. Required when type is \"script\"."
              }
            },
            "if": { "properties": { "type": { "const": "script" } }, "required": ["type"] },
            "then": { "required": ["path"] }
          }
        }
      }
    },
    "TlsConfig": {
      "type": "object",
      "properties": {
        "cert":             { "type": "string" },
        "key":              { "type": "string" },
        "ca":               { "type": "string" },
        "httpRedirectPort": { "type": "integer", "minimum": 1, "maximum": 65535 },
        "versions":         { "type": "array", "items": { "type": "string" } },
        "ciphers":          { "type": "array", "items": { "type": "string" } },
        "acme": {
          "type": "object",
          "required": ["email"],
          "properties": {
            "email":     { "type": "string", "format": "email" },
            "directory": { "type": "string" },
            "storage":   { "type": "string" },
            "challenge": { "type": "string", "enum": ["http-01", "tls-alpn-01"] }
          }
        }
      }
    },
    "LoggingConfig": {
      "oneOf": [
        { "type": "boolean" },
        { "type": "string", "enum": ["dev", "json", "combined", "common", "short"] },
        {
          "type": "object",
          "properties": {
            "format":    { "type": "string", "enum": ["dev", "json", "combined", "common", "short"] },
            "file":      { "type": "string" },
            "skipPaths": { "type": "array", "items": { "type": "string" }, "description": "Paths to suppress from access logs." }
          }
        }
      ]
    },
    "CompressionConfig": {
      "oneOf": [
        { "type": "boolean" },
        {
          "type": "object",
          "properties": {
            "algorithms": { "type": "array", "items": { "type": "string", "enum": ["br", "gzip", "deflate"] } },
            "level":      { "type": "integer", "minimum": 0, "maximum": 9 },
            "minBytes":   { "type": "integer", "minimum": 0 }
          }
        }
      ]
    },
    "SecurityHeadersConfig": {
      "oneOf": [
        { "type": "boolean" },
        {
          "type": "object",
          "properties": {
            "hstsMaxAgeSecs":        { "type": "integer", "minimum": 0, "description": "max-age for Strict-Transport-Security (seconds). 0 disables HSTS." },
            "hstsIncludeSubDomains": { "type": "boolean", "description": "Add includeSubDomains to HSTS. Default: true when hstsMaxAgeSecs is set." },
            "hstsPreload":           { "type": "boolean", "description": "Add preload directive to HSTS for preload list submission." },
            "csp":                   { "type": "string", "description": "Content-Security-Policy header value." },
            "xFrameOptions":         { "type": "string", "enum": ["DENY", "SAMEORIGIN"], "description": "X-Frame-Options value." },
            "referrerPolicy":        { "type": "string", "description": "Referrer-Policy header value." },
            "permissionsPolicy":     { "type": "string", "description": "Permissions-Policy header value (replaces Feature-Policy). E.g. 'geolocation=(), microphone=()'." },
            "allowedHosts":          { "type": "array", "items": { "type": "string" }, "description": "Allowlist of Host header values. Requests with disallowed Host return 400. Use '*' to allow any." }
          }
        }
      ]
    },
    "BoolOrObject": {
      "oneOf": [
        { "type": "boolean" },
        { "type": "object" }
      ]
    },
    "CorsConfig": {
      "oneOf": [
        { "type": "boolean" },
        {
          "type": "object",
          "properties": {
            "origins":        { "type": "array", "items": { "type": "string" } },
            "methods":        { "type": "array", "items": { "type": "string" } },
            "allowedHeaders": { "type": "array", "items": { "type": "string" } },
            "credentials":    { "type": "boolean" },
            "maxAgeSecs":     { "type": "integer", "minimum": 0 }
          }
        }
      ]
    },
    "HotReloadConfig": {
      "oneOf": [
        { "type": "boolean" },
        {
          "type": "object",
          "properties": {
            "extensions": { "type": "array", "items": { "type": "string" } }
          }
        }
      ]
    },
    "HealthCheckConfig": {
      "oneOf": [
        { "type": "boolean" },
        {
          "type": "object",
          "properties": {
            "path":             { "type": "string" },
            "includeUpstreams": { "type": "boolean" }
          }
        }
      ]
    },
    "StaticConfig": {
      "oneOf": [
        { "type": "string" },
        { "type": "array", "items": { "type": "string" } },
        { "type": "object", "additionalProperties": { "type": "string" } }
      ]
    },
    "ProxyConfig": {
      "oneOf": [
        { "type": "string", "description": "Single upstream URL." },
        {
          "type": "object",
          "description": "Route map: path prefix → upstream config.",
          "additionalProperties": { "$ref": "#/$defs/ProxyRouteTarget" }
        }
      ]
    },
    "ProxyRouteTarget": {
      "oneOf": [
        { "type": "string", "description": "Single upstream URL shorthand." },
        { "type": "array", "items": { "type": "string" }, "description": "Round-robin shorthand." },
        { "$ref": "#/$defs/ProxyRouteConfig" }
      ]
    },
    "ProxyRouteConfig": {
      "type": "object",
      "properties": {
        "targets": {
          "type": "array",
          "items": { "$ref": "#/$defs/ProxyTarget" },
          "description": "Upstream targets. Required unless 'groups' is set."
        },
        "strategy": {
          "type": "string",
          "enum": [
            "round-robin", "weighted-round-robin", "random",
            "least-conn", "least-response-time", "ip-hash", "consistent-hash"
          ],
          "description": "Load-balancing strategy for flat targets."
        },
        "groups": {
          "type": "array",
          "items": { "$ref": "#/$defs/UpstreamGroup" },
          "description": "Two-level load balancing: outer groupStrategy picks a group, inner strategy picks a target within that group."
        },
        "groupStrategy": {
          "type": "string",
          "enum": [
            "round-robin", "weighted-round-robin", "random",
            "least-conn", "least-response-time", "ip-hash", "consistent-hash"
          ],
          "description": "Strategy used to select among groups when 'groups' is configured."
        },
        "rewrite": {
          "type": "array",
          "items": { "$ref": "#/$defs/RewriteRule" },
          "description": "Path rewrite rules applied after strip_prefix. First matching rule wins."
        },
        "hashKey":     { "type": "string" },
        "http2":       { "type": "boolean" },
        "stripPrefix": { "type": "boolean" },
        "timeout": {
          "type": "object",
          "properties": {
            "connectMs":    { "type": "integer", "minimum": 0 },
            "sendMs":       { "type": "integer", "minimum": 0 },
            "readMs":       { "type": "integer", "minimum": 0 },
            "firstByteMs":  { "type": "integer", "minimum": 0,
                              "description": "Max ms to wait for the first byte of the upstream response. Overrides readMs." }
          }
        },
        "pool": {
          "type": "object",
          "properties": {
            "maxIdle":         { "type": "integer", "minimum": 0 },
            "idleTimeoutSecs": { "type": "integer", "minimum": 0 }
          }
        },
        "retry": {
          "type": "object",
          "required": ["attempts", "conditions"],
          "properties": {
            "attempts":       { "type": "integer", "minimum": 1 },
            "conditions":     { "type": "array", "items": { "type": "string", "enum": ["connection_error", "5xx", "timeout"] } },
            "backoffMs":      { "type": "integer", "minimum": 0 },
            "budgetPercent":  { "type": "number", "minimum": 0, "maximum": 100, "description": "Retry budget: max % of active requests that may be retries." }
          }
        },
        "healthCheck": {
          "type": "object",
          "properties": {
            "path":               { "type": "string" },
            "intervalSecs":       { "type": "integer", "minimum": 1 },
            "unhealthyThreshold": { "type": "integer", "minimum": 1 },
            "healthyThreshold":   { "type": "integer", "minimum": 1 },
            "slowStartSecs":      { "type": "integer", "minimum": 0, "description": "Slow-start ramp-up window in seconds after recovery." },
            "maxConnectionsPerUpstream": { "type": "integer", "minimum": 1, "description": "Circuit breaker: max concurrent connections per upstream. Returns 503 when all upstreams are at this limit." }
          }
        },
        "mirror": { "type": "string", "format": "uri", "description": "Fire-and-forget mirror URL. V1: headers only." },
        "upstreamTls": {
          "type": "object",
          "properties": {
            "verify":     { "type": "boolean", "description": "Verify upstream TLS certificate (default true)." },
            "serverName": { "type": "string", "description": "Override SNI hostname for cert verification." }
          }
        },
        "rateLimit": {
          "type": "object",
          "required": ["windowSecs", "limit"],
          "properties": {
            "windowSecs": { "type": "integer", "minimum": 1 },
            "limit":      { "type": "integer", "minimum": 1 },
            "keyBy":      { "type": "string" },
            "skipPaths":  { "type": "array", "items": { "type": "string" } }
          },
          "description": "Per-route rate limit (in addition to site-level rateLimit)."
        },
        "priority": {
          "type": "integer",
          "minimum": 0,
          "maximum": 100,
          "description": "Route priority for load shedding (0=lowest, 100=highest, default 50). When the site exceeds limits.priorityThreshold, requests to routes with priority < 50 receive 503 Load Shedding."
        },
        "websocket": {
          "type": "boolean",
          "description": "Allow WebSocket upgrades (101 Switching Protocols) on this route. Default false — unexpected upgrades are rejected with 502."
        },
        "sticky": {
          "type": "object",
          "required": ["cookie"],
          "properties": {
            "cookie": { "type": "string", "description": "Cookie name used for sticky-session routing." },
            "secret": { "type": "string", "minLength": 1, "description": "HMAC-SHA256 secret. When set, Conduit signs the upstream URL into the cookie and verifies it on every request, preventing forgery. Use an env-var reference ($MY_SECRET). Must be non-empty." },
            "strict": { "type": "boolean", "description": "When true, return 503 if the pinned upstream is unhealthy instead of failing over to another backend. Default false." }
          },
          "description": "Sticky-session configuration. Routes the same client to the same upstream using a cookie."
        },
        "cache": {
          "type": "object",
          "required": ["store"],
          "properties": {
            "store":                    { "type": "string" },
            "maxSizeMb":                { "type": "integer", "minimum": 1 },
            "ttlSecs":                  { "type": "integer", "minimum": 0 },
            "staleWhileRevalidateSecs": { "type": "integer", "minimum": 0, "description": "Serve stale while refreshing in background (RFC 5861)." },
            "staleIfErrorSecs":         { "type": "integer", "minimum": 0, "description": "Serve stale when upstream returns 5xx, including after retries are exhausted (RFC 5861)." },
            "earlyRefreshSecs":         { "type": "integer", "minimum": 0, "description": "Spawn a background refresh when remaining TTL drops below this value. Clients always see fresh content." },
            "varyHeaders":              { "type": "array", "items": { "type": "string" } },
            "skipPaths":                { "type": "array", "items": { "type": "string" } },
            "skipIfCookie":             { "type": "boolean" },
            "methods":                  { "type": "array", "items": { "type": "string" } }
          }
        }
      }
    },
    "UpstreamGroup": {
      "type": "object",
      "required": ["name", "targets"],
      "properties": {
        "name":     { "type": "string", "description": "Logical group name for identification." },
        "targets":  { "type": "array", "items": { "$ref": "#/$defs/ProxyTarget" }, "minItems": 1 },
        "strategy": {
          "type": "string",
          "enum": [
            "round-robin", "weighted-round-robin", "random",
            "least-conn", "least-response-time", "ip-hash", "consistent-hash"
          ],
          "description": "Intra-group load-balancing strategy."
        }
      }
    },
    "RewriteRule": {
      "type": "object",
      "required": ["from", "to"],
      "properties": {
        "from": { "type": "string", "description": "Regex pattern matched against the request path." },
        "to":   { "type": "string", "description": "Replacement string (supports capture groups $1, $2, ...)." }
      }
    },
    "ProxyTarget": {
      "oneOf": [
        { "type": "string", "description": "Upstream URL." },
        {
          "type": "object",
          "required": ["url", "weight"],
          "properties": {
            "url":    { "type": "string" },
            "weight": { "type": "integer", "minimum": 1 }
          }
        }
      ]
    },
    "FallbackConfig": {
      "type": "object",
      "properties": {
        "status":  { "type": "integer" },
        "body":    {},
        "file":    { "type": "string" },
        "headers": { "type": "object", "additionalProperties": { "type": "string" } },
        "byAccept": {
          "type": "object",
          "additionalProperties": {
            "type": "object",
            "properties": {
              "status":  { "type": "integer" },
              "body":    {},
              "file":    { "type": "string" },
              "headers": { "type": "object", "additionalProperties": { "type": "string" } }
            }
          }
        }
      }
    },
    "RouteConfig": {
      "type": "object",
      "required": ["match"],
      "properties": {
        "match": { "$ref": "#/$defs/MatchConfig" },
        "proxy":  { "$ref": "#/$defs/ProxyRouteTarget" },
        "static": { "$ref": "#/$defs/StaticConfig" }
      },
      "anyOf": [
        { "required": ["proxy"] },
        { "required": ["static"] }
      ],
      "description": "A single route rule: match criteria → proxy or static handler. At least one of proxy or static must be specified."
    },
    "MatchConfig": {
      "type": "object",
      "description": "Criteria that must all be satisfied for a route to fire. Absent fields match anything.",
      "properties": {
        "path": {
          "type": "string",
          "description": "Glob path pattern. '*' matches within a segment; '**' matches any depth.",
          "examples": ["/api/**", "/blog/*", "/health"]
        },
        "method": {
          "type": "array",
          "items": { "type": "string" },
          "description": "HTTP methods (case-insensitive). Examples: [\"GET\"], [\"POST\", \"PUT\"]."
        },
        "headers": {
          "type": "object",
          "additionalProperties": { "type": "string" },
          "description": "Request header values that must be present and match (exact or regex)."
        },
        "query": {
          "type": "object",
          "additionalProperties": { "type": "string" },
          "description": "Query parameter values that must be present and match (exact or regex)."
        }
      }
    },
    "OutlierDetectionConfig": {
      "type": "object",
      "description": "Passive health checking via consecutive 5xx tracking.",
      "properties": {
        "consecutive5xx":       { "type": "integer", "minimum": 1, "default": 5, "description": "Consecutive 5xx responses before ejection." },
        "baseEjectionTimeSecs": { "type": "integer", "minimum": 1, "default": 30 },
        "maxEjectionTimeSecs":  { "type": "integer", "minimum": 1, "default": 300 },
        "maxEjectionPercent":   { "type": "integer", "minimum": 1, "maximum": 100, "default": 10 }
      }
    },
    "FaultInjectionConfig": {
      "type": "object",
      "description": "Inject artificial faults for chaos testing. Not for production.",
      "properties": {
        "abort": {
          "type": "object",
          "properties": {
            "percent": { "type": "number", "minimum": 0, "maximum": 100 },
            "status":  { "type": "integer", "minimum": 100, "maximum": 999 },
            "body":    { "type": "string" }
          }
        },
        "delay": {
          "type": "object",
          "properties": {
            "percent":   { "type": "number", "minimum": 0, "maximum": 100 },
            "durationMs":{ "type": "integer", "minimum": 0 }
          }
        }
      }
    },
    "JwtAuthConfig": {
      "type": "object",
      "description": "JWT bearer-token authentication. Requires either secret (HS256) or jwksUrl (RS256/ES256).",
      "properties": {
        "secret":          { "type": "string", "description": "HMAC-SHA256 secret for HS256 tokens." },
        "jwksUrl":         { "type": "string", "format": "uri", "description": "Remote JWKS URL for RS256/ES256 tokens." },
        "jwksRefreshSecs": { "type": "integer", "minimum": 60, "default": 3600 },
        "audience":        { "type": "array", "items": { "type": "string" } },
        "issuer":          { "type": "string" },
        "skipPaths":       { "type": "array", "items": { "type": "string" } }
      }
    },
    "ForwardAuthConfig": {
      "type": "object",
      "required": ["url"],
      "description": "Delegate auth to an external service. 2xx=allow, 4xx/5xx=deny, unreachable=fail closed.",
      "properties": {
        "url":             { "type": "string", "format": "uri" },
        "requestHeaders":  { "type": "array", "items": { "type": "string" }, "description": "Request headers to forward to auth service." },
        "responseHeaders": { "type": "array", "items": { "type": "string" }, "description": "Auth service response headers to inject into upstream request." },
        "timeoutMs":       { "type": "integer", "minimum": 1, "default": 5000 },
        "skipPaths":       { "type": "array", "items": { "type": "string" } }
      }
    },
    "HeaderTransformConfig": {
      "type": "object",
      "description": "Static header injection/removal applied to every upstream request or response.",
      "properties": {
        "setHeaders":    { "type": "object", "additionalProperties": { "type": "string" } },
        "removeHeaders": { "type": "array", "items": { "type": "string" } }
      }
    },
    "ConsumersConfig": {
      "type": "object",
      "description": "Named-consumer authentication model. Each consumer has its own credentials and per-consumer policies.",
      "properties": {
        "consumers":    { "type": "array", "items": { "$ref": "#/$defs/Consumer" }, "description": "List of named consumers (evaluated in order; first match wins)." },
        "idHeader":     { "type": "string", "default": "x-consumer-id", "description": "Header injected with the consumer's username into the upstream request." },
        "apiKeyHeader": { "type": "string", "default": "x-api-key", "description": "Request header that carries the API key." },
        "skipPaths":    { "type": "array", "items": { "type": "string" }, "description": "Paths that bypass consumers auth." },
        "sharedJwt":    { "$ref": "#/$defs/ConsumersSharedJwtConfig", "description": "V3: shared JWKS for all consumers. Identifies consumer by matching jwt.sub (or usernameClaim) to consumer.username." }
      }
    },
    "ConsumersSharedJwtConfig": {
      "type": "object",
      "description": "Shared JWT config (V3): one JWKS/secret for all consumers. Consumer is identified by matching usernameClaim (default: sub) to consumer.username.",
      "properties": {
        "jwksUrl":       { "type": "string", "format": "uri", "description": "Remote JWKS URL for RS256/ES256 tokens." },
        "secret":        { "type": "string", "description": "HS256 shared secret. Mutually exclusive with jwksUrl." },
        "audience":      { "type": "array", "items": { "type": "string" } },
        "issuer":        { "type": "string" },
        "usernameClaim": { "type": "string", "default": "sub", "description": "JWT claim matched against consumer.username. Default: \"sub\"." }
      }
    },
    "Consumer": {
      "type": "object",
      "required": ["username"],
      "description": "A named API client with credentials and per-consumer policies.",
      "properties": {
        "username":  { "type": "string", "description": "Unique consumer name. Injected as X-Consumer-ID into the upstream request." },
        "apiKey":    { "type": "string", "description": "API key credential." },
        "basicAuth": {
          "type": "object",
          "required": ["password"],
          "properties": { "password": { "type": "string" } },
          "description": "Basic Auth credential. Username comes from Consumer.username."
        },
        "jwt": { "$ref": "#/$defs/ConsumerJwtConfig", "description": "JWT Bearer token credential (V2). Consumer is identified when the token validates." },
        "rateLimit": { "$ref": "#/$defs/RateLimitConfigInline", "description": "Per-consumer rate limit (independent of site-level rateLimit)." },
        "headers":   { "type": "object", "additionalProperties": { "type": "string" }, "description": "Additional headers injected into the upstream request for this consumer." }
      }
    },
    "ConsumerJwtConfig": {
      "type": "object",
      "description": "JWT credential for a consumer. Requires either secret (HS256) or jwksUrl (RS256/ES256).",
      "properties": {
        "secret":   { "type": "string", "description": "HS256 shared secret." },
        "jwksUrl":  { "type": "string", "format": "uri", "description": "Remote JWKS URL for RS256/ES256 tokens." },
        "audience": { "type": "array", "items": { "type": "string" } },
        "issuer":   { "type": "string" }
      }
    },
    "RateLimitConfigInline": {
      "type": "object",
      "required": ["windowSecs", "limit"],
      "properties": {
        "windowSecs": { "type": "integer", "minimum": 1 },
        "limit":      { "type": "integer", "minimum": 1 }
      }
    }
  }
}
