feat(desktop): connect to OAuth-gated remote gateways#37888
Conversation
🔎 Lint report:
|
|
Field verification + CI triage from the desktop/cloud integration path: With #37870 landed, this PR looks like the desktop-side half needed for OAuth-gated remote Hermes gateways. I validated this flow from a desktop-to-remote-gateway setup, with private endpoint/token details intentionally omitted. The critical path I would keep as the merge/release gate is:
The current red checks look unrelated to the files in this PR. The PR patch is limited to desktop connection/auth/settings files, while the failing jobs I see are:
So I do not think either red check is evidence against the desktop OAuth/websocket-ticket change. I can also contribute a follow-up release/runbook doc PR that captures the rollout checklist once this lands. |
Kenmege
left a comment
There was a problem hiding this comment.
Non-blocking implementation question from the desktop OAuth path review:
Both reconnect call sites now do this shape:
const wsUrl = (await desktop.getGatewayWsUrl?.().catch(() => null)) || conn.wsUrl
await gateway.connect(wsUrl)The fallback is fine for local/token mode, and I can see why optional chaining helps older preload/runtime shapes. For OAuth mode, though, a failed getGatewayWsUrl() means the app could fall back to conn.wsUrl, which may contain a single-use or expired ticket. That can mask the better failure path from mintGatewayWsTicket() / needsOauthLogin and turn it into a generic websocket failure.
Would it be safer to let fresh-mint failures propagate when conn.authMode === "oauth", and only fall back to conn.wsUrl for token/local mode or when the preload method is genuinely absent? That keeps the reauth prompt path intact while preserving compatibility for non-OAuth connections.
58ede30 to
aaae6a9
Compare
The desktop remote-gateway settings now auto-detect whether a gateway
authenticates with OAuth or a static session token and present the
matching UI + connection mechanism.
Detection: an unauthenticated GET {base}/api/status reads auth_required
(true => OAuth, false => session token); /api/auth/providers supplies the
provider label. The settings UI debounce-probes the entered URL and shows
either a 'Sign in with <provider>' button or the session-token box.
OAuth connection mechanism:
- REST is authed by the HttpOnly session cookie held in a persistent
Electron session partition (persist:hermes-remote-oauth); main-process
REST routes through electron net bound to that partition so the cookie
attaches automatically.
- Login opens a BrowserWindow on {base}/login in that partition and
resolves once the hermes_session_at cookie lands.
- WebSocket upgrades use a single-use ?ticket= minted at
POST /api/auth/ws-ticket (the gateway rejects ?token= in gated mode);
getGatewayWsUrl() re-mints before every (re)connect since tickets are
single-use and short-lived.
- Missing cookie / 401 surfaces needsOauthLogin to prompt re-sign-in
(Nous Portal contract v1 issues no refresh token).
Local and token modes are unchanged.
Pure helpers (URL normalize, ws-url token/ticket builders, auth-mode
classify/resolve, cookie detector) are extracted to a standalone
connection-config.cjs (no electron import) and unit-tested with
node --test (26 tests), matching the backend-probes.cjs pattern.
The remote-gateway settings rendered the session-token box for every gateway during the idle/probing window before the first /api/status probe lands, because authMode defaults to 'token'. Gate both the OAuth sign-in button and the token box behind an authResolved flag so neither renders until the probe resolves the scheme (or a previously-saved remote config is being re-shown, so re-opening settings doesn't flicker). The gateway-side WS Origin fix that lets the packaged desktop (file:// origin) connect to an OAuth-gated remote gateway landed separately in #37870; this branch is now purely the desktop client + this UI fix.
…ilure The reconnect and boot paths resolved the WS URL with `(await getGatewayWsUrl().catch(() => null)) || conn.wsUrl`. For OAuth gateways the cached conn.wsUrl carries a single-use, ~30s-TTL ticket; the desktop connection is memoized for the process lifetime, so on reconnect that ticket is both expired and already consumed. A failed fresh mint therefore fell back to a guaranteed-dead ticket and surfaced as an opaque "connection closed", masking the gateway's actionable "session expired, sign in again" message. Extract resolveGatewayWsUrl() (with unit tests): in OAuth mode a mint failure throws a tagged GatewayReauthRequiredError instead of falling back; token/local modes keep the long-lived-token fallback. Thread that error through the reconnect path so requestGateway surfaces the reauth message rather than the generic transport error that triggered the retry. Co-authored-by: Kenmege <205099287+Kenmege@users.noreply.github.com>
943f0e4 to
733e722
Compare
Summary
Adds the desktop-client half of remote OAuth-gated gateway support: Hermes Desktop can now connect to a hosted/remote Hermes gateway that authenticates with OAuth (cookie + single-use WS ticket) instead of a static session token. The gateway/server half — the WS Origin guard fix that lets the packaged
file://renderer through — already landed in #37870.Background
A remote Hermes gateway can authenticate two ways:
token— legacy static dashboard session token (X-Hermes-Session-Tokenheader for REST,?token=for WS).oauth— hosted gateways gate behind an OAuth provider. REST is authed by an HttpOnly session cookie; WS upgrades require a single-use?ticket=minted at the cookie-authedPOST /api/auth/ws-ticket. The gateway advertises this via the public/api/statusfieldauth_required: true.Previously the desktop's remote-gateway settings only supported a static session token, so it could not drive an OAuth-gated hosted gateway at all.
What this does
Auto-detection. As the user types a remote URL, the settings page debounce-probes
GET {base}/api/status(auth_required→ oauth vs token) andGET {base}/api/auth/providers(provider label). It then renders either a "Sign in with <provider>" button or the existing session-token box.OAuth connection mechanism (main process):
persist:hermes-remote-oauth); main-process REST routes through Electronnetbound to that partition so the cookie attaches automatically.BrowserWindowon{base}/loginin that partition and resolves once thehermes_session_atcookie lands.?ticket=minted atPOST /api/auth/ws-ticket.getGatewayWsUrl()re-mints before every (re)connect, since tickets are single-use and short-lived (the ticket baked into the cachedconn.wsUrlis already stale on the second connect).needsOauthLoginto prompt re-sign-in (Portal contract v1 issues no refresh token).UI flicker fix. The session-token box previously rendered for every gateway during the idle/probing window (because
authModedefaults to'token'). Both the OAuth button and the token box are now gated behind anauthResolvedflag, so neither renders until the probe resolves the scheme (or a previously-saved remote config is being re-shown, so re-opening settings doesn't flicker).Local and token modes are unchanged.
Changes
apps/desktop/electron/main.cjs— OAuth session partition, cookie-authed REST via Electronnet, loginBrowserWindow, ws-ticket minting,freshGatewayWsUrl()re-mint-before-connect,needsOauthLoginsurfacing.apps/desktop/electron/connection-config.cjs(+.test.cjs) — pure, electron-free helpers (URL normalize, token/ticket WS-URL builders, auth-mode classify/resolve, cookie detector), unit-tested withnode --test(26 tests), matching thebackend-probes.cjspattern.apps/desktop/electron/preload.cjs—getGatewayWsUrl/probeConnectionConfig/oauthLogin*/oauthLogout*IPC bridges.apps/desktop/src/app/settings/gateway-settings.tsx— probe-driven OAuth/token UI +authResolvedflicker fix.apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts/use-gateway-request.ts,src/global.d.ts,src/lib/icons.ts,package.json— supporting plumbing/types.Validation
tsc -bpasses.node --test electron/connection-config.test.cjs— 26/26.POST /api/auth/ws-ticket→ 200 + ticket, andwss://…/api/ws?ticket=<real>(no cookie, exactly the renderer's situation) → connected + receivedgateway.ready. After fix(dashboard): trust non-web WS origins on OAuth-gated binds after ticket auth #37870 thefile://-origin upgrade is accepted server-side.Dependency
Requires #37870 (merged) on the gateway. The hosted gateway image must be rebuilt/redeployed with #37870 before the desktop can complete the WS upgrade to an OAuth-gated gateway.
Touches the gateway/connection layer (
main.cjs, gateway hooks), so flagging for @teknium1 review per the desktop/gateway lane.