The /api/auth/register handler now branches on homeInstance:
- Local path (no homeInstance): gated by registrationOpen. When closed, a
valid inviteToken bypasses the gate and is consumed atomically inside
redeemInvite()'s transaction (user insert + usedCount bump + redemption
row all commit together, or all roll back). When open, inviteToken is
silently ignored.
- Federated path (homeInstance set): gated by federatedRegistrationOpen.
Token is ignored entirely on this path -- tokens never unlock federated
creation. Closed → 403 with "Federated registration is closed".
InviteUnavailableError thrown by redeemInvite() (concurrent revoke,
last-slot race, expiry between check-invite and submit) is mapped to 403
"Invalid or expired invite". The in-txn re-derive closes the TOCTOU window.
9 new tests cover the toggle matrix from spec §5.6 + invite consumption
semantics + federated-gate independence + last-slot race rejection.
Updates docs/systems/auth.md: rewrites the Registration Gate section to
describe the three-path model (open / invite / federated), adds the toggle
matrix, adds an Invite Tokens subsection with the atomic-redemption shape,
notes that the federated stub upgrade is always gated by
federatedRegistrationOpen, never by an invite token.
Mirrors the REST-login change in f17c46c on the registration insert path.
A successful POST /api/auth/register does not by itself imply a live
WebSocket — the client may never connect (transient network, mobile
background, error path between the 201 and /ws), leaving a permanently
stuck-online row that no disconnect timer can clean up. The schema
default 'offline' is correct; ws/handler.ts flips it to 'online' on real
WS auth.
The federated-stub upgrade path in the same handler is unaffected: it
only updates passwordHash/username/homeUserId/displayName/avatarColor,
leaving the stub's pre-existing 'offline' status (set when the stub was
created via replication) untouched.
Updates docs/systems/auth.md step 7 to reflect the new behavior.
users.status was only flipped back to offline by the WebSocket disconnect
path (5s grace timer in ConnectionManager). Process exits (deploy/crash/OOM)
lose those in-memory timers, freezing any non-offline row at its last value
and making the user appear permanently online to friends and space co-members.
Confirmed in production on the Pi instance: a user appeared online for ~3
days with no live socket.
Add resetStalePresenceOnBoot() in utils/presenceBoot.ts and call it from
index.ts after getDb()/seedDatabase() and before WebSocket route registration.
The reset is federation-safe: it only updates rows where home_instance IS
NULL (replicated stubs are projections of remote presence and must not be
stomped) and is_deleted = 0 (tombstoned users are excluded from broadcasts).
Also remove the redundant status='online' write from POST /api/auth/login.
A successful REST login does not imply a live socket; the WS auth handshake
is the single source of truth. Login alone could otherwise produce the same
stuck-online row when a client logs in and never establishes a WS.
Tests cover: locally-homed online/idle/dnd reset, replicated rows untouched,
tombstoned rows untouched, idempotence, mixed populations.
Updates docs/systems/activity-presence.md (Connect/Disconnect Flow, new Boot
Reset section) and docs/systems/auth.md (login no longer mutates status).