Proxy to another Paskia #5

Merged
LeoVasanko merged 9 commits from feature/remote-satellite into main 2026-09-29 03:49:23 +00:00
9 Commits
Author SHA1 Message Date
LeoVasanko b039422f26 Harden satellite: uplink probe in domain dialog, live e2e tests
Review follow-ups for remote domains:

- Domain dialog probes {url}/auth/api/settings as the admin types:
  reachability, rp-id match, and non-nesting (via a new remote flag in
  the settings response) — warnings, never submit blockers
- Reject conflicting settings for domains sharing one remote;
  sanitize aligns with a warning instead
- Forward profile/avatar requests (/auth/api/user/*) to the remote when
  a remote domain's auth host reaches the satellite, never write locally
- Proxied calls answer 503 on transport errors (logout then deliberately
  keeps the local replica session — the remote never saw it)
- Close forwarding httpx clients on reconcile removal and shutdown
- Clamp cache_ttl/refresh_interval server-side; syncfeed.emit is a true
  no-op without subscribers; sync pump tolerates unexpected send errors
- Rebuild listeners: no running loop warns instead of raising, tasks are
  strongly referenced and failures logged
- tests/test_satellite_e2e.py: live over-the-wire suite — real uvicorn
  remote, real replica via reconcile: snapshot, events, write-behind,
  token gate, TTL fail-closed, reconnect reconciliation
2026-09-29 03:44:57 +00:00
LeoVasanko 2a04d7e0d3 Admin UI remote option; docs match simplified protocol
Domain edit dialog: satellite-mode toggle with remote URL, write-only
sync token, cache TTL and re-sync interval, with auth-host requirement
validated before submit; domain list marks remote domains. AdminApp
sends remote wholesale on PATCH (null clears, absent token keeps the
stored one).
2026-09-21 01:14:00 +00:00
LeoVasanko 33e3185b39 Fix sync client resync condition (ready-tracked, not staging) 2026-09-21 01:07:47 +00:00
LeoVasanko ea780a20e1 Host-keyed dispatch in the satellite module; snapshot-only sync
Callers never see stores: session_ctx/verify/user-info resolve the store
from the request host via satellite.store_for_host; session refresh and
logout eviction are dispatch functions too (satellite.refresh_session /
evict_session). API handlers keep one code path plus forward_request
one-liners; proxy.py folds into satellite.py; Domain.store and the
store parameters are gone; 503 comes from the dispatch point as a plain
HTTPException.

The sync protocol drops replay/generation/seq: snapshots are small, so
every connect starts from a full snapshot and a single ordered WebSocket
cannot gap; a slow subscriber is dropped and resyncs. The satellite
reconnects every refresh_interval to reconcile drift.
2026-09-21 01:04:53 +00:00
LeoVasanko 2b21bfb98a Docs: remote satellite configuration, behavior, and design review 2026-09-20 23:46:53 +00:00
LeoVasanko 44364fdffc Fix replica-path leaks and availability semantics from live testing
- _remote_headers and /check used struct convenience properties that
  read the global database; they now use the SessionContext / the handed
  store (also fixes Remote-Credential carrying a struct repr instead of
  the UUID).
- Replica availability: TTL clock starts at disconnect, not at last
  message or failed reconnect; tight WS keepalive for prompt dead-peer
  detection.
- Proxy preserves repeated Set-Cookie via raw headers; sync endpoint
  does its own accept (wsutil decorator pre-accepts) and bypasses host
  dispatch (server-to-server; satellite may use an out-of-domain
  address).
- Admin-credential bootstrap warning skips remote domains.

Verified live with two instances (remote :4501, satellite :4402):
replica snapshot + events, 204 forward with Remote-* in <1ms, validate
write-behind landing on the remote, proxied logout with instant local
eviction, 503 after cache_ttl of disconnect, resync after remote
restart.
2026-09-20 23:45:01 +00:00
LeoVasanko 9394c38179 Remote domain configuration via admin domains API
ApiDomain carries the remote block (sync token write-only, never
echoed); create/patch accept it, validated with the combined config
(auth host mandatory for remote domains). db.update_domain replaces
remote wholesale like the other domain fields.
2026-09-20 23:26:00 +00:00
LeoVasanko 5ecb10166d Add remote domains: RAM replica + RAM-only sync channel
DomainConfig.remote {url, token, cache_ttl, refresh_interval} marks a
domain as backed by a remote paskia instance (auth host required). The
remote publishes committed changes via struct store()/delete() hooks and
explicit emits in field-mutating operations into syncfeed, an in-RAM
sequenced ring buffer served over a token-gated WebSocket
(/auth/api/sync/ws, tokens from PASKIA_SYNC_TOKENS env). The satellite
keeps a plain DB replica per remote URL, applies snapshots/events,
enforces expiry locally, and writes session refreshes back over the same
channel. /validate refreshes locally with write-behind; /logout,
/set-session, /token-info and /auth/oidc/* are proxied to the remote
with the original Host header; logout also evicts from the replica.
Replicas go fail-closed (503) after cache_ttl of silence.
2026-09-20 23:21:08 +00:00
LeoVasanko 20b145d816 Make session read path store-explicit
DB.session_ctx now reads only its own tables instead of struct
convenience properties that reach the global database, so a DB instance
(a read replica) is self-contained. session_ctx/session_context/
authz.verify/build_user_info take an explicit store parameter defaulting
to the local database; api.py endpoints resolve it from the dispatched
domain (Domain.store).
2026-09-20 23:08:56 +00:00