import type { HermesConnection } from '@/global' /** * The desktop main process exposes `getGatewayWsUrl()` to re-mint a WebSocket * URL immediately before every `gateway.connect()`. For OAuth-gated remote * gateways the WS ticket is single-use with a ~30s TTL, so the ticket baked * into the cached `conn.wsUrl` is stale (and, after the first connect, already * consumed). For local/token gateways the URL carries a long-lived token and * never needs re-minting. * * Resolution rules: * * - OAuth: the fresh mint is the *only* viable URL. If it fails, do NOT fall * back to `conn.wsUrl` — that ticket is dead and the connect is guaranteed to * fail with an opaque "connection closed" error. Instead, let the mint error * propagate so the caller can surface the gateway's reauth message * ("session has expired… Sign in again"). * * - token / local, or when the preload method is genuinely absent (older * preload shapes): fall back to `conn.wsUrl`. The token URL is long-lived, so * the fallback is safe and preserves compatibility. * * The error thrown for OAuth mint failures is tagged with `needsOauthLogin` so * callers can distinguish "the user must re-authenticate" from a generic * transport failure. */ export interface ResolveGatewayWsUrlDeps { /** `window.hermesDesktop.getGatewayWsUrl`, if the preload exposes it. The * optional profile selects which backend to mint for — critical when swapping * to a pooled profile, since the default mint resolves the primary backend. */ getGatewayWsUrl?: (profile?: null | string) => Promise } export class GatewayReauthRequiredError extends Error { readonly needsOauthLogin = true constructor(message: string, options?: { cause?: unknown }) { super(message, options) this.name = 'GatewayReauthRequiredError' } } export function isGatewayReauthRequired(error: unknown): error is GatewayReauthRequiredError { return ( error instanceof GatewayReauthRequiredError || (typeof error === 'object' && error !== null && (error as { needsOauthLogin?: unknown }).needsOauthLogin === true) ) } export async function resolveGatewayWsUrl( desktop: ResolveGatewayWsUrlDeps, conn: Pick ): Promise { const mint = desktop.getGatewayWsUrl // Mint for THIS connection's profile, not the primary. Without it a pooled // profile swap re-mints the default backend's URL and connects to the wrong // backend. const profile = conn.profile ?? null if (conn.authMode === 'oauth') { if (!mint) { // OAuth gateway but no way to mint a fresh ticket: the cached ticket is // dead, so connecting with it cannot succeed. Surface a reauth error // rather than silently attempting a doomed connect. throw new GatewayReauthRequiredError( 'Your remote gateway session needs to be refreshed. Open Settings → Gateway and click "Sign in" again.' ) } try { return await mint(profile) } catch (error) { throw new GatewayReauthRequiredError( 'Your remote gateway session has expired. Open Settings → Gateway and click "Sign in" again.', { cause: error } ) } } // token / local: the URL carries a long-lived token. Re-mint when available // (cheap, keeps parity), but the cached URL is a safe fallback. if (mint) { const fresh = await mint(profile).catch(() => null) if (fresh) { return fresh } } return conn.wsUrl }