import { createPiWebServer, getLanIp, validateToken } from "@ifi/pi-web-server"; import type { AgentSessionLike, PiWebServer } from "@ifi/pi-web-server"; import type { DiscoveryRecord, DiscoveryService } from "./discovery.js"; import { startTailscaleServe } from "./tailscale.js"; import type { TailscaleServeSession } from "./tailscale.js"; export const DEFAULT_HOSTED_UI_URL = "https://pi-remote.dev"; export const REMOTE_MODE_ENV = "PI_REMOTE_TAILSCALE_MODE"; const DEFAULT_SERVER_HOST = "0.0.0.0"; const INVALID_PORT_ERROR = "Unable to determine the remote server port."; export interface RemoteSessionServerOptions { discovery?: DiscoveryService; enableTailscale?: boolean; getLanIpFn?: () => string | undefined; host?: string; hostedUiUrl?: string; pid?: number; port?: number; resolveSession?: () => AgentSessionLike | undefined; session?: AgentSessionLike; startTailscale?: (options: { instanceId: string; port: number }) => Promise; token?: string; } export interface RemoteSessionHandle { connectUrl: string; discoveryRecordId?: string; instanceId: string; lanUrl?: string; localUrl: string; server: PiWebServer; stop: () => Promise; token: string; tunnelUrl?: string; } export function isRemoteSessionEnv(env: NodeJS.ProcessEnv = process.env): boolean { return env[REMOTE_MODE_ENV]?.trim() === "remote"; } export function buildRemoteModeEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv { return { ...env, [REMOTE_MODE_ENV]: "remote", }; } export function parsePortFromServerUrl(serverUrl: string): number { const parsed = new URL(serverUrl); const port = Number(parsed.port); if (!Number.isFinite(port) || port <= 0) { throw new Error(INVALID_PORT_ERROR); } return port; } export function appendAuthToken(url: string, token: string): string { const parsed = new URL(url); parsed.searchParams.set("t", token); return parsed.toString(); } export function buildHostedConnectUrl(tunnelUrl: string, token: string, hostedUiUrl = DEFAULT_HOSTED_UI_URL): string { const parsed = new URL(hostedUiUrl); parsed.searchParams.set("host", tunnelUrl); parsed.searchParams.set("t", token); return parsed.toString(); } export function buildBestConnectUrl(options: { hostedUiUrl?: string; lanUrl?: string; localUrl: string; token: string; tunnelUrl?: string; }): string { if (options.tunnelUrl) { return buildHostedConnectUrl(options.tunnelUrl, options.token, options.hostedUiUrl); } if (options.lanUrl) { return options.lanUrl; } return options.localUrl; } export function createAuthHeaders(token: string): { Authorization: string } { return { Authorization: `Bearer ${token}` }; } export function hasValidToken(provided: string | undefined | null, expected: string): boolean { if (!provided) { return false; } return validateToken(provided, expected); } export function renderErrorPage(status: 403 | 404, title?: string, detail?: string): string { const resolvedTitle = title ?? (status === 403 ? "Forbidden" : "Not found"); const resolvedDetail = detail ?? (status === 403 ? "A valid token is required to view this remote session." : "The requested remote resource does not exist."); return ` ${resolvedTitle}
HTTP ${status}

${resolvedTitle}

${resolvedDetail}

`; } function buildDiscoveryRecord(options: { connectUrl: string; instanceId: string; lanUrl?: string; localUrl: string; pid: number; tunnelUrl?: string; }): Omit { return { connectUrl: options.connectUrl, cwd: process.cwd(), instanceId: options.instanceId, lanUrl: options.lanUrl, localUrl: options.localUrl, pid: options.pid, remoteMode: isRemoteSessionEnv(), tunnelUrl: options.tunnelUrl, }; } export async function startRemoteSessionServer(options: RemoteSessionServerOptions = {}): Promise { const server = createPiWebServer({ host: options.host ?? DEFAULT_SERVER_HOST, port: options.port, token: options.token, }); const session = options.session ?? options.resolveSession?.(); if (session) { server.attachSession(session); } const started = await server.start(); const port = parsePortFromServerUrl(started.url); const localUrl = appendAuthToken(started.url, started.token); const getLanIpFn = options.getLanIpFn ?? getLanIp; const lanIp = getLanIpFn(); const lanUrl = lanIp ? appendAuthToken(`http://${lanIp}:${port}`, started.token) : undefined; const startTailscale = options.startTailscale ?? startTailscaleServe; let tunnelUrl: string | undefined; let tailscaleSession: TailscaleServeSession | undefined; if (options.enableTailscale !== false) { try { tailscaleSession = await startTailscale({ instanceId: started.instanceId, port }); tunnelUrl = tailscaleSession.publicUrl; server.setTunnel({ provider: "tailscale", publicUrl: tailscaleSession.publicUrl, stop: () => { void tailscaleSession?.stop(); }, }); } catch { // Continue with LAN or localhost URLs. } } const connectUrl = buildBestConnectUrl({ hostedUiUrl: options.hostedUiUrl ?? DEFAULT_HOSTED_UI_URL, lanUrl, localUrl, token: started.token, tunnelUrl, }); let discoveryRecordId: string | undefined; if (options.discovery) { const record = await options.discovery.register( buildDiscoveryRecord({ connectUrl, instanceId: started.instanceId, lanUrl, localUrl, pid: options.pid ?? process.pid, tunnelUrl, }), ); discoveryRecordId = record.id; } return { connectUrl, discoveryRecordId, instanceId: started.instanceId, lanUrl, localUrl, server, stop: async () => { if (discoveryRecordId) { await options.discovery?.unregister(discoveryRecordId); } await server.stop(); }, token: started.token, tunnelUrl, }; }