CLI: paskia satellite <wss-url> — one-step satellite-only setup

The same register_uplink path as the admin UI, runnable without any web
interface (a satellite may be a purely local instance serving backend
apps on one server): the handshake runs immediately, the link credential
and pushed domains are stored, rejections are printed. With no database
yet, one is created first (localhost domain + admin bootstrap) — the
command is the whole setup; 'paskia' then serves the linked domains.
This commit is contained in:
LeoVasanko committed 2026-10-01 06:09:14 +00:00
1 parent 0f38a8b096
commit 3a6e2bb370
2 files changed
+78 -1

No files matched your search

+72
View File
@@ -28,6 +28,7 @@ EPILOG = """\
Examples:
paskia init example.com "Example Corporation"
paskia migrate example.com
paskia satellite wss://key@auth.example.com
paskia --listen 4402 --save
paskia
"""
@@ -190,6 +191,60 @@ def cmd_migrate(args: argparse.Namespace) -> None:
print(f"✅ {action} {db_file_path()} (domains: {', '.join(rp_ids)})")
def cmd_satellite(args: argparse.Namespace) -> None:
"""Register this instance as a satellite using an upstream's link URL.
The one setup step for a satellite-only installation: with no database
yet, one is created first (default localhost domain and admin
bootstrap, like 'paskia init'). The registration handshake runs right
here — the URL's initial key is spent, the permanent link credential
and the pushed domains are stored. Afterwards 'paskia' serves the
linked domains.
"""
from paskia import satellite
from paskia.db.lifecycle import kanta
db_path = db_file_path()
fresh = not db_path.exists()
result = {}
if fresh:
# Satellite-only bootstrap: default config (localhost domain) and
# the standard admin seed.
@kanta.bootstrap
def _bootstrap(data: DB) -> None:
result["passphrase"] = bootstrap(data, config=Config())
async def _register():
try:
await kanta.open()
except Exception as e:
raise SystemExit(
f"Could not open {db_path} (is a server running?): {e}"
) from e
try:
return await satellite.register_uplink(args.url)
finally:
await kanta.close()
try:
info = asyncio.run(_register())
except (ValueError, RuntimeError) as e:
raise SystemExit(f"Registration failed: {e}") from e
domains_list = ", ".join(info["domains"]) or "(none yet — the upstream can scope the link later)"
print(f"🛰 Linked to {info['url']} — serving: {domains_list}")
for rp_id, reason in (info.get("rejected") or {}).items():
print(f"⚠️ Domain {rp_id} rejected: {reason}")
if fresh:
print("Created the database. Serve with: paskia")
configure_domains(listen=None)
registry = build_registry(_load_stored_config(db_path))
log_reset_link(
registry.get("localhost").reset_link_url(result["passphrase"]),
"Admin registration (for the local admin interface):",
)
def cmd_serve(args: argparse.Namespace) -> None:
"""Open the combined database and serve all configured domains."""
db_path = db_file_path()
@@ -297,11 +352,28 @@ def main():
"in the current directory is selected automatically.",
)
satellite_parser = argparse.ArgumentParser(
prog="paskia satellite",
description="Register this instance as a satellite of an upstream "
"paskia instance, using the link URL minted in the upstream's admin "
"interface (Satellites panel). The registration handshake runs "
"immediately; afterwards 'paskia' serves the linked domains. With no "
"database yet, one is created first — this is the whole setup of a "
"satellite-only installation.",
)
satellite_parser.add_argument(
"url",
help="The link URL (wss://<key>@<auth-host>) from the upstream's "
"Satellites panel. Single-use; valid for 10 minutes from minting.",
)
argv = sys.argv[1:]
if argv and argv[0] == "init":
cmd_init(init_parser.parse_args(argv[1:]))
elif argv and argv[0] == "migrate":
cmd_migrate(migrate_parser.parse_args(argv[1:]))
elif argv and argv[0] == "satellite":
cmd_satellite(satellite_parser.parse_args(argv[1:]))
else:
cmd_serve(parser.parse_args(argv))
+6 -1
View File
@@ -309,7 +309,12 @@ async def register_uplink(url: str) -> dict:
domains.init_registry(db.data().config)
if manager.started:
await manager.sync_uplinks()
return {"link": link_id, "domains": sorted(accepted), "rejected": rejected}
return {
"link": link_id,
"url": base_url,
"domains": sorted(accepted),
"rejected": rejected,
}
class UplinkReplica: