Skip to content

feat(share): end-to-end encrypt invite-link terminal sessions - #561

Merged
kipavy merged 1 commit into
devfrom
feat/link-session-e2ee
Oct 7, 2026
Merged

kipavy merged 1 commit into
devfrom
feat/link-session-e2ee

Conversation

@kipavy

@kipavy kipavy commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Closes #521. Server side: VoltiusApp/server#98, already deployed (sha-9208738).

Invite-link sessions uploaded the raw session key, and the server gave it to anyone holding the link or a short code. Now the link and the code only let a guest in. The host's client wraps the key for each guest the server admits, the same way People invites already work.

Host

  • createInviteLinkSession stops sending session_key_bytes. It now calls prepareWrappedSessionKey([]), which also publishes the host's public key: guests unwrap against that key. It keeps sessionKeyBytes on the connection.
  • key_request {user_id} on the socket wraps the key through inviteUserToSession, the same path a People invite takes, with a fresh public-key read. inviteUserToSession now takes a user id, since that's all it ever used, and the store's invite and key_request share one wrapKeyForUser helper.

Guest

  • getMySessionKey returns "pending" on 202. Before returning it publishes the guest's own public key, because the socket admission that triggers key_request must not race that publish.
  • joinSession no longer waits for the key. The tab opens right away and MultiplayerBar shows "Waiting for the host…". openWebSocket accepts a key promise, and output that arrives early (the history replay) waits on it, in order.
  • key_ready triggers a new my-key fetch and an unwrap. After 30 s without it, the socket closes and the bar reads "Host didn't share the key".
  • A raw_key from an older host still imports, for one release.

The socket frames are key_request and key_ready, with underscores like the other frames, not the hyphenated names in the issue.

Also

joinSession now reuses makeCallbacks instead of carrying its own copy (repo DRY rule). The guest keeps its no-op onInput: the host's relay must never write into a guest's local session. A regression test covers that; I confirmed it fails without the override.

Compatibility

  • New client against today's server: works for both host and guest.
  • Old guest on a new host's session: gets the 202 and fails. Server#96 accepts this, so ship this client to everyone at once.

Tests

  • Service: no session_key_bytes is sent and the host key is published; 202 gives "pending" and publishes first; the wait succeeds on key_ready, times out with no fetch, and fails if still pending after key_ready.
  • WS: key_request and key_ready dispatch to their handlers; early output is held and delivered in order.
  • Store: the link host answers key_request with exactly one wrap and POST; a pending guest goes waiting, then key_ready, then the key; a failed wait ends the session and closes the socket; a guest never relays input.
  • tsc --noEmit is clean. All 53 multiplayer, terminal and members test files pass (428 tests, plus the new ones). One full-suite run hit a 5 s timeout in an unrelated test (TriStateToggle) under load; it passes when run alone.

Still to do

  • End-to-end test with two real accounts against the deployed server, joining by link and by short code.
  • After release, update landing/content/blog/shared-terminals.mdx in VoltiusApp/web, which still says link sessions aren't E2EE.

🤖 Generated with Claude Code

The host no longer uploads the raw session key for an invite-link
session. The link and short code only admit; the server asks the host's
socket to wrap the key for each admitted guest (VoltiusApp/server#96).

- Host: createInviteLinkSession goes through prepareWrappedSessionKey,
  so it publishes the host's public key and keeps sessionKeyBytes. A
  key_request frame wraps for that guest through the same path as a
  People invite (inviteUserToSession now takes a user id; it only ever
  used that).
- Guest: my-key answering 202 publishes the guest's public key before
  the socket opens, so the host wraps to the current key. joinSession no
  longer blocks on the key: the tab opens, the bar reads "Waiting for the
  host…", and frames that arrive first wait on the pending key in order.
  key_ready fetches and unwraps it; after 30 s the session ends with
  "Host didn't share the key".
- raw_key from an older host is still imported, for one release.
- joinSession reuses makeCallbacks instead of a copy of it, keeping the
  guest's no-op onInput.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kipavy
kipavy merged commit 69ddb1b into dev Oct 7, 2026
4 checks passed
@kipavy
kipavy deleted the feat/link-session-e2ee branch October 7, 2026 10:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant