4 Commits
Author SHA1 Message Date
Juan Denis 386cd6aa3a fix(connect): stop the client loop when poll mode learns of revocation
_poll_session reports 'revoked' when ServerKit Cloud refuses the poll
with the revoked reason, but the loop only honored 'stopped' and fell
through to re-probe the websocket: ws refused, poll revoked, ws
refused — forever, and the panel never wrote the revoked state. This
is the path a panel behind a close-code-stripping edge depends on, old
builds unconditionally and current ones whenever they are in limited
mode. The poll outcome now gets the same terminal handling as the
websocket one.
2026-09-12 15:09:28 -04:00
Juan Denis 60f8bd8c60 fix(connect): learn revocation from the relay's close-reason frame
The production edge strips non-standard WebSocket close codes, so the
relay's 4009 never reached the panel: a revoked panel kept reconnecting
forever instead of stopping. ServerKit Cloud's relay now sends the
reason as a text frame before closing, and the panel honors it both
during the hello exchange and mid-session, ending the session as
revoked either way. The numeric code still serves direct connections,
and the long-poll fallback's HTTP refusal is unchanged.
2026-09-12 06:31:50 -04:00
Juan Denis fda418d5fa fix(connect): refetch the JWKS when a command arrives signed with an unknown key
ServerKit Cloud mints each signing key the first time it is used and
rotates keys on its own schedule, so a command can legitimately arrive
signed with a key the connection's cached JWKS predates. The panel used
to refuse it as untrusted; it now refetches the JWKS once on an unknown
kid and only refuses a key that is still unknown afterwards.
2026-09-11 22:16:40 -04:00
Juan Denis 232c6978c5 feat(connect): hold a live outbound connection to the relay
- app/services/connect_client.py: RelayClient background thread (WS hello
  signed device_id|ts|nonce, 20s pings, jittered 1s->60s backoff with reset
  after 2 stable minutes, >6 reconnects/10min flap log, close-code ->
  state_reason mapping, terminal revoked, long-poll fallback with 15min WS
  re-probe); connect-state.json runtime state merged into status();
  disconnect() stops the client and removes the state file; run_doctor()
  implements the connect check table
- app/services/connect_keys.py: strict load_keypair() for the transport
- app/__init__.py: start_client_if_paired() next to the linked-panel hook
- cli.py: connect status shows transport/last-seen/relay-instance; new
  connect doctor command (exit 1 on hard-check failure)
- requirements.txt: websockets==15.0.1 (WS client; the SocketIO stack is
  server-side only)
- tests/test_connect_relay.py: 21 unit tests for backoff, flap detection,
  close-code mapping, hello signature, state file, loop transitions
2026-09-02 10:31:26 -04:00