From f746085eadea5d6b10430d121c9f63c5e5dae2f3 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Thu, 19 Feb 2026 14:24:16 +0000 Subject: [PATCH] CLI main and RuntimeConfig cleanup. Added a session_ctx wrapper function for easier access and avoiding hostutil import in db. --- paskia/__main__.py | 127 +++++++++++++------------------------- paskia/authsession.py | 7 ++- paskia/config.py | 17 ----- paskia/db/structs.py | 11 ++-- paskia/fastapi/api.py | 8 +-- paskia/fastapi/mainapp.py | 23 +++---- paskia/fastapi/user.py | 11 ++-- paskia/fastapi/ws.py | 4 +- paskia/fastapi/wschat.py | 5 +- paskia/oidc_notify.py | 2 +- paskia/util/hostutil.py | 42 ++++++++----- paskia/util/permutil.py | 4 +- paskia/util/runtime.py | 31 ++++++++++ paskia/util/startupbox.py | 52 +++++++++------- 14 files changed, 166 insertions(+), 178 deletions(-) create mode 100644 paskia/util/runtime.py diff --git a/paskia/__main__.py b/paskia/__main__.py index de063ba..f7b2081 100644 --- a/paskia/__main__.py +++ b/paskia/__main__.py @@ -1,16 +1,16 @@ import argparse -import json import logging import os from urllib.parse import urlparse +import msgspec from fastapi_vue import server from fastapi_vue.hostutil import parse_endpoints -from paskia.config import PaskiaConfig from paskia.db.jsonl import load_readonly from paskia.util import startupbox from paskia.util.hostutil import normalize_origin +from paskia.util.runtime import RuntimeConfig DEFAULT_PORT = 4401 DEVMODE = os.getenv("PASKIA_DEV") == "1" @@ -51,7 +51,6 @@ def add_common_options(p: argparse.ArgumentParser) -> None: "--origin", action="append", dest="origins", - default=[], metavar="URL", help="Allowed origin URL(s). May be specified multiple times. If any are specified, only those origins are permitted for WebSocket authentication.", ) @@ -91,106 +90,66 @@ def main(): args = parser.parse_args() - # Handle clearing options - if getattr(args, "auth_host", None) == "": - args.auth_host = None - if getattr(args, "rp_name", None) == "": - args.rp_name = None - if getattr(args, "listen", None) == "": - args.listen = None - - # Read-only load to get stored config (no writes, no global state) + # Load stored config (read-only, no writes, no global state) db_path = os.environ.get("PASKIA_DB", f"{args.rp_id}.paskiadb") - stored_db = load_readonly(db_path, rp_id=args.rp_id) - stored_config = stored_db.config + config = load_readonly(db_path, rp_id=args.rp_id).config - # Apply defaults from stored config - if args.rp_name is None and stored_config.rp_name is not None: - args.rp_name = stored_config.rp_name - if args.origins is None and stored_config.origins is not None: - args.origins = stored_config.origins - if args.auth_host is None and stored_config.auth_host is not None: - args.auth_host = stored_config.auth_host - if args.listen is None and stored_config.listen is not None: - args.listen = stored_config.listen - - # Parse first endpoint for config display and site_url - ep = next(iter(parse_endpoints(args.listen, DEFAULT_PORT)), {}) - host, port, uds = ep.get("host"), ep.get("port"), ep.get("uds") + # Override stored config with CLI args, or clear with empty string + if args.rp_name is not None: + config.rp_name = args.rp_name or None + if args.auth_host is not None: + config.auth_host = args.auth_host or None + if args.origins is not None: + config.origins = None if args.origins == [""] else args.origins + if args.listen is not None: + config.listen = None if args.listen == [""] else args.listen # Process and normalize auth_host - if args.auth_host: - if "://" not in args.auth_host: - args.auth_host = f"https://{args.auth_host}" - args.auth_host = args.auth_host.rstrip("/") - validate_auth_host(args.auth_host, args.rp_id) - if args.origins: - args.origins.insert(0, args.auth_host) # Ensure first in origins + if config.auth_host: + if "://" not in config.auth_host: + config.auth_host = f"https://{config.auth_host}" + config.auth_host = config.auth_host.rstrip("/") + validate_auth_host(config.auth_host, config.rp_id) + if config.origins: + config.origins.insert(0, config.auth_host) # Ensure first in origins - # Normalize, strip trailing slashes, and deduplicate while preserving order - origins = list({normalize_origin(o).rstrip("/"): ... for o in (args.origins)}) + # Normalize and deduplicate while preserving order + if config.origins: + config.origins = list({normalize_origin(o): ... for o in config.origins}) - # Compute site_url and site_path for reset links - # Priority: auth_host > first configured origin > PASKIA_VITE_URL (devserver) > http://localhost:port > https://rp_id + # Parse first endpoint for site_url fallback + ep = next(iter(parse_endpoints(config.listen, DEFAULT_PORT)), {}) + port = ep.get("port") + + # Compute site_url and site_path + # Priority: auth_host > origins[0] > PASKIA_VITE_URL > http://localhost:port > https://rp_id site_path = "/auth/" - if args.auth_host: - site_url = args.auth_host - site_path = "/" - elif origins: - # Find localhost origin if rp_id is localhost, else use first origin - localhost_origin = ( - next((o for o in origins if "://localhost" in o), None) - if args.rp_id == "localhost" - else None - ) - site_url = localhost_origin or origins[0] + if config.auth_host: + site_url, site_path = config.auth_host, "/" + elif config.origins: + site_url = config.origins[0] elif vite_url := os.environ.get("PASKIA_VITE_URL"): site_url = vite_url.rstrip("/") # Devserver - elif args.rp_id == "localhost" and port: + elif config.rp_id == "localhost" and port: site_url = f"http://localhost:{port}" # Backend directly if we can else: - site_url = f"https://{args.rp_id}" # Assume external reverse proxy + site_url = f"https://{config.rp_id}" # Assume external reverse proxy - # Build runtime configuration - config = PaskiaConfig( - rp_id=args.rp_id, - rp_name=args.rp_name or None, - origins=origins or None, - auth_host=args.auth_host or None, + # Build runtime configuration for the server + runtime = RuntimeConfig( + config=config, site_url=site_url, site_path=site_path, - host=host, - port=port, - uds=uds, + save=args.save, ) + startupbox.print_startup_config(runtime) + os.environ["PASKIA_CONFIG"] = msgspec.json.encode(runtime).decode() - # Export configuration via single JSON env variable for worker processes - # Include cli_config and save flag so lifespan can handle bootstrap/persistence - cli_config = { - "rp_id": args.rp_id, - "rp_name": args.rp_name, - "origins": args.origins, - "auth_host": args.auth_host, - "listen": args.listen, - } - config_json = { - "rp_id": config.rp_id, - "rp_name": config.rp_name, - "origins": config.origins, - "auth_host": config.auth_host, - "site_url": config.site_url, - "site_path": config.site_path, - "save": args.save, - "cli_config": cli_config, - } - os.environ["PASKIA_CONFIG"] = json.dumps(config_json) - - startupbox.print_startup_config(config) - + # Run the server (spawns processes in dev mode) dev = {"reload": True, "reload_dirs": ["paskia"]} if DEVMODE else {} server.run( "paskia.fastapi.mainapp:app", - listen=args.listen, + listen=config.listen, default_port=DEFAULT_PORT, log_level="warning", access_log=False, diff --git a/paskia/authsession.py b/paskia/authsession.py index f8bf667..b084808 100644 --- a/paskia/authsession.py +++ b/paskia/authsession.py @@ -23,6 +23,11 @@ if TYPE_CHECKING: EXPIRES = SESSION_LIFETIME +def session_ctx(auth: str, host: str | None = None): + """Get session context with normalized host.""" + return db.data().session_ctx(auth, hostutil.normalize_host(host)) + + def expires() -> datetime: return datetime.now(UTC) + EXPIRES @@ -42,7 +47,7 @@ def get_reset(token: str) -> "ResetToken": def delete_credential(credential_uuid: UUID, auth: str, host: str | None = None): """Delete a specific credential for the current user.""" - ctx = db.data().session_ctx(auth, hostutil.normalize_host(host)) + ctx = session_ctx(auth, host) if not ctx: raise ValueError("Session expired") db.delete_credential(credential_uuid, ctx.user.uuid) diff --git a/paskia/config.py b/paskia/config.py index 0991cbe..7859330 100644 --- a/paskia/config.py +++ b/paskia/config.py @@ -1,4 +1,3 @@ -from dataclasses import dataclass from datetime import timedelta # Shared configuration constants for session management. @@ -6,19 +5,3 @@ SESSION_LIFETIME = timedelta(hours=24) # Lifetime for reset links created by admins RESET_LIFETIME = timedelta(days=14) - - -@dataclass -class PaskiaConfig: - """Runtime configuration for the Paskia authentication server.""" - - rp_id: str - rp_name: str | None - origins: list[str] | None - auth_host: str | None - site_url: str # Base URL without trailing path (e.g. https://example.com) - site_path: str # Path to auth UI: "/" if auth_host, else "/auth/" - # Listen address (one of host:port or uds) - host: str | None = None - port: int | None = None - uds: str | None = None diff --git a/paskia/db/structs.py b/paskia/db/structs.py index 621def7..fb176e1 100644 --- a/paskia/db/structs.py +++ b/paskia/db/structs.py @@ -9,7 +9,6 @@ import msgspec import uuid7 from paskia import db -from paskia.util import hostutil from paskia.util import passphrase as passphrase_util from paskia.util.crypto import hash_secret @@ -601,14 +600,14 @@ class OIDC(msgspec.Struct, dict=True): key: bytes | None = None -class Config(msgspec.Struct, frozen=True, dict=True, omit_defaults=True): +class Config(msgspec.Struct, omit_defaults=True): """Stored configuration for the instance.""" rp_id: str rp_name: str | None = None - origins: list[str] | None = None auth_host: str | None = None - listen: str | None = None + origins: list[str] | None = None + listen: list[str] | None = None # ------------------------------------------------------------------------- @@ -679,10 +678,8 @@ class DB(msgspec.Struct, dict=True, omit_defaults=False): if s.client_uuid is not None: return None - # Normalize host for comparison (stored hosts are already normalized) - normalized_input = hostutil.normalize_host(host) - # Validate host matches (sessions are always created with a host) + normalized_input = host if s.host != normalized_input: # Session bound to different host return None diff --git a/paskia/fastapi/api.py b/paskia/fastapi/api.py index 5b27e5f..4a7782a 100644 --- a/paskia/fastapi/api.py +++ b/paskia/fastapi/api.py @@ -15,7 +15,7 @@ from fastapi.security import HTTPBearer from paskia import authcode, db from paskia._version import __version__ -from paskia.authsession import EXPIRES, get_reset +from paskia.authsession import EXPIRES, get_reset, session_ctx from paskia.fastapi import authz, session, user from paskia.fastapi.response import MsgspecResponse from paskia.fastapi.session import AUTH_COOKIE, AUTH_COOKIE_NAME, get_client_ip @@ -202,7 +202,7 @@ async def api_user_info( detail="Authentication required", mode="login", ) - ctx = db.data().session_ctx(auth, request.headers.get("host")) + ctx = session_ctx(auth, request.headers.get("host")) if not ctx: raise authz.AuthException( status_code=401, @@ -249,7 +249,7 @@ async def api_logout(request: Request, response: Response, auth=AUTH_COOKIE): if not auth: return {"message": "Already logged out"} host = request.headers.get("host") - ctx = db.data().session_ctx(auth, host) + ctx = session_ctx(auth, host) if not ctx: return {"message": "Already logged out"} with suppress(Exception): @@ -282,7 +282,7 @@ async def api_set_session( secret = a.session_key # Verify the session exists - ctx = db.data().session_ctx(secret, host) + ctx = session_ctx(secret, host) if not ctx: raise HTTPException(401, f"Session not found on {host}") diff --git a/paskia/fastapi/mainapp.py b/paskia/fastapi/mainapp.py index 544e837..222fd83 100644 --- a/paskia/fastapi/mainapp.py +++ b/paskia/fastapi/mainapp.py @@ -1,9 +1,9 @@ -import json import logging import os from contextlib import asynccontextmanager from pathlib import Path +import msgspec from fastapi import FastAPI, HTTPException, Request, Response from fastapi.responses import FileResponse, RedirectResponse @@ -13,7 +13,6 @@ from paskia.bootstrap import bootstrap_if_needed from paskia.db import start_background, stop_background from paskia.db.background import flush from paskia.db.logging import configure_db_logging -from paskia.db.structs import Config from paskia.fastapi import admin, api, auth_host, oid, ws # Import frontend instance @@ -21,6 +20,7 @@ from paskia.fastapi.front import frontend from paskia.fastapi.logging import AccessLogMiddleware, configure_access_logging from paskia.fastapi.session import AUTH_COOKIE from paskia.util import hostutil, passphrase, vitedev +from paskia.util.runtime import RuntimeConfig # Configure custom logging configure_access_logging() @@ -40,13 +40,13 @@ async def lifespan(app: FastAPI): # pragma: no cover - startup path so that uvicorn reload / multiprocess workers inherit the settings. All keys are guaranteed to exist; values are already normalized by __main__.py. """ - config = json.loads(os.environ["PASKIA_CONFIG"]) + runtime = msgspec.json.decode(os.environ["PASKIA_CONFIG"], type=RuntimeConfig) try: await globals.init( - rp_id=config["rp_id"], - rp_name=config["rp_name"], - origins=config["origins"], + rp_id=runtime.config.rp_id, + rp_name=runtime.config.rp_name, + origins=runtime.config.origins, bootstrap=False, ) except ValueError as e: @@ -55,13 +55,10 @@ async def lifespan(app: FastAPI): # pragma: no cover - startup path raise # Bootstrap and persist config now that the full DB is loaded - cli_config_data = config.get("cli_config") - if cli_config_data: - cli_config = Config(**cli_config_data) - await bootstrap_if_needed(config=cli_config) - if config.get("save"): - await db.update_config(cli_config) - await flush() + await bootstrap_if_needed(config=runtime.config) + if runtime.save: + await db.update_config(runtime.config) + await flush() # Restore uvicorn info logging (suppressed during startup in dev mode) # Keep uvicorn.error at WARNING to suppress WebSocket "connection open/closed" messages diff --git a/paskia/fastapi/user.py b/paskia/fastapi/user.py index 932cf88..1b5e227 100644 --- a/paskia/fastapi/user.py +++ b/paskia/fastapi/user.py @@ -13,6 +13,7 @@ from paskia import db from paskia.authsession import ( delete_credential, expires, + session_ctx, ) from paskia.fastapi import authz, session from paskia.fastapi.response import MsgspecResponse @@ -45,7 +46,7 @@ async def user_update_display_name( status_code=401, detail="Authentication Required", mode="login" ) host = request.headers.get("host") - ctx = db.data().session_ctx(auth, host) + ctx = session_ctx(auth, host) if not ctx: raise authz.AuthException( status_code=401, detail="Session expired", mode="login" @@ -74,7 +75,7 @@ async def user_update_info( raise authz.AuthException( status_code=401, detail="Authentication Required", mode="login" ) - ctx = db.data().session_ctx(auth, request.headers.get("host")) + ctx = session_ctx(auth, request.headers.get("host")) if not ctx: raise authz.AuthException( status_code=401, detail="Session expired", mode="login" @@ -112,7 +113,7 @@ async def user_update_theme( raise authz.AuthException( status_code=401, detail="Authentication Required", mode="login" ) - ctx = db.data().session_ctx(auth, request.headers.get("host")) + ctx = session_ctx(auth, request.headers.get("host")) if not ctx: raise authz.AuthException( status_code=401, detail="Session expired", mode="login" @@ -129,7 +130,7 @@ async def api_logout_all(request: Request, response: Response, auth=AUTH_COOKIE) if not auth: return {"message": "Already logged out"} host = request.headers.get("host") - ctx = db.data().session_ctx(auth, host) + ctx = session_ctx(auth, host) if not ctx: raise authz.AuthException( status_code=401, detail="Session expired", mode="login" @@ -151,7 +152,7 @@ async def api_delete_session( status_code=401, detail="Authentication Required", mode="login" ) host = request.headers.get("host") - ctx = db.data().session_ctx(auth, host) + ctx = session_ctx(auth, host) if not ctx: raise authz.AuthException( status_code=401, detail="Session expired", mode="login" diff --git a/paskia/fastapi/ws.py b/paskia/fastapi/ws.py index aa1d06d..94e7907 100644 --- a/paskia/fastapi/ws.py +++ b/paskia/fastapi/ws.py @@ -7,7 +7,7 @@ from fastapi import FastAPI, WebSocket from paskia import authcode, db from paskia.authcode import CookieCode, OIDCCode -from paskia.authsession import get_reset +from paskia.authsession import get_reset, session_ctx from paskia.db.structs import Session from paskia.fastapi import authz, remote from paskia.fastapi.session import AUTH_COOKIE, infodict @@ -195,7 +195,7 @@ async def websocket_authenticate( # If there's an existing session, restrict to that user's credentials (reauth) session_user_uuid = None if auth: - existing_ctx = db.data().session_ctx(auth, host) + existing_ctx = session_ctx(auth, host) if existing_ctx: session_user_uuid = existing_ctx.user.uuid diff --git a/paskia/fastapi/wschat.py b/paskia/fastapi/wschat.py index 9388990..9acf7bf 100644 --- a/paskia/fastapi/wschat.py +++ b/paskia/fastapi/wschat.py @@ -7,6 +7,7 @@ from uuid import UUID from fastapi import WebSocket from paskia import db +from paskia.authsession import session_ctx from paskia.db import Credential, SessionContext from paskia.fastapi.session import infodict from paskia.fastapi.wsutil import validate_origin @@ -90,7 +91,7 @@ async def authenticate_and_login( # Get credential IDs if restricting to a user's credentials credential_ids = None if auth: - existing_ctx = db.data().session_ctx(auth, host) + existing_ctx = session_ctx(auth, host) if existing_ctx: credential_ids = existing_ctx.user.credential_ids or None @@ -107,7 +108,7 @@ async def authenticate_and_login( ) # Fetch and return the full session context - ctx = db.data().session_ctx(secret, normalized_host) + ctx = session_ctx(secret, host) if not ctx: raise ValueError("Failed to create session context") return ctx, secret diff --git a/paskia/oidc_notify.py b/paskia/oidc_notify.py index 6eeba4a..3b1d320 100644 --- a/paskia/oidc_notify.py +++ b/paskia/oidc_notify.py @@ -13,7 +13,7 @@ import httpx from paskia import db from paskia.util import oidjwt -from paskia.util.hostutil import _load_config +from paskia.util.runtime import _load_config _logger = logging.getLogger(__name__) diff --git a/paskia/util/hostutil.py b/paskia/util/hostutil.py index e7c68bb..c34e9b6 100644 --- a/paskia/util/hostutil.py +++ b/paskia/util/hostutil.py @@ -1,27 +1,23 @@ """Utilities for determining the auth UI host and base URLs.""" -import json -import os -from functools import lru_cache from urllib.parse import urlparse, urlsplit +from paskia.util.runtime import _load_config -@lru_cache(maxsize=1) -def _load_config() -> dict: - """Load PASKIA_CONFIG JSON.""" - config_json = os.getenv("PASKIA_CONFIG") - if not config_json: - return {} - return json.loads(config_json) + +def _cfg(): + return _load_config() def is_root_mode() -> bool: - return _load_config().get("auth_host") is not None + cfg = _cfg() + return cfg is not None and cfg.config.auth_host is not None def dedicated_auth_host() -> str | None: """Return configured auth_host netloc, or None.""" - auth_host = _load_config().get("auth_host") + cfg = _cfg() + auth_host = cfg.config.auth_host if cfg else None if not auth_host: return None @@ -35,8 +31,10 @@ def ui_base_path() -> str: def auth_site_url() -> str: """Return the base URL for the auth site UI (computed at startup).""" - cfg = _load_config() - return cfg.get("site_url", "https://localhost") + cfg.get("site_path", "/auth/") + cfg = _cfg() + if cfg: + return cfg.site_url + cfg.site_path + return "https://localhost/auth/" def reset_link_url(token: str) -> str: @@ -45,10 +43,10 @@ def reset_link_url(token: str) -> str: def normalize_origin(origin: str) -> str: - """Normalize an origin URL by adding https:// if no scheme is present.""" + """Normalize an origin URL by adding https:// if no scheme is present, removing trailing slashes.""" if "://" not in origin: return f"https://{origin}" - return origin + return origin.rstrip("/") def reload_config() -> None: @@ -74,3 +72,15 @@ def normalize_host(raw_host: str | None) -> str | None: # Strip port from host:port netloc = netloc.rsplit(":", 1)[0] return netloc.lower() or None + + +def format_endpoint(ep: dict) -> str: + """Format an endpoint dict to a listen string (e.g. 'unix:/path' or 'host:port').""" + if uds := ep.get("uds"): + return f"unix:{uds}" + host = ep["host"] + port = ep["port"] + # Bracket IPv6 addresses + if ":" in host: + host = f"[{host}]" + return f"{host}:{port}" diff --git a/paskia/util/permutil.py b/paskia/util/permutil.py index 42791db..faf635a 100644 --- a/paskia/util/permutil.py +++ b/paskia/util/permutil.py @@ -3,7 +3,7 @@ from collections.abc import Sequence from fnmatch import fnmatchcase -from paskia import db +from paskia.authsession import session_ctx from paskia.util.hostutil import normalize_host __all__ = ["has_any", "has_all", "session_context"] @@ -40,4 +40,4 @@ async def session_context(auth: str | None, host: str | None = None): if not auth: return None normalized_host = normalize_host(host) if host else None - return db.data().session_ctx(auth, normalized_host) + return session_ctx(auth, normalized_host) diff --git a/paskia/util/runtime.py b/paskia/util/runtime.py new file mode 100644 index 0000000..df0713c --- /dev/null +++ b/paskia/util/runtime.py @@ -0,0 +1,31 @@ +"""Runtime configuration utilities.""" + +import os +from functools import lru_cache + +import msgspec + +from paskia.db.structs import Config + + +class RuntimeConfig(msgspec.Struct): + """Runtime configuration for the Paskia authentication server. + + Wraps the db Config (CLI/stored settings) with computed runtime fields. + Serialized to PASKIA_CONFIG env var as JSON via msgspec. + """ + + config: Config # CLI/stored configuration to persist + site_url: str # Base URL without trailing path (e.g. https://example.com) + site_path: str # Path to auth UI: "/" if auth_host, else "/auth/" + save: bool = False # Whether to persist config to database + + +@lru_cache(maxsize=1) +def _load_config() -> "RuntimeConfig | None": + """Load RuntimeConfig from PASKIA_CONFIG env var.""" + config_json = os.getenv("PASKIA_CONFIG") + if not config_json: + return None + + return msgspec.json.decode(config_json.encode(), type=RuntimeConfig) diff --git a/paskia/util/startupbox.py b/paskia/util/startupbox.py index f86daf2..9d652f9 100644 --- a/paskia/util/startupbox.py +++ b/paskia/util/startupbox.py @@ -1,14 +1,19 @@ """Startup configuration box formatting utilities.""" +from __future__ import annotations + import os import re from sys import stderr from typing import TYPE_CHECKING +from fastapi_vue.hostutil import parse_endpoints + from paskia._version import __version__ +from paskia.util.hostutil import format_endpoint if TYPE_CHECKING: - from paskia.config import PaskiaConfig + from paskia.util.runtime import RuntimeConfig BOX_WIDTH = 60 # Inner width (excluding box chars) @@ -42,7 +47,7 @@ def bottom() -> str: return "┗" + "━" * (BOX_WIDTH + 2) + "┛\n" -def print_startup_config(config: "PaskiaConfig") -> None: +def print_startup_config(runtime: RuntimeConfig) -> None: """Print server configuration on startup.""" # Key graphic with yellow shading (bright for highlights, dark for body) y = YELLOW # Dark yellow for main body @@ -57,41 +62,40 @@ def print_startup_config(config: "PaskiaConfig") -> None: lines.append( line( f"{b}█{y} {b}█{y}▀▀▀▀{b}█{y}▀▀{b}█{y}▀▀{b}█{r} {w}" - + config.site_url - + config.site_path + + runtime.site_url + + runtime.site_path + r ) ) lines.append(line(f" {y}▀▀▀▀▀{r}")) # Format auth host section - if config.auth_host: - lines.append(line(f"Auth Host: {config.auth_host}")) + if runtime.config.auth_host: + lines.append(line(f"Auth Host: {runtime.config.auth_host}")) + + from paskia.__main__ import DEFAULT_PORT as P # noqa: PLC0415 - circular + from paskia.__main__ import DEVMODE # noqa: PLC0415 - circular # Show frontend URL if in dev mode - devmode = os.environ.get("PASKIA_VITE_URL") - if devmode: - lines.append(line(f"Dev Frontend: {devmode}")) + if DEVMODE: + lines.append(line(f"Dev Frontend: {os.environ.get('PASKIA_VITE_URL')}")) - # Format listen address with scheme - if config.uds: - listen = f"unix:{config.uds}" - elif config.host: - listen = f"http://{config.host}:{config.port}" - else: - listen = f"http://0.0.0.0:{config.port} + [::]:{config.port}" - lines.append(line(f"Backend: {listen}")) + # Format listen endpoints (dev mode only uses the first endpoint) + + endpoints = list(parse_endpoints(runtime.config.listen, P)) + if DEVMODE: + endpoints = endpoints[:1] # server.run reload=True uses only one + parts = [format_endpoint(ep) for ep in endpoints] + lines.append(line(f"Backend: {' '.join(parts)}")) # Relying Party line (omit name if same as id) - rp_id = config.rp_id - rp_name = config.rp_name - if rp_name and rp_name != rp_id: - lines.append(line(f"Relying Party: {rp_id} ({rp_name})")) - else: - lines.append(line(f"Relying Party: {rp_id}")) + rp_id = runtime.config.rp_id + rp_name = runtime.config.rp_name + suffix = f" ({rp_name})" if rp_name and rp_name != rp_id else "" + lines.append(line(f"Relying Party: {rp_id}{suffix}")) # Format origins section - allowed = config.origins + allowed = runtime.config.origins if allowed: lines.append(line("Permitted Origins:")) for origin in sorted(allowed):