"""Configuration helpers for the Borealis Engine runtime. Stage 2 of the migration focuses on lifting the legacy configuration loading behaviour from :mod:`Data.Server.server` into reusable helpers so the Engine start-up path honours the same environment variables, filesystem layout, and logging expectations. This module documents the supported launch parameters and exposes typed helpers that the application factory consumes. Launch overview --------------- The Engine can be started via :func:`Data.Engine.bootstrapper.main` or by invoking :func:`Data.Engine.server.create_app` manually. Configuration is assembled from (in precedence order): ``config`` mapping overrides provided to :func:`load_runtime_config`, environment variables prefixed with ``BOREALIS_``, and finally built-in defaults that mirror the legacy server runtime. Key environment variables are ``BOREALIS_DATABASE_PATH`` path to the SQLite database file. Defaults to ``/database.db``. ``BOREALIS_CORS_ORIGINS`` comma separated list of allowed origins for CORS. ``BOREALIS_SECRET`` Flask session secret key. ``BOREALIS_COOKIE_*`` Session cookie policies (``SAMESITE``, ``SECURE``, ``DOMAIN``). ``BOREALIS_TLS_*`` TLS certificate, private key, and bundle paths. When TLS values are not provided explicitly the Engine falls back to the certificate helper shipped with the legacy server, ensuring bundling parity. Logs are written to ``Logs/Engine/engine.log`` with daily rotation and errors are additionally duplicated to ``Logs/Engine/error.log`` so the runtime integrates with the platform's logging policy. """ from __future__ import annotations import logging import os from dataclasses import asdict, dataclass, field from logging.handlers import TimedRotatingFileHandler from pathlib import Path from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Sequence, Tuple try: # pragma: no-cover - optional dependency during early migration stages. from Modules.crypto import certificates # type: ignore except Exception: # pragma: no-cover - Engine configuration still works without it. certificates = None # type: ignore[assignment] ENGINE_DIR = Path(__file__).resolve().parent PROJECT_ROOT = ENGINE_DIR.parent.parent DEFAULT_DATABASE_PATH = PROJECT_ROOT / "database.db" LOG_ROOT = PROJECT_ROOT / "Logs" / "Engine" LOG_FILE_PATH = LOG_ROOT / "engine.log" ERROR_LOG_FILE_PATH = LOG_ROOT / "error.log" def _ensure_parent(path: Path) -> None: try: path.parent.mkdir(parents=True, exist_ok=True) except Exception: # Directory creation failure is non-fatal; subsequent file operations # will surface the issue with clearer context. pass def _resolve_static_folder() -> str: candidate_roots = [ ENGINE_DIR.parent / "Engine" / "web-interface", ENGINE_DIR / "web-interface", ] candidates = [] for root in candidate_roots: absolute_root = root.resolve() candidates.extend( [ absolute_root / "build", absolute_root / "dist", absolute_root, ] ) for candidate in candidates: if candidate.is_dir(): return str(candidate) return str(candidates[0]) def _parse_origins(raw: Optional[Any]) -> Optional[List[str]]: if raw is None: return None if isinstance(raw, str): parts = [part.strip() for part in raw.split(",")] elif isinstance(raw, Sequence): parts = [str(part).strip() for part in raw] else: return None origins = [part for part in parts if part] return origins or None def _parse_bool(raw: Any, *, default: bool = False) -> bool: if raw is None: return default if isinstance(raw, bool): return raw lowered = str(raw).strip().lower() if lowered in {"1", "true", "yes", "on"}: return True if lowered in {"0", "false", "no", "off"}: return False return default def _discover_tls_material(config: Mapping[str, Any]) -> Sequence[Optional[str]]: cert_path = config.get("TLS_CERT_PATH") or os.environ.get("BOREALIS_TLS_CERT") or None key_path = config.get("TLS_KEY_PATH") or os.environ.get("BOREALIS_TLS_KEY") or None bundle_path = config.get("TLS_BUNDLE_PATH") or os.environ.get("BOREALIS_TLS_BUNDLE") or None if certificates and not all([cert_path, key_path, bundle_path]): try: auto_cert, auto_key, auto_bundle = certificates.certificate_paths() except Exception: auto_cert = auto_key = auto_bundle = None else: cert_path = cert_path or auto_cert key_path = key_path or auto_key bundle_path = bundle_path or auto_bundle if cert_path: os.environ.setdefault("BOREALIS_TLS_CERT", str(cert_path)) if key_path: os.environ.setdefault("BOREALIS_TLS_KEY", str(key_path)) if bundle_path: os.environ.setdefault("BOREALIS_TLS_BUNDLE", str(bundle_path)) return cert_path, key_path, bundle_path @dataclass class EngineSettings: """Resolved configuration values for the Engine runtime.""" database_path: str static_folder: str cors_origins: Optional[List[str]] secret_key: str session_cookie_samesite: str session_cookie_secure: bool session_cookie_domain: Optional[str] tls_cert_path: Optional[str] tls_key_path: Optional[str] tls_bundle_path: Optional[str] log_file: str error_log_file: str api_groups: Tuple[str, ...] raw: MutableMapping[str, Any] = field(default_factory=dict) def to_flask_config(self) -> MutableMapping[str, Any]: config: MutableMapping[str, Any] = { "SESSION_COOKIE_HTTPONLY": True, "SESSION_COOKIE_SAMESITE": self.session_cookie_samesite, "SESSION_COOKIE_SECURE": self.session_cookie_secure, "PREFERRED_URL_SCHEME": "https", } if self.session_cookie_domain: config["SESSION_COOKIE_DOMAIN"] = self.session_cookie_domain return config def as_dict(self) -> MutableMapping[str, Any]: data = asdict(self) data["raw"] = dict(self.raw) return data def _parse_api_groups(raw: Optional[Any]) -> Tuple[str, ...]: if raw is None: return tuple() if isinstance(raw, str): parts: Iterable[str] = (part.strip() for part in raw.split(",")) elif isinstance(raw, Sequence): parts = (str(part).strip() for part in raw) else: return tuple() cleaned = [part.lower() for part in parts if part] return tuple(dict.fromkeys(cleaned)) def load_runtime_config(overrides: Optional[Mapping[str, Any]] = None) -> EngineSettings: """Resolve Engine configuration values. Parameters ---------- overrides: Optional mapping of explicit configuration values. These take precedence over environment variables and built-in defaults. """ runtime_config: MutableMapping[str, Any] = dict(overrides or {}) database_path = str( runtime_config.get("DATABASE_PATH") or os.environ.get("BOREALIS_DATABASE_PATH") or DEFAULT_DATABASE_PATH ) database_path = os.path.abspath(database_path) _ensure_parent(Path(database_path)) static_folder = str(runtime_config.get("STATIC_FOLDER") or _resolve_static_folder()) cors_origins = _parse_origins( runtime_config.get("CORS_ORIGINS") or os.environ.get("BOREALIS_CORS_ORIGINS") ) secret_key = str(runtime_config.get("SECRET_KEY") or os.environ.get("BOREALIS_SECRET") or "borealis-dev-secret") session_cookie_samesite = str( runtime_config.get("SESSION_COOKIE_SAMESITE") or os.environ.get("BOREALIS_COOKIE_SAMESITE") or "Lax" ) session_cookie_secure = _parse_bool( runtime_config.get("SESSION_COOKIE_SECURE"), default=_parse_bool(os.environ.get("BOREALIS_COOKIE_SECURE"), default=False), ) session_cookie_domain = runtime_config.get("SESSION_COOKIE_DOMAIN") or os.environ.get("BOREALIS_COOKIE_DOMAIN") session_cookie_domain = str(session_cookie_domain) if session_cookie_domain else None tls_cert_path, tls_key_path, tls_bundle_path = _discover_tls_material(runtime_config) log_file = str(runtime_config.get("LOG_FILE") or LOG_FILE_PATH) _ensure_parent(Path(log_file)) error_log_file = str(runtime_config.get("ERROR_LOG_FILE") or ERROR_LOG_FILE_PATH) _ensure_parent(Path(error_log_file)) api_groups = _parse_api_groups( runtime_config.get("API_GROUPS") or os.environ.get("BOREALIS_API_GROUPS") ) if not api_groups: api_groups = ("tokens", "enrollment") settings = EngineSettings( database_path=database_path, static_folder=static_folder, cors_origins=cors_origins, secret_key=secret_key, session_cookie_samesite=session_cookie_samesite, session_cookie_secure=session_cookie_secure, session_cookie_domain=session_cookie_domain, tls_cert_path=tls_cert_path if tls_cert_path else None, tls_key_path=tls_key_path if tls_key_path else None, tls_bundle_path=tls_bundle_path if tls_bundle_path else None, log_file=str(log_file), error_log_file=str(error_log_file), api_groups=api_groups, raw=runtime_config, ) return settings def initialise_engine_logger(settings: EngineSettings, name: str = "borealis.engine") -> logging.Logger: """Configure the Engine logger to write to Engine log files.""" logger = logging.getLogger(name) if not logger.handlers: formatter = logging.Formatter("%(asctime)s-%(name)s-%(levelname)s: %(message)s") stream_handler = logging.StreamHandler() stream_handler.setFormatter(formatter) logger.addHandler(stream_handler) file_handler = TimedRotatingFileHandler( settings.log_file, when="midnight", backupCount=0, encoding="utf-8", ) file_handler.setFormatter(formatter) logger.addHandler(file_handler) error_handler = TimedRotatingFileHandler( settings.error_log_file, when="midnight", backupCount=0, encoding="utf-8", ) error_handler.setLevel(logging.ERROR) error_handler.setFormatter(formatter) logger.addHandler(error_handler) logger.setLevel(logging.INFO) logger.propagate = False return logger __all__ = [ "EngineSettings", "initialise_engine_logger", "load_runtime_config", ]