2026-02-21 16:51:07 -05:00
|
|
|
"""Configuration models and loader for the signaling server."""
|
|
|
|
|
|
2026-02-20 08:16:43 -05:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import tomllib
|
|
|
|
|
|
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ServerConfigSection(BaseModel):
|
2026-02-21 16:51:07 -05:00
|
|
|
"""Bind address and port options for websocket serving."""
|
|
|
|
|
|
2026-02-20 08:16:43 -05:00
|
|
|
bind_ip: str = "127.0.0.1"
|
|
|
|
|
port: int = 8765
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class NetworkConfigSection(BaseModel):
|
2026-02-21 16:51:07 -05:00
|
|
|
"""Network transport and safety limits."""
|
|
|
|
|
|
2026-02-20 08:16:43 -05:00
|
|
|
max_message_bytes: int = Field(default=2_000_000, gt=0)
|
|
|
|
|
allow_insecure_ws: bool = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TlsConfigSection(BaseModel):
|
2026-02-21 16:51:07 -05:00
|
|
|
"""TLS certificate/key file configuration."""
|
|
|
|
|
|
2026-02-20 08:16:43 -05:00
|
|
|
cert_file: str = ""
|
|
|
|
|
key_file: str = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LoggingConfigSection(BaseModel):
|
2026-02-21 16:51:07 -05:00
|
|
|
"""Runtime logging verbosity options."""
|
|
|
|
|
|
2026-02-20 08:16:43 -05:00
|
|
|
level: str = "INFO"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class StorageConfigSection(BaseModel):
|
2026-02-21 16:51:07 -05:00
|
|
|
"""Persistent state file location."""
|
|
|
|
|
|
2026-02-20 08:16:43 -05:00
|
|
|
state_file: str = "runtime/items.json"
|
2026-02-24 02:50:47 -05:00
|
|
|
state_save_debounce_ms: int = Field(default=200, gt=0)
|
|
|
|
|
state_save_max_delay_ms: int = Field(default=1000, gt=0)
|
2026-02-20 08:16:43 -05:00
|
|
|
|
|
|
|
|
|
2026-02-21 17:19:27 -05:00
|
|
|
class WorldConfigSection(BaseModel):
|
|
|
|
|
"""Authoritative world geometry options."""
|
|
|
|
|
|
|
|
|
|
grid_size: int = Field(default=41, ge=1)
|
|
|
|
|
|
|
|
|
|
|
2026-02-24 22:03:10 -05:00
|
|
|
class AuthConfigSection(BaseModel):
|
|
|
|
|
"""Authentication persistence and validation settings."""
|
|
|
|
|
|
|
|
|
|
db_file: str = "runtime/chatgrid.db"
|
|
|
|
|
password_min_length: int = Field(default=8, ge=1)
|
|
|
|
|
password_max_length: int = Field(default=32, ge=1)
|
|
|
|
|
username_min_length: int = Field(default=2, ge=1)
|
|
|
|
|
username_max_length: int = Field(default=32, ge=1)
|
|
|
|
|
|
|
|
|
|
|
2026-02-20 08:16:43 -05:00
|
|
|
class AppConfig(BaseModel):
|
2026-02-21 16:51:07 -05:00
|
|
|
"""Top-level application configuration document."""
|
|
|
|
|
|
2026-02-20 08:16:43 -05:00
|
|
|
server: ServerConfigSection = ServerConfigSection()
|
|
|
|
|
network: NetworkConfigSection = NetworkConfigSection()
|
|
|
|
|
tls: TlsConfigSection = TlsConfigSection()
|
|
|
|
|
logging: LoggingConfigSection = LoggingConfigSection()
|
|
|
|
|
storage: StorageConfigSection = StorageConfigSection()
|
2026-02-21 17:19:27 -05:00
|
|
|
world: WorldConfigSection = WorldConfigSection()
|
2026-02-24 22:03:10 -05:00
|
|
|
auth: AuthConfigSection = AuthConfigSection()
|
2026-02-20 08:16:43 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_config(path: Path | None) -> AppConfig:
|
2026-02-21 16:51:07 -05:00
|
|
|
"""Load and validate config TOML, applying defaults and TLS checks."""
|
|
|
|
|
|
2026-02-20 08:16:43 -05:00
|
|
|
if path is None:
|
|
|
|
|
return AppConfig()
|
|
|
|
|
|
|
|
|
|
if not path.exists():
|
|
|
|
|
raise FileNotFoundError(f"Config file not found: {path}")
|
|
|
|
|
|
|
|
|
|
with path.open("rb") as fp:
|
|
|
|
|
data = tomllib.load(fp)
|
|
|
|
|
|
|
|
|
|
config = AppConfig.model_validate(data)
|
|
|
|
|
|
|
|
|
|
cert = config.tls.cert_file.strip()
|
|
|
|
|
key = config.tls.key_file.strip()
|
|
|
|
|
|
|
|
|
|
if not config.network.allow_insecure_ws and (not cert or not key):
|
|
|
|
|
raise ValueError(
|
|
|
|
|
"TLS is required when network.allow_insecure_ws=false; set tls.cert_file and tls.key_file"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return config
|