diff --git a/Data/Server/Modules/auth/jwt_service.py b/Data/Server/Modules/auth/jwt_service.py index 9d77859..aa66cc9 100644 --- a/Data/Server/Modules/auth/jwt_service.py +++ b/Data/Server/Modules/auth/jwt_service.py @@ -14,7 +14,9 @@ import jwt from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ed25519 -_KEY_DIR = Path(__file__).resolve().parent.parent / "keys" +from Modules.runtime import ensure_runtime_dir, runtime_path + +_KEY_DIR = runtime_path("keys") _KEY_FILE = _KEY_DIR / "borealis-jwt-ed25519.key" @@ -96,7 +98,7 @@ def load_service() -> JWTService: def _load_or_create_private_key() -> ed25519.Ed25519PrivateKey: - _KEY_DIR.mkdir(parents=True, exist_ok=True) + ensure_runtime_dir("keys") if _KEY_FILE.exists(): with _KEY_FILE.open("rb") as fh: return serialization.load_pem_private_key(fh.read(), password=None) diff --git a/Data/Server/Modules/crypto/certificates.py b/Data/Server/Modules/crypto/certificates.py index b50c40f..ff6b1db 100644 --- a/Data/Server/Modules/crypto/certificates.py +++ b/Data/Server/Modules/crypto/certificates.py @@ -19,7 +19,9 @@ from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec from cryptography.x509.oid import NameOID -_CERT_DIR = Path(__file__).resolve().parent.parent / "certs" +from Modules.runtime import ensure_runtime_dir, runtime_path + +_CERT_DIR = runtime_path("certs") _CERT_FILE = _CERT_DIR / "borealis-server-cert.pem" _KEY_FILE = _CERT_DIR / "borealis-server-key.pem" _BUNDLE_FILE = _CERT_DIR / "borealis-server-bundle.pem" @@ -35,7 +37,7 @@ def ensure_certificate(common_name: str = "Borealis Server") -> Tuple[Path, Path Returns (cert_path, key_path, bundle_path). """ - _CERT_DIR.mkdir(parents=True, exist_ok=True) + ensure_runtime_dir("certs") regenerate = not (_CERT_FILE.exists() and _KEY_FILE.exists()) if not regenerate: diff --git a/Data/Server/Modules/crypto/signing.py b/Data/Server/Modules/crypto/signing.py index 18403c9..13d6a86 100644 --- a/Data/Server/Modules/crypto/signing.py +++ b/Data/Server/Modules/crypto/signing.py @@ -10,9 +10,11 @@ from typing import Tuple from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ed25519 +from Modules.runtime import ensure_runtime_dir, runtime_path + from .keys import base64_from_spki_der -_KEY_DIR = Path(__file__).resolve().parent.parent / "keys" +_KEY_DIR = runtime_path("keys") _SIGNING_KEY_FILE = _KEY_DIR / "borealis-script-ed25519.key" _SIGNING_PUB_FILE = _KEY_DIR / "borealis-script-ed25519.pub" @@ -41,7 +43,7 @@ def load_signer() -> ScriptSigner: def _load_or_create() -> ed25519.Ed25519PrivateKey: - _KEY_DIR.mkdir(parents=True, exist_ok=True) + ensure_runtime_dir("keys") if _SIGNING_KEY_FILE.exists(): with _SIGNING_KEY_FILE.open("rb") as fh: return serialization.load_pem_private_key(fh.read(), password=None) diff --git a/Data/Server/Modules/runtime.py b/Data/Server/Modules/runtime.py new file mode 100644 index 0000000..822c994 --- /dev/null +++ b/Data/Server/Modules/runtime.py @@ -0,0 +1,78 @@ +"""Utility helpers for locating runtime storage paths. + +The Borealis repository keeps the authoritative source code under ``Data/`` +so that the bootstrap scripts can copy those assets into sibling ``Server/`` +and ``Agent/`` directories for execution. Runtime artefacts such as TLS +certificates or signing keys must therefore live outside ``Data`` to avoid +polluting the template tree. This module centralises the path selection so +other modules can rely on a consistent location regardless of whether they +are executed from the copied runtime directory or directly from ``Data`` +during development. +""" + +from __future__ import annotations + +import os +from functools import lru_cache +from pathlib import Path +from typing import Optional + + +def _env_path(name: str) -> Optional[Path]: + """Return a resolved ``Path`` for the given environment variable.""" + + value = os.environ.get(name) + if not value: + return None + try: + return Path(value).expanduser().resolve() + except Exception: + return None + + +@lru_cache(maxsize=None) +def project_root() -> Path: + """Best-effort detection of the repository root.""" + + env = _env_path("BOREALIS_PROJECT_ROOT") + if env: + return env + + current = Path(__file__).resolve() + for parent in current.parents: + if (parent / "Borealis.ps1").exists() or (parent / ".git").is_dir(): + return parent + + # Fallback to the ancestor that corresponds to ``/`` when the module + # lives under ``Data/Server/Modules``. + try: + return current.parents[4] + except IndexError: + return current.parent + + +@lru_cache(maxsize=None) +def server_runtime_root() -> Path: + """Location where the running server stores mutable artefacts.""" + + env = _env_path("BOREALIS_SERVER_ROOT") + if env: + return env + + root = project_root() + runtime = root / "Server" / "Borealis" + return runtime + + +def runtime_path(*parts: str) -> Path: + """Return a path relative to the server runtime root.""" + + return server_runtime_root().joinpath(*parts) + + +def ensure_runtime_dir(*parts: str) -> Path: + """Create (if required) and return a runtime directory.""" + + path = runtime_path(*parts) + path.mkdir(parents=True, exist_ok=True) + return path diff --git a/Data/Server/WebUI/vite.config.mts b/Data/Server/WebUI/vite.config.mts index c43aa0a..09be004 100644 --- a/Data/Server/WebUI/vite.config.mts +++ b/Data/Server/WebUI/vite.config.mts @@ -4,16 +4,20 @@ import react from '@vitejs/plugin-react'; import path from 'path'; import fs from 'fs'; +const runtimeCertDir = process.env.BOREALIS_CERT_DIR; + const certCandidates = [ process.env.BOREALIS_TLS_CERT, + runtimeCertDir && path.resolve(runtimeCertDir, 'borealis-server-cert.pem'), path.resolve(__dirname, '../certs/borealis-server-cert.pem'), - path.resolve(__dirname, '../../Data/Server/certs/borealis-server-cert.pem'), + path.resolve(__dirname, '../../../Server/Borealis/certs/borealis-server-cert.pem'), ] as const; const keyCandidates = [ process.env.BOREALIS_TLS_KEY, + runtimeCertDir && path.resolve(runtimeCertDir, 'borealis-server-key.pem'), path.resolve(__dirname, '../certs/borealis-server-key.pem'), - path.resolve(__dirname, '../../Data/Server/certs/borealis-server-key.pem'), + path.resolve(__dirname, '../../../Server/Borealis/certs/borealis-server-key.pem'), ] as const; const pickFirst = (candidates: readonly (string | undefined)[]) => { diff --git a/Data/Server/server.py b/Data/Server/server.py index f657bef..d1c234f 100644 --- a/Data/Server/server.py +++ b/Data/Server/server.py @@ -20,6 +20,7 @@ Section Guide: import os import sys +from pathlib import Path # Ensure the modular server package is importable when the runtime is launched # from a packaged directory (e.g., Server/Borealis). We look for the canonical @@ -167,6 +168,10 @@ TLS_CERT_PATH, TLS_KEY_PATH, TLS_BUNDLE_PATH = certificates.certificate_paths() os.environ.setdefault("BOREALIS_TLS_CERT", TLS_CERT_PATH) os.environ.setdefault("BOREALIS_TLS_KEY", TLS_KEY_PATH) os.environ.setdefault("BOREALIS_TLS_BUNDLE", TLS_BUNDLE_PATH) +try: + os.environ.setdefault("BOREALIS_CERT_DIR", str(Path(TLS_CERT_PATH).resolve().parent)) +except Exception: + pass JWT_SERVICE = jwt_service_module.load_service() SCRIPT_SIGNER = signing.load_signer()