{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://schema.spbx.tel/v1/structs.server_config",
  "$ref": "#/$defs/structs.server_config",
  "$defs": {
    "structs.admin_api_config": {
      "properties": {
        "enabled": {
          "type": "boolean",
          "description": "Whether the admin API is served."
        },
        "listen": {
          "type": "string",
          "description": "host:port the admin HTTP server binds to, e.g. 127.0.0.1:8080. When listen_tls is also set, this listener redirects to HTTPS by default (see redirect_http_to_https).",
          "x-validate": "required_if=Enabled true,omitempty,hostname_port"
        },
        "listen_tls": {
          "type": "string",
          "description": "host:port for an HTTPS admin listener, e.g. 127.0.0.1:8443. When set, the admin API is also served over TLS reusing the server's SIP TLS certificate (tls_cert_file/tls_key_file become required). Omit to serve plain HTTP only.",
          "x-validate": "omitempty,hostname_port"
        },
        "users_file": {
          "type": "string",
          "description": "Path to the JSON file holding admin users (username, argon2id PHC hash, role, realm). Generated with a random super_admin on first start if missing; watched for edits; plaintext password fields in it are hashed and rewritten in place.",
          "x-validate": "required_if=Enabled true"
        },
        "jwt_secret_file": {
          "type": "string",
          "description": "Path to the file holding the HS256 JWT signing secret. A random 32-byte secret is generated and written if the file is missing.",
          "x-validate": "required_if=Enabled true"
        },
        "redirect_http_to_https": {
          "type": "boolean",
          "description": "When an HTTPS listener (listen_tls) is configured, redirect plain-HTTP requests on 'listen' to HTTPS with 308 instead of serving the API over HTTP. Defaults to true. Set false to serve the API over both HTTP and HTTPS."
        }
      },
      "additionalProperties": false,
      "type": "object",
      "required": [
        "listen",
        "users_file",
        "jwt_secret_file"
      ]
    },
    "structs.call_log_config": {
      "properties": {
        "directory": {
          "type": "string",
          "description": "Directory where per-call log files are written. Relative paths are resolved against the directory containing the server config file.",
          "x-validate": "required"
        },
        "max_files": {
          "type": "integer",
          "description": "Maximum number of call log files kept; the oldest are deleted to make room (ring buffer).",
          "x-validate": "min=1"
        },
        "network_log": {
          "type": "boolean",
          "description": "When true, raw SIP messages sent and received for a call are recorded in its log file."
        }
      },
      "additionalProperties": false,
      "type": "object",
      "required": [
        "directory",
        "max_files"
      ]
    },
    "structs.observability_config": {
      "properties": {
        "service_name": {
          "type": "string",
          "description": "OpenTelemetry service.name resource attribute for every exported signal. Defaults to \"s.pbx\" when empty."
        },
        "insecure": {
          "type": "boolean",
          "description": "When true, the OTLP/gRPC connection to the collector uses plaintext (no TLS). When false (the default), the exporter uses TLS with the system certificate pool."
        },
        "headers": {
          "additionalProperties": {
            "type": "string"
          },
          "type": "object",
          "description": "Extra headers (gRPC metadata) sent with every OTLP export request on all enabled pillars, e.g. {\"authorization\": \"Bearer \u003ctoken\u003e\"} or an api-key header for authenticated collectors.",
          "x-validate": "omitempty"
        },
        "traces": {
          "$ref": "#/$defs/structs.otlp_pillar_config",
          "description": "OTLP trace export (root span per call plus dialplan and outbound-dial child spans). Omit or disable to skip trace export.",
          "x-validate": "omitempty"
        },
        "metrics": {
          "$ref": "#/$defs/structs.otlp_pillar_config",
          "description": "OTLP metric export (call/registration counters, active-call and registered-extension gauges, call-duration histogram). Omit or disable to skip metric export.",
          "x-validate": "omitempty"
        },
        "logs": {
          "$ref": "#/$defs/structs.otlp_pillar_config",
          "description": "OTLP log export bridging zerolog records to the collector. Additive: stdout logging is unaffected. Omit or disable to skip log export.",
          "x-validate": "omitempty"
        }
      },
      "additionalProperties": false,
      "type": "object"
    },
    "structs.otlp_pillar_config": {
      "properties": {
        "enabled": {
          "type": "boolean",
          "description": "Whether this signal is exported."
        },
        "endpoint": {
          "type": "string",
          "description": "OTLP/gRPC collector endpoint as host:port (e.g. localhost:4317). Required when enabled is true.",
          "x-validate": "omitempty,min=3"
        }
      },
      "additionalProperties": false,
      "type": "object",
      "required": [
        "enabled"
      ]
    },
    "structs.server_config": {
      "properties": {
        "host": {
          "type": "string",
          "description": "IP address the SIP listeners and RTP sockets bind to. Use 0.0.0.0 to listen on all interfaces; a concrete address also becomes the default advertised in SIP/SDP.",
          "x-validate": "required,ip"
        },
        "from": {
          "type": "string",
          "description": "Hostname this PBX identifies as (e.g. pbx.example.com). Used as the From-header host of internal dials and as the subject (CN and DNS SAN) of certificates generated by --new-tls-cert.",
          "x-validate": "required"
        },
        "reachable_ip": {
          "type": "string",
          "description": "Public IP address this PBX is reachable at (static NAT). Overrides the per-peer route lookup when advertising addresses in SIP/SDP. A trunk's reachable_ip takes precedence for legs toward that trunk.",
          "x-validate": "omitempty,ip"
        },
        "port_tcp": {
          "type": "integer",
          "description": "TCP port for plain SIP over TCP. Omit to disable the TCP listener.",
          "x-validate": "omitempty,min=1,max=65535"
        },
        "port_tls": {
          "type": "integer",
          "description": "TCP port for SIP over TLS (SIPS). Requires tls_cert_file and tls_key_file.",
          "x-validate": "omitempty,min=1,max=65535"
        },
        "port_udp": {
          "type": "integer",
          "description": "UDP port for plain SIP over UDP. Omit to disable the UDP listener.",
          "x-validate": "omitempty,min=1,max=65535"
        },
        "port_ws": {
          "type": "integer",
          "description": "TCP port for SIP over WebSocket (WS). Omit to disable the WebSocket listener.",
          "x-validate": "omitempty,min=1,max=65535"
        },
        "port_wss": {
          "type": "integer",
          "description": "TCP port for SIP over secure WebSocket (WSS). Requires tls_cert_file and tls_key_file.",
          "x-validate": "omitempty,min=1,max=65535"
        },
        "tls_cert_file": {
          "type": "string",
          "description": "Path to the PEM-encoded server certificate chain (leaf first). Relative paths are resolved against the directory containing this server config file. Required when port_tls or port_wss is set. Loaded once at startup; rotating the certificate requires a server restart.",
          "x-validate": "required_with=PortTls PortWss TlsKeyFile,omitempty,file"
        },
        "tls_key_file": {
          "type": "string",
          "description": "Path to the PEM-encoded private key matching tls_cert_file. Relative paths are resolved against the directory containing this server config file. Required when port_tls or port_wss is set.",
          "x-validate": "required_with=PortTls PortWss TlsCertFile,omitempty,file"
        },
        "max_connections": {
          "type": "integer",
          "description": "Maximum number of concurrent connections. Reserved: currently not enforced.",
          "x-validate": "min=1"
        },
        "rtp_port_range_start": {
          "type": "integer",
          "description": "First UDP port (inclusive) of the range RTP media sockets are allocated from; one port is taken per call leg.",
          "x-validate": "min=1024,max=65535"
        },
        "rtp_port_range_end": {
          "type": "integer",
          "description": "Last UDP port (inclusive) of the RTP media port range. Must be greater than rtp_port_range_start.",
          "x-validate": "min=1024,max=65535,gtfield=RtpPortRangeStart"
        },
        "disabled_codecs": {
          "items": {
            "type": "string"
          },
          "type": "array",
          "description": "Codec engines removed from the registry at startup, disabling them server-wide. Entries match the engine's unique name exactly (e.g. PCMA, OPUS_48000). For per-peer filtering see the disabled_codecs list on trunks and extensions.",
          "x-validate": "omitempty,dive,required"
        },
        "raise_udp_mtu": {
          "type": "boolean",
          "description": "When true (the default), sipgo's RFC 3261 UDP size cap (1300 bytes) is raised to 65535, relying on IP fragmentation for large SIP messages, and outgoing calls offer every compiled codec. When false the RFC cap is enforced and outgoing calls offer only simple narrowband codecs (PCMU/PCMA/G722) so INVITEs stay under it."
        },
        "stun_server": {
          "type": "string",
          "description": "STUN server (host:port, e.g. stun.example.com:3478) used to discover the NAT mapping of each trunk-leg RTP socket before the SDP is sent. Omit to disable STUN discovery. A trunk's reachable_ip overrides it.",
          "x-validate": "omitempty,min=3"
        },
        "srtp": {
          "type": "string",
          "description": "SDES-SRTP media encryption mode (RFC 4568). 'disabled' refuses RTP/SAVP offers; 'optional' (the default) accepts SRTP from peers that offer it — including best-effort crypto on RTP/AVP offers — and places outbound calls with a best-effort crypto offer that falls back to plain RTP when the peer declines; 'required' offers strict RTP/SAVP on every outbound leg (a crypto-less answer drops the leg) and rejects inbound calls without SRTP or arriving over unencrypted signaling transports. SDES keys travel inside SDP, so use TLS/WSS signaling whenever this is not 'disabled'.",
          "x-validate": "omitempty,oneof=disabled optional required"
        },
        "call_log": {
          "$ref": "#/$defs/structs.call_log_config",
          "description": "Per-call JSON trace logging. Omit to disable call logging entirely.",
          "x-validate": "omitempty"
        },
        "admin_api": {
          "$ref": "#/$defs/structs.admin_api_config",
          "description": "REST admin API (HTTP, optionally HTTPS). Omit or set enabled=false to disable. The plain-HTTP listener can be fronted by a TLS-terminating reverse proxy, or set listen_tls to serve HTTPS directly using the server's SIP TLS certificate.",
          "x-validate": "omitempty"
        },
        "observability": {
          "$ref": "#/$defs/structs.observability_config",
          "description": "OpenTelemetry (OTLP/gRPC) export of traces, metrics and logs. Omit to disable all three pillars. Each pillar is enabled independently.",
          "x-validate": "omitempty"
        },
        "log_format": {
          "type": "string",
          "description": "Logging output format: json (the default) or console (human-readable, colorized). An empty value means json.",
          "x-validate": "omitempty,oneof=json console"
        },
        "log_level": {
          "type": "string",
          "description": "Global logging level, parsed by zerolog: trace, debug, info, warn, error, fatal, panic or disabled. An empty value means info.",
          "x-validate": "omitempty"
        },
        "realm_config": {
          "type": "string",
          "description": "Path to a single realm configuration file. Relative paths are resolved against the directory containing this server config file. Exactly one of realm_config and realm_config_dir must be set to run the server.",
          "x-validate": "excluded_with=RealmConfigDir"
        },
        "realm_config_dir": {
          "type": "string",
          "description": "Path to a directory of realm configuration files. Relative paths are resolved against the directory containing this server config file. Exactly one of realm_config and realm_config_dir must be set to run the server.",
          "x-validate": "omitempty"
        },
        "watch": {
          "type": "boolean",
          "description": "When true the realm configuration file or directory is watched for changes and hot-reloaded while the server runs."
        },
        "$schema": {
          "type": "string"
        }
      },
      "additionalProperties": false,
      "type": "object",
      "required": [
        "host",
        "from",
        "max_connections",
        "rtp_port_range_start",
        "rtp_port_range_end",
        "disabled_codecs",
        "log_format",
        "log_level",
        "watch"
      ]
    }
  }
}
