Desktop: call page on its own loopback origin, opt-in (#43) #252

Merged
jared merged 1 commits from desktop-call-origin into lotus 2026-09-29 09:34:49 -04:00
Owner

Part of #43, for the desktop app. The web moved the call page to call.chat.lotusguild.org. The desktop app still loads the bundled call page from its own origin (http://localhost:44548), so the call frame can read the app's localStorage (login token) and DOM.

Change

  • New origin: the desktop's local server also answers on http://127.0.0.1:<port>. That's the same server and bundle, but a different origin, and still a secure context. The new resolveDesktopCallPageUrl loads the bundled call page from there when the desktop config sets desktopCallOrigin.
  • Strict validation: only a loopback http://127.0.0.1 origin on the same port as the app is accepted, with no path, query or credentials. It applies only when the app itself runs on http://localhost (release builds).
  • Inert until the desktop opts in: merging this changes nothing anywhere. Every desktop build keeps the same-origin page until cinny-desktop sets desktopCallOrigin, together with the server bind, CSP and permission changes it needs (cinny-desktop PR, linked below).
  • Web app: unchanged.

Tested

Simulated desktop app: Tauri bridge stub plus the desktop config.json, served on both localhost and 127.0.0.1, against a local Synapse + LiveKit with two users. Chromium is the Windows (WebView2) engine.

check split origin same-origin control
call page origin http://127.0.0.1:44600 http://localhost:44600
parent.localStorage / parent.document from the frame SecurityError / SecurityError readable / readable
join (2 tiles), speaking indicator, mic off/on, screenshare start/stop, layout, hang up ✅ ✅
full run 12/12 5 of 6 runs 3 of 4 runs

The misses were the same on both sides: the local LiveKit connection ("Couldn't connect to voice").

  • Unit tests: 1,290 pass, including new ones for the resolver: accepted values, rejected values, and non-localhost app origins.
  • Playwright: 20 passed.
  • Also verified on the real Linux desktop binary: the frame loads from 127.0.0.1 under the app's CSP and is isolated.

Not tested: a real call in the Windows app. That's needed on the cinny-desktop PR before it merges.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA

Part of **#43**, for the desktop app. The web moved the call page to `call.chat.lotusguild.org`. The desktop app still loads the bundled call page from its own origin (`http://localhost:44548`), so the call frame can read the app's `localStorage` (login token) and DOM. ## Change - **New origin:** the desktop's local server also answers on `http://127.0.0.1:<port>`. That's the same server and bundle, but a different origin, and still a secure context. The new `resolveDesktopCallPageUrl` loads the bundled call page from there when the desktop config sets **`desktopCallOrigin`**. - **Strict validation:** only a loopback `http://127.0.0.1` origin on the **same port** as the app is accepted, with no path, query or credentials. It applies only when the app itself runs on `http://localhost` (release builds). - **Inert until the desktop opts in:** merging this changes nothing anywhere. Every desktop build keeps the same-origin page until cinny-desktop sets `desktopCallOrigin`, together with the server bind, CSP and permission changes it needs (cinny-desktop PR, linked below). - **Web app:** unchanged. ## Tested **Simulated desktop app:** Tauri bridge stub plus the desktop `config.json`, served on both `localhost` and `127.0.0.1`, against a local Synapse + LiveKit with two users. Chromium is the Windows (WebView2) engine. | check | split origin | same-origin control | |---|---|---| | call page origin | `http://127.0.0.1:44600` | `http://localhost:44600` | | `parent.localStorage` / `parent.document` from the frame | **SecurityError** / **SecurityError** | readable / readable | | join (2 tiles), speaking indicator, mic off/on, screenshare start/stop, layout, hang up | ✅ | ✅ | | full run 12/12 | 5 of 6 runs | 3 of 4 runs | The misses were the same on both sides: the local LiveKit connection ("Couldn't connect to voice"). - **Unit tests:** 1,290 pass, including new ones for the resolver: accepted values, rejected values, and non-localhost app origins. - **Playwright:** 20 passed. - **Also verified on the real Linux desktop binary:** the frame loads from 127.0.0.1 under the app's CSP and is isolated. **Not tested:** a real call in the Windows app. That's needed on the cinny-desktop PR before it merges. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
jared added 1 commit 2026-09-29 00:10:07 -04:00
feat(desktop): call page on its own loopback origin, opt-in (#43)
CI / Build & Quality Checks (pull_request) Successful in 1m46s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 8s
CI / Playwright smoke (e2e) (pull_request) Successful in 10m24s
7d7a379ce0
The desktop app loads the bundled Element Call page from its own origin
(http://localhost:<port>), so the call frame can read the app's storage
(login token) and DOM — the hole #43 closed on the web by moving the page
to call.chat.lotusguild.org.

The desktop's local server can also answer on http://127.0.0.1:<port>: the
same server and bundle, but a different origin (and still a secure
context). resolveDesktopCallPageUrl loads the bundled page from there when
the desktop config sets `desktopCallOrigin`:
- only a loopback http origin on the SAME port as the app, no path, query
  or credentials;
- only when the app itself runs on http://localhost (release builds; debug
  builds on tauri:// keep the same-origin page);
- unset (every desktop build until cinny-desktop opts in, together with the
  server bind, CSP and permission changes it needs): unchanged.

The web app is unchanged (elementCallUrl as before).

Tested in a simulated desktop app (Tauri bridge stub + the desktop
config.json, served on localhost and 127.0.0.1) against a local Synapse +
LiveKit, two users: call page from http://127.0.0.1:<port>, parentUrl =
the app origin; the frame gets SecurityError on parent.localStorage and
parent.document (same-origin control: readable); join, speaking indicator,
mic off/on, screenshare start/stop, layout switch and hang-up all work, no
page errors — 12/12 in 5 of 6 runs, like the same-origin control (3 of 4;
the misses on both sides were the local LiveKit connection).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
jared merged commit 23f059d9a4 into lotus 2026-09-29 09:34:49 -04:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: LotusGuild/cinny#252