/** * Credentials for the Device panel's media requests. * * The panel reaches simulator streams through `/api/device-hub/*` on the * environment origin. ``, `EventSource`, and `WebSocket` cannot set * bearer or DPoP headers, so bearer and DPoP connections mint a * short-lived WebSocket ticket and pass it as `wsTicket`, the same way the * app's own `/ws` upgrade authenticates. Cookie sessions send the cookie. * * A ticket lives five minutes server-side and is bound to the session, not * to one request, so one ticket covers everything a panel opens at once. * Callers fetch a fresh one each time they (re)connect a stream. */ import * as Effect from "effect/Effect"; import type { HttpClient } from "effect/unstable/http"; import { RemoteEnvironmentAuthorization } from "../authorization/service.ts"; import type { PreparedConnection } from "../connection/model.ts"; import { environmentEndpointUrl } from "../environment/endpoint.ts"; import { ManagedRelayDpopSigner } from "../relay/managedRelay.ts"; import type { RemoteEnvironmentRequestError } from "../rpc/http.ts"; import { executeAuthenticatedEnvironmentHttpRequest } from "./environmentHttpAuth.ts"; const TICKET_TIMEOUT_MS = 8_000; export interface DeviceHubAccess { /** Absolute origin-relative base, e.g. `https://env.example/api/device-hub`. */ readonly httpBase: string; /** Same base with the `ws(s)` scheme. */ readonly wsBase: string; /** Query parameters to append to every hub request; empty for cookie sessions. */ readonly query: Readonly>; /** Whether requests must include cookies (same-origin session). */ readonly credentials: boolean; } export const resolveDeviceHubAccess = Effect.fn("clientRuntime.state.resolveDeviceHubAccess")( function* (input: { readonly prepared: PreparedConnection; readonly hubBasePath: string; }): Effect.fn.Return { const httpBase = environmentEndpointUrl(input.prepared.httpBaseUrl, input.hubBasePath); const wsBase = httpBase.replace(/^http/, "ws"); if (input.prepared.httpAuthorization === null) { return { httpBase, wsBase, query: {}, credentials: true }; } const signer = yield* Effect.serviceOption(ManagedRelayDpopSigner); const remoteAuthorization = yield* Effect.serviceOption(RemoteEnvironmentAuthorization); const ticket = yield* executeAuthenticatedEnvironmentHttpRequest({ prepared: input.prepared, signer, remoteAuthorization, method: "POST", url: (httpBaseUrl) => environmentEndpointUrl(httpBaseUrl, "/api/auth/websocket-ticket"), timeoutMs: TICKET_TIMEOUT_MS, request: ({ client, headers }) => client.auth.webSocketTicket({ headers }), }); return { httpBase, wsBase, query: { wsTicket: ticket.ticket }, credentials: false, }; }, ); export const withDeviceHubQuery = (url: string, access: DeviceHubAccess): string => { const entries = Object.entries(access.query); if (entries.length === 0) return url; const separator = url.includes("?") ? "&" : "?"; return `${url}${separator}${new URLSearchParams(entries).toString()}`; };