From 4152052c90896c2638c607cd9c72428ecbc63753 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Tue, 8 Sep 2026 01:42:28 +0000 Subject: [PATCH] Startup box: serve-only, per-domain clickable URLs, compact sign-in summary - Print the box only when serving; 'paskia init' output is the reset link, which already carries the full auth site URL - Domain row is always 'Domain:'; rows beneath it are unlabeled, belonging to the domain by position - Multi-domain: each domain's auth site printed as a full clickable URL (auth host root when marked, else /auth/) - In-domain sign-in sites collapsed to a one-line summary ('example.com and all subdomains, +N sites'); related origins are few and surprising, so always listed in full --- paskia/__main__.py | 1 - paskia/util/startupbox.py | 60 ++++++++++++++++++++++++++------------- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/paskia/__main__.py b/paskia/__main__.py index 4377a8d..83d7f82 100644 --- a/paskia/__main__.py +++ b/paskia/__main__.py @@ -171,7 +171,6 @@ def cmd_init(args: argparse.Namespace) -> None: configure_domains(listen=config.listen) registry = build_registry(config) - startupbox.print_startup_config(registry, listen=config.listen) log_reset_link( registry.get(rp_id).reset_link_url(result["passphrase"]), "✅ Bootstrap completed!", diff --git a/paskia/util/startupbox.py b/paskia/util/startupbox.py index 208c664..c363ed7 100644 --- a/paskia/util/startupbox.py +++ b/paskia/util/startupbox.py @@ -10,15 +10,13 @@ from typing import TYPE_CHECKING from fastapi_vue.hostutil import parse_endpoints from paskia._version import __version__ +from paskia.domains import auth_host_url, origin_url, partition_origins from paskia.util.constants import DEFAULT_PORT, DEVMODE -from paskia.util.hostutil import format_endpoint +from paskia.util.hostutil import format_endpoint, wildcard_base if TYPE_CHECKING: from paskia.domains import DomainRegistry -from paskia.db.structs import OriginEntry -from paskia.domains import is_related_key, origin_url - BOX_WIDTH = 60 # Inner width (excluding box chars) # ANSI color codes @@ -51,6 +49,25 @@ def bottom() -> str: return "┗" + "━" * (BOX_WIDTH + 2) + "┛\n" +def _signin_summary(in_domain: list[str]) -> str: + """One-line summary of a domain's in-domain sign-in sites.""" + phrases = [] + for key in sorted(in_domain): + if base := wildcard_base(key): + phrase = ( + f"{base} and all subdomains" + if key.startswith("**.") + else f"subdomains of {base}" + ) + else: + phrase = origin_url(key) + phrases.append(phrase) + if len(phrases) > 2: + n = len(phrases) - 1 + return f"{phrases[0]}, +{n} site{'s' if n > 1 else ''}" + return ", ".join(phrases) + + def print_startup_config( registry: DomainRegistry, listen: list[str] | None = None ) -> None: @@ -70,8 +87,7 @@ def print_startup_config( lines.append( line( f"{b}█{y} {b}█{y}▀▀▀▀{b}█{y}▀▀{b}█{y}▀▀{b}█{r} {w}" - + domains[0].site_url - + domains[0].site_path + + domains[0].auth_site_url + r ) ) @@ -89,24 +105,28 @@ def print_startup_config( parts = [format_endpoint(ep) for ep in endpoints] lines.append(line(f"Backend: {' '.join(parts)}")) + multi = len(domains) > 1 for domain in domains: - # Domain line (omit name if same as id) + # Domain line (omit name if same as id); the rows beneath it belong + # to the domain by position, so they carry no labels of their own. rp_name = domain.rp_name suffix = f" ({rp_name})" if rp_name and rp_name != domain.rp_id else "" - header = "Domain: " if len(domains) > 1 else "Relying Party: " - lines.append(line(f"{header}{domain.rp_id}{suffix}")) - if len(domains) > 1: - lines.append(line(f" URL: {domain.site_url}{domain.site_path}")) - for key, props in sorted(domain.config.origins.items()): - marker = ( - " (auth host)" - if isinstance(props, OriginEntry) and props.auth_host - else "" - ) - label = "Related:" if is_related_key(domain.rp_id, key) else "Origin:" - lines.append(line(f" {label:<14}{origin_url(key)}{marker}")) + lines.append(line(f"Domain: {domain.rp_id}{suffix}")) + if multi: + lines.append(line(f" {domain.auth_site_url}")) + in_domain, related = partition_origins(domain.rp_id, domain.config.origins) + # The auth host is already presented as the domain's URL, so it is + # not counted among the sign-in sites. + auth_url = auth_host_url(domain.config) + in_domain = [k for k in in_domain if origin_url(k) != auth_url] if not domain.config.origins: - lines.append(line(" Origins: (none configured)")) + lines.append(line(" (none — no site may sign in)")) + elif in_domain: + lines.append(line(f" {_signin_summary(in_domain)}")) + # Related origins are few (capped) and genuinely surprising cross-domain + # info, so they are always listed in full. + for key in sorted(related): + lines.append(line(f" {origin_url(key)}")) lines.append(bottom()) stderr.write("".join(lines))