#1 control passes (harness validated); #2 fails (200 vs expected 409 — false success);
#4 fails (s2sHealthy false — Re-peer reports success on dead peering). Turned green by the fixes.
lookupRemoteUser now maps peer HTTP failures (403/5xx, malformed body) to a
structured {ok:false,reason:'unreachable'} instead of throwing, and the
federated friend-add wraps the call in try/catch as defense-in-depth. A
desynced/unreachable peer no longer surfaces as a raw 500 on a user action.
README.md left unstaged.
Detects when a federated peer is factory-reset on the same domain (via a
persistent instance epoch), routes it to needs_attention (detection-only,
never auto-rekeys), and after an admin re-peer soft-tombstones the dead
incarnation's replicated stubs — clearing stale friendships/DMs while
preserving message history. 12 tasks + a needs_attention detection fix;
server suite 1201/1201. Phase 2 (login-hijack guard, real-account
quarantine, admin Reset-cleanup UI) deferred — see design spec.
Deployed + verified live on nova (Pi) and orbit (VM), commit d8fec00.
A reset peer can reach needs_attention via the auth-failure path (HTTP up,
401/403 from a new incarnation crossing AUTH_FAILURE_THRESHOLD) without ever
passing through unreachable, so the unreachable-only recovery probe never
observes its epoch change and no reset journal is created — leaving a later
manual Re-peer with nothing to heal.
Add detectResetOnNeedsAttentionPeers() to the 15-minute health-check tick:
probe needs_attention peers with a non-null baseline (excluding those already
peer_reset_detected) and call markPeerReset on an observed epoch mismatch.
Detection only — never recovers a needs_attention peer to active; baseline
(peer_instance_id) and hmac_secret untouched.
Add healResetIncarnation (federationReset.ts): fires from onPeerActivated after
an authenticated re-peer to soft-tombstone the flagged pure S2S stubs of a reset
peer's dead incarnation, clearing stale friendships/DMs so the reported bug is
fixed. Two mandatory guards: a reason gate (allow-list of 8 genuine handshake
activation reasons; excludes health_check_recovery + startup_bootstrap so their
stale baseline can never silently resolve a journal without healing) and an
epoch comparison (dead_epoch === newEpoch => false alarm, no tombstone). Uses
tombstoneUser(uid, { purgeContent: false }); real federated accounts are left
flagged + intact for Phase 2. Runs outside any transaction. Wire into
onPeerActivated before the mutation-log re-sync.
Found by running the previously-untested Docker-not-installed path in a
clean, no-dig container:
- resolve the current user with $(id -un), not $USER: under `set -u`
an unset $USER aborted the script with 'USER: unbound variable' right
after Docker was installed ($USER is not guaranteed set under sudo,
`su` without -l, cron, or docker exec)
- make the Docker-install prompt EOF-safe (read ... || yn="") so a
non-interactive / piped stdin falls through to the [Y/n] default
instead of tripping `set -e`
- swallow getent's exit-2 on an unresolved domain (... || true): with
`set -o pipefail` it aborted the DNS check before the graceful
'Could not resolve' warning, breaking installs on minimal hosts (no
dig) that run before DNS is pointed — an explicitly supported flow
- guard the LAN-IP and disk-space pipelines the same way so a no-match
grep / missing `ip` can't abort the summary or prereq checks
- inject the git commit at build time so /api/instance/info advertises
the exact source version on the public clone->install.sh path (AGPL
\xc2\xa713), passed as --build-arg so it survives the sudo/non-sudo split
- pass INSTANCE_NAME through the container env instead of interpolating
it into node -e source; names with quotes/spaces/$ are stored verbatim
and can't break or inject into the program
- add a post-install, NAT-hairpin-safe HTTPS reachability check (curl
--resolve to the local Caddy with full cert verification) and report
an honest 'Live' vs 'Not live yet' status instead of always claiming
success
- list ALL required ports in the summary (80/443 + voice) with explicit
host-firewall vs router port-forwarding guidance and the auto-detected
LAN IP as the forward target; note 7880 must NOT be forwarded
- document the non-interactive env vars (DOMAIN/ENABLE_VOICE/INSTANCE_NAME)
Two mirror-image bugs from voice/DM-call transitions leaving stale state.
DM call → space channel (stuck "Connecting…"):
The last participant to leave a DM call for a space channel receives a
`dm_call_ended` echo (server empties the DM room on their `voice_join`).
The handlers called `disconnectFn()` unconditionally, tearing down the
space room they had just connected to. Route `dm_call_ended` /
`dm_call_rejected` / terminal `dm_call_undeliverable` through a new
`teardownDmCall()` that only disconnects LiveKit when not in a space
channel (`currentVoiceChannelId` null).
Space channel → DM call (still shown as "in" the voice channel):
1. Entering a DM call never cleared `currentVoiceChannelId`, so
`VoiceChannel` mapped the DM call's live LiveKit participants onto the
old space channel. Add `clearSpaceVoiceForDmCall()`, called in
`connect()` when `isDm`, restoring the invariant that a DM call has no
`currentVoiceChannelId`.
2. `dm_call_accepted` gated the caller's connect on `!isLiveKitConnected`,
so a caller already in a space channel was never connected to the DM
room. Gate on `wasOutgoingCall` only (connect() de-dupes same-room).
Tests: teardownDmCall.test.ts, clearSpaceVoiceForDmCall.test.ts.
Docs: docs/systems/voice.md.