Skip to content

feat(staged): add browser-accessible web mode - #709

Draft
matt2e wants to merge 5 commits into
mainfrom
mobile-web
Draft

matt2e wants to merge 5 commits into
mainfrom
mobile-web

Conversation

@matt2e

@matt2e matt2e commented May 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds a web server mode that serves the Staged UI via HTTP, enabling browser-based access without the Tauri desktop shell
  • Implements a transport abstraction layer so the frontend can communicate over either Tauri IPC (desktop) or HTTP/WebSocket (web)
  • Broadcasts web project setup progress events and fixes missing function arguments in web server handlers

Test plan

  • Verify desktop app continues to work normally via Tauri IPC
  • Start the web server and confirm the UI loads in a browser
  • Test session launching, branch operations, and event streaming via the web transport
  • Confirm rebase/squash pipeline commands work through the web API

@matt2e
matt2e force-pushed the mobile-web branch 5 times, most recently from 808c1dd to 075b8bf Compare May 15, 2026 08:15
@matt2e
matt2e force-pushed the mobile-web branch 2 times, most recently from 6b9a11f to 5dcf4a0 Compare June 28, 2026 03:35
@matt2e
matt2e force-pushed the mobile-web branch 3 times, most recently from d42d13f to 9c5f909 Compare July 9, 2026 03:26
@matt2e
matt2e force-pushed the mobile-web branch 2 times, most recently from 62e8088 to b5016a7 Compare July 30, 2026 07:41
@matt2e
matt2e force-pushed the mobile-web branch 2 times, most recently from 60c1853 to c9d8534 Compare August 11, 2026 10:51
@matt2e
matt2e force-pushed the mobile-web branch 3 times, most recently from 55ac096 to fde912a Compare September 3, 2026 05:08
@matt2e
matt2e force-pushed the mobile-web branch 2 times, most recently from be89621 to c3c3c23 Compare September 4, 2026 07:08
@matt2e
matt2e force-pushed the mobile-web branch 3 times, most recently from b46417f to 66e81c9 Compare October 15, 2026 12:08
@matt2e
matt2e force-pushed the mobile-web branch 3 times, most recently from 5ca0602 to 9451a97 Compare October 28, 2026 12:44
@matt2e
matt2e force-pushed the mobile-web branch 3 times, most recently from 70b355a to 9d6a43e Compare November 2, 2026 09:32
@matt2e
matt2e force-pushed the mobile-web branch 3 times, most recently from 8203cc3 to adf4f03 Compare November 9, 2026 13:16
matt2e and others added 5 commits November 11, 2026 17:05
Restore the Axum web server startup that was previously stubbed out, so
Staged can be used from a phone or browser when STAGED_WEB_SERVER=1 is set.

- Start the TLS listener on :5175, serving /api/invoke, the /api/events
  WebSocket, note media under /api/media, and the built frontend from dist/
- Drop the bearer-token/session-cookie auth layer and its dependencies
  (axum-extra, rand, subtle, time); the server is unauthenticated and
  only starts when explicitly opted in
- Add `just dev-web` (Vite over HTTPS with an /api proxy to the server)
  and `just run-web` (release build serving the production bundle, with a
  readiness probe since release builds log nothing)
- Configure Vite HTTPS, allowed host, and /api proxy from
  STAGED_WEB_CERT_PATH / STAGED_WEB_KEY_PATH / STAGED_WEB_HOST

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ilure tests

vitest resolves a run of consecutive queued doMocks in parallel, so the
setup-failure tests' throwing re-mock of prPollingService raced the
beforeEach mock of the same module and lost about half the time, letting
the socket connect and the onRegistrationFailed wait time out. Queue a
doUnmock in between so the override always lands last.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
… unauthenticated

Review follow-ups on the mobile-ui branch.

- web_server.rs: remove `CorsLayer::permissive()` and the tower-http `cors`
  feature. With the auth layer gone, the permissive layer let any page in a
  browser that could reach the host POST to /api/invoke/* cross-origin and
  run repo actions. Nothing needs CORS: `just run-web` serves dist/ from the
  server's own origin and `just dev-web` proxies /api through Vite (same
  origin for the browser; Vite's http-proxy forwards server-side with
  `ws: true` for the /api/events upgrade, and axum's WebSocketUpgrade does
  not check Origin), so same-origin fetches and the WebSocket keep working
  with no CORS headers at all.
- lib.rs gate, web_server.rs module doc, justfile recipe comments, README:
  say plainly that once STAGED_WEB_SERVER=1 is set the server binds
  0.0.0.0:5175 with TLS and no authentication, so anyone who can reach the
  port can run every command the desktop window can, including repo actions.
- web_server.rs `start()`: add a `STAGED_WEB_DIST` override, checked before
  the exe-relative candidates, and have `just run-web` export it as an
  absolute path instead of `cd src-tauri` so the release binary finds dist/
  regardless of launch cwd. The 404 probe message now points at the variable.
- transport.test.ts: the `mockPrPollClientId` comment no longer claims a
  vitest race. vitest 5.0.2 applies a batch of same-path `doMock`s
  sequentially in queue order, so the override already wins there; the
  `doUnmock` split is kept as an ordering guarantee that does not depend on
  vitest internals.

Verified with `pnpm -s check` (0 errors), the transport test file (21/21),
`cargo check` in src-tauri, `cargo fmt --check`, and Prettier on the changed
frontend files.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…rop unused dev:web script

Review follow-ups on the mobile-ui branch.

Dropping the CORS layer closed the invoke path (Json<Value> needs a
Content-Type a cross-origin page cannot send without a preflight), but CORS
never applied to WebSocket handshakes, so a page on any origin in a browser
that could reach the host could still open wss://<host>:5175/api/events and
read the full event stream: session content and the change feed.

- web_server.rs `ws_events`: take the request headers and refuse the upgrade
  with 403 when an `Origin` is present and its authority does not match the
  request's `Host` exactly, host and port both (`host:5175` vs `host:5176`,
  or an implied vs explicit port, are different origins). The scheme is
  ignored since the server only speaks TLS. A missing `Origin` is allowed:
  that is a non-browser client, which the unauthenticated server does not
  try to keep out, and a browser page cannot omit the header.
- Under `just dev-web` the page lives on the Vite origin and reaches the
  server through Vite's /api proxy, which rewrites Host to localhost:5175
  (`changeOrigin: true`) but forwards the browser's Origin, so Origin and
  Host never match there. `STAGED_WEB_HOST:VITE_PORT`, both exported by the
  recipe into the Tauri process, is accepted as a second allowed authority,
  also exact on host and port.
- The comparison is a pure `ws_origin_allowed(origin, host, dev_origin)`
  with `origin_authority` splitting the authority off the header value
  (`Origin: null`, empty and schemeless values never match). Unit tests
  cover same origin allowed (including case-insensitive host and default
  ports), different host rejected (including a host that merely contains
  ours, `null`, malformed, and no Host at all), different port rejected,
  absent Origin allowed, and the Vite dev authority allowed only when
  configured and only for the exact host and port.
- Module doc: the "cannot drive it" paragraph now also covers the event
  stream and points at the Origin check.
- package.json: remove the unreferenced `dev:web` script. `just dev-web`
  runs vite with its own flags through the Tauri beforeDevCommand and is
  the single definition; README and justfile never mentioned the script.

Verified with `cargo test --lib web_server::tests` (21/21, 6 new),
`cargo check`, `cargo clippy` and `cargo fmt --check` in src-tauri, and
`pnpm -s check` (0 errors, 0 warnings).

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
… back to the URI authority

Review follow-up on the mobile-ui branch.

The Origin check added in a31bbb3 compared `Origin` against the `Host`
header and failed closed when `Host` was missing. That is correct for
HTTP/1.1, but this server advertises `h2` over ALPN and axum's serve enables
the RFC 8441 extended CONNECT protocol, so Chrome and Firefox open the
WebSocket on the page's existing HTTP/2 connection. An h2 request has no
`Host` header: the authority travels in `:authority`, which the h2 crate maps
into the request URI and hyper does not copy into a `Host`. Every same-origin
desktop browser on h2 was therefore refused with 403 and sat in the client's
2 s reconnect loop with no live events. `just dev-web` was unaffected because
Vite's proxy speaks HTTP/1.1 to :5175, and the iOS-simulator check did not
surface it.

- web_server.rs `ws_events`: take the request `Uri` and derive the expected
  authority through a new `request_authority(headers, uri)`: the `Host`
  header first, else `uri.authority()`. `ws_origin_allowed` is unchanged and
  still fails closed when both are absent (origin-form URI, no Host). The
  refusal log now says "authority" rather than "Host".
- Tests: the h2 shape (Origin present, Host absent, URI authority present and
  matching, with and without an explicit port) is allowed; the same shape with
  a mismatched host or port in the Origin is rejected; an origin-form URI with
  no Host still yields no authority and is rejected; and `Host` wins over an
  absolute-form request URI when both are present.
- `ws_origin_allowed` doc: note that comparing `Origin` to the request's own
  authority instead of a fixed allow-list is only sound because the server is
  TLS-only. A DNS-rebinding host would match on both sides but fails the
  certificate check first, so a plain-HTTP variant must not reuse this check
  as-is.

Verified with `cargo test --lib web_server::tests` (24/24, 3 new),
`cargo check`, `cargo clippy` and `cargo fmt --check` in src-tauri.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
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