test_remote.py: link URL parsing, RAM-only offer mint/spend/expiry,
pushed-domain application (accept/idempotence/removal/local conflict/
auth-host collision), admin link lifecycle (offer, scope, revoke) and
uplink list/unlink, remote-write rejection on the domains API, live feed
scope mutation.
test_satellite_e2e.py: real registration handshake over TCP (key material
identical on both sides, duplicate and bad-URL rejections), established
link reconnect, revocation unlinking the satellite, live scope shrink
stopping the feed, domain conflicts reported upstream.
Also: ruff format drift fixes in startupbox/test_cli.
Configuration is one copy-paste of a wss link URL (Satellites panel or
the paskia satellite CLI), registration is the online 3-way handshake,
scope is adjustable post-registration, the upstream is authoritative for
pushed domains, revocation unlinks both sides.
The domain edit dialog returns to pure local domain editing — the remote
toggle, sync URL field, uplink probing and per-domain token management
are gone (the bad checkbox-on-its-own-line UX with them).
Satellite setup now lives in a self-contained Satellites section of the
admin overview, covering both roles:
- Upstream: links table (satellite label, served domains, live status,
the satellite's rejection reports), New satellite link dialog with
domain scope checkboxes minting the wss link URL (shown once, 10 min,
single-use), scope editing pushed live, revocation with confirm.
- Satellite: uplinks table (upstream URL, served domains, live
connected/cached/offline status with gentle polling while
disconnected), Add uplink dialog taking the pasted link URL with the
registration handshake running synchronously (immediate errors),
unlink with confirm.
Remote domains in the domains list are no longer clickable/editable or
deletable — they carry a 'via uplink' badge pointing at the Satellites
panel.
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.
/satellites (upstream role): mint registration offers (RAM-only initial
keys shown once as a wss link URL), list links with live state and the
satellite's domain rejections, adjust scope (pushed live), revoke (drops
connections; the satellite unlinks on the policy close).
/uplinks (satellite role): register from a pasted link URL with the full
handshake running synchronously in the request (immediate feedback), list
uplinks with served domains and replica state, unlink.
Domain writes no longer accept a remote object: remote domains are
uplink-managed. Editing a domain pushes its new configuration to the
links serving it; deleting one shrinks their scopes.
Replace pre-minted sync tokens with links created by an online
registration dialogue. The auth host mints a RAM-only, short-lived,
single-use registration offer shown as a wss://<initial-key>@<host> URL
(a scheme browsers refuse to open — the URL is only for a satellite to
connect to). The existing sync WebSocket endpoint multiplexes both
flows on the first frame:
satellite -> upstream hello {link: null, key: <initial key>}
upstream -> satellite welcome {link, key: <server part>, domains}
satellite -> upstream ready {key: <client part>, rejected}
The permanent credential is created during the handshake from parts both
sides contribute; both sides then store the link in their databases
(SatelliteLink / Uplink, with a key: bytes field). Placeholder
cryptography: parts are exchanged as-is, the final version derives the
channel key from the handshake. Established links run the same dialogue
with hello {link, key} as proof; JSON frames follow as before (to be
transparently encrypted in the final version).
Scope is no longer bound to the credential: a link's rp-id set is
adjustable on the upstream at any time and pushed live as a domains
frame — register once, adjust scope later. The upstream is authoritative
for the pushed domains (rp-id, name, origins verbatim); the satellite
validates them against its local config (conflicting rp-ids and origin
collisions are rejected and reported in the ready/ack frames), persists
the accepted set, and rebuilds its registry.
Satellite side: one replica connection per uplink (not per domain),
keyed by link; dispatch resolves host -> domain -> uplink. A policy
close (1008) at the handshake means the link was revoked — the satellite
unlinks its side. Registration is one function (register_uplink) shared
by the admin UI and the CLI.
Registration simplified to a single sync URL: minted in the upstream's
admin UI (Satellite access section of the domain dialog), carrying the
token as userinfo — https://<token>@<auth-host>. Tokens live hashed in
the upstream database, scoped to one rp-id, revocable (drops live
connections). PASKIA_SYNC_TOKENS env is gone.
Data minimization: the sync feed now carries only the scope's sessions
and credentials; the user graph never leaves the upstream. Each session
upsert embeds its master-computed ApiSessionContext (identity display
data + effective permissions for the session's host); user-graph
mutations eagerly re-push affected sessions with fresh contexts, so
revocations land within ~1 RTT. /check and /user-info are proxied to
the upstream (they need the user graph).
Satellite side: replicas keyed by rp-id (no more shared-replica config
conflicts — each remote domain has its own token), the replica refuses
to go live on a ready-message scope mismatch, write-backs are confined
to the token's scope server-side.
Admin dialog: one sync-URL paste field (token split from userinfo,
write-only), uplink verification required to save (reachability, rp-id
match, no nesting, auth host present); a verified uplink takes over the
domain's origins (**.<rp-id> + upstream auth host) and auth host. New
domains adopt the upstream's rp-id and name.
Geographically separate servers may have notable latency between them, making forward/validate request roundtrip unnecessarily slow. This adds a proxy support, where the local server can keep a local copy of a remote domain, while the auth host remains authoritative, but requests can be answered in less than 1ms from local copy.
Reviewed-on: https://git.zi.fi/LeoVasanko/paskia/pulls/5
The restricted entry validates the session before rendering anything
(no load-time flash): 401/403 switches to the existing login component
in place of the profile, success renders the panel fully populated via
props, other failures show a minimal card with Back only. The dialog
drops the standalone page's heading and help text.
The overlay now tracks which dialog kind is open: auth-success from a
profile dialog resolves 'login', auth-back only rejects
AuthCancelledError for auth dialogs.
The Vite dev server now maps /paskia-js/ to the local paskia-js build
(the examples page's module import has 404'd since it was introduced,
leaving all buttons dead). Navigation actions on the examples page are
now plain same-window links so the flows' back navigation returns to
the page; added a Profile Summary button exercising profile().
The restricted page's early script now honors the URL theme parameter
before the cached profile theme, so a fresh server-provided override
wins and the host color scheme applies without a flash. After session
load, an empty profile theme clears the cache but keeps the URL
parameter in effect instead of reverting to the system default.
HostProfileView now renders the same centered frame card as the login
flows, whether shown full-page at /auth/ (host mode) or inside the new
#mode=profile restricted iframe. The component self-fetches its data
when the parent does not provide it, and emits back/logout so each
context reacts appropriately: the full page reloads, the iframe posts
auth-back / auth-logout to the host.
New profile() function opens the minimal profile in a compact dialog
iframe and always resolves ('logout' | 'back'), keeping the auth flow's
resolve/reject contract separate and unchanged. The overlay now injects
the host page's computed color-scheme into the iframe URL theme param
when the server has not provided one.
The **.{rp-id} wildcard was only added when the resulting origins dict
was empty, so a legacy database with a dedicated auth host but no
configured origins ended up allowing only the auth host.
- paskia migrate accepts an rp-id, a legacy *.paskiadb path, or a
current-format *.kantadb path; with an existing target database the
incoming data is merged (uuid-keyed records make conflicts a non-issue,
domains merge per rp-id with a union of origins)
- Migration transactions are labeled migrate:cli:{rp-id} (slash-joined
for multi-domain sources) instead of 'bootstrap'
Break the monolithic admin dialog component into a thin dispatcher plus
one component per dialog type under admin/dialogs/, with a shared
AdminDialog frame (Modal wrapper, title, error and Cancel/Save actions).
No functional change. Also drop two unused input refs (nameInput,
displayNameInput).
- 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 <site>/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
- Serve multiple domains (RP IDs) from one instance: host-based dispatch,
per-domain credentials and sessions, domains managed at runtime in the
admin UI — previously one RP per instance
- Cross-domain sign-in via Related Origin Requests: per-domain related-origins
list with a served .well-known/webauthn document
- Explicit per-domain origin lists with shell-glob wildcards (**. for apex +
any subdomain depth, *. for one level), editable in the admin UI with
validation and self-lockout guards
- Per-domain auth hosts: the account/admin UI can live on a different host
per domain, no longer confined to subdomains of a single RP
- CLI: 'paskia init <rp-id [rp-name]' initializes or adds a domain to an
existing database; 'paskia migrate' converts legacy databases
BREAKING CHANGES (v2.0):
- Database schema: config is now per-domain and credentials/sessions carry
an rp_id — existing databases must be converted with 'paskia migrate'
- Origins are now explicit: main implicitly allowed every subdomain of the
RP; configure '**.' origins to reproduce that behavior
- CLI: the flat '--rp-id/--rp-name/--origin/--auth/--save' flags are
replaced by the 'init' and 'migrate' subcommandsReviewed-on: https://git.zi.fi/LeoVasanko/paskia/pulls/4
/auth/api/forward?public=1 passes requests through with a Remote-Public
header (anonymous/forbidden/authenticated) instead of 401/403, so routes
can allow anonymous visitors while still identifying logged-in users.
Reauth (max_age) still requires the auth flow. Documented in Headers.md,
api/forward.md, Integration.md and all proxy guides.
perm=a|b+c now means (a or b) and c; repeated perm args remain ANDed.
Out-of-spec values (empty alternatives, chars outside the scope charset,
stray %2B) are rejected with 400 instead of being silently misparsed;
extra spaces between groups are tolerated. Forward endpoint 400/500
details name /auth/api/forward as origin without echoing query args.
Wildcards are now filename-like: * stays within a :- or /-separated
segment, ** spans segments, partial segments allowed. Slash added to
allowed scope characters for path-based permissions.
The vendored db/logging module duplicated what kanta now provides:
diff formatting, UUID-to-label resolution via logfmt callbacks, unsafe
character filtering and value truncation. Censoring of oidc.key material
moves into the format_log_uuid logfmt callback in db.lifecycle, taking
care to hide only the value, not the 'key' path component itself.
- Make use of its new features and cleanup our interfacing and init/shutdown processes and migrations
- Clean up circular deps, simplify app init
- Add specific pytest for CLI main to cover the changes