Commit Graph

2 Commits

Author SHA1 Message Date
wmantly a0964ce350 feat(multi-site): route replication traffic over the mesh when available
Cross-component routing TODO item: a spoke's resync push now prefers
its WG mesh IP (reported via the noInbound/meshIp fields added for the
relay automation) over the public endpoint, falling back to the public
endpoint if the mesh attempt fails for any reason (tunnel not actually
up between these two particular gateways yet, transient failure,
etc.) -- a mesh-routing preference must never turn into "spoke stops
getting updates."

Plain HTTP over the mesh IP, not HTTPS: the WG tunnel is already
encrypted, same reasoning already applied to the no-inbound relay
terminating at the master.

A spoke with no meshIp on file behaves exactly as before (public
endpoint only) -- this is additive, not a behavior change for spokes
that haven't opted into mesh registration.
2026-08-10 20:39:27 -04:00
wmantly d27763e556 feat(multi-site): live catalog replication + identical-directory signing key
The shipped join flow (v2.2.0-v2.3.0) was a one-time snapshot: a spoke's
catalog never updated after joining. This adds the two pieces that were
explicitly designed but missing:

- Live replication: a spoke registers its own endpoint with the master
  right after joining (POST /api/site/spokes, Bearer join-key), receiving
  a pushToken. Every successful catalog write on the master now fires a
  fire-and-forget resync ping (utils/site_replicate.js) at every known
  spoke, concurrently -- one unreachable spoke never blocks or delays
  another (wired into the existing write-gate middleware in
  api_directory_admin.js). The spoke's POST /api/site/resync handler
  reuses the already-tested export+import path rather than applying a
  partial diff.

- Identical directories: POST /api/site/export now best-effort includes
  the master's agent-signing key; a spoke adopts it via agent_keys.adopt()
  on both join and every resync, so every site's sso-manager can validly
  sign a command for any agent enrolled anywhere -- the accepted tradeoff
  discussed for this deployment's scale (blast radius for simplicity).

New SiteSpoke model tracks registered spokes (endpoint + pushToken);
registered it in models/index.js (a real bug the e2e test below caught --
SiteSpoke.list() 500'd with "Cannot read properties of null (reading
'adapter')" until the model was added to initORM's model list).

Verified end-to-end against docker-compose.multisite-e2e.yml: mint join
key -> join with selfUrl -> write a NEW resource on master post-join ->
poll the spoke -> it shows up within a few seconds via the resync push,
no manual re-join needed. MULTISITE E2E PASS.

Unit tests: nodejs/tests/site_replicate.test.js (concurrent fan-out, one
failing spoke doesn't block another, empty-registry and list()-throws
edge cases).
2026-08-10 16:34:38 -04:00