import type { AuthBootstrapInput, AuthBootstrapResult, AuthClientMetadata, AuthCreatePairingCredentialInput, AuthPairingCredentialResult, AuthRevokeClientSessionInput, AuthRevokePairingLinkInput, AuthSessionId, AuthSessionState, } from "@t3tools/contracts"; import { getPairingTokenFromUrl, stripPairingTokenFromUrl as stripPairingTokenUrl, } from "../../pairingUrl"; import { resolvePrimaryEnvironmentHttpUrl } from "./target"; import { Data, Predicate } from "effect"; export class BootstrapHttpError extends Data.TaggedError("BootstrapHttpError")<{ readonly message: string; readonly status: number; }> {} const isBootstrapHttpError = (u: unknown): u is BootstrapHttpError => Predicate.isTagged(u, "BootstrapHttpError"); export interface ServerPairingLinkRecord { readonly id: string; readonly credential: string; readonly role: "owner" | "client"; readonly subject: string; readonly label?: string; readonly createdAt: string; readonly expiresAt: string; } export interface ServerClientSessionRecord { readonly sessionId: AuthSessionId; readonly subject: string; readonly role: "owner" | "client"; readonly method: "browser-session-cookie" | "bearer-session-token"; readonly client: AuthClientMetadata; readonly issuedAt: string; readonly expiresAt: string; readonly lastConnectedAt: string | null; readonly connected: boolean; readonly current: boolean; } type ServerAuthGateState = | { status: "authenticated" } | { status: "requires-auth"; auth: AuthSessionState["auth"]; errorMessage?: string; }; let bootstrapPromise: Promise | null = null; let resolvedAuthenticatedGateState: ServerAuthGateState | null = null; const AUTH_SESSION_ESTABLISH_TIMEOUT_MS = 2_000; const AUTH_SESSION_ESTABLISH_STEP_MS = 100; export function peekPairingTokenFromUrl(): string | null { return getPairingTokenFromUrl(new URL(window.location.href)); } export function stripPairingTokenFromUrl() { const url = new URL(window.location.href); const next = stripPairingTokenUrl(url); if (next.toString() === url.toString()) { return; } window.history.replaceState({}, document.title, next.toString()); } export function takePairingTokenFromUrl(): string | null { const token = peekPairingTokenFromUrl(); if (!token) { return null; } stripPairingTokenFromUrl(); return token; } function getDesktopBootstrapCredential(): string | null { const bootstrap = window.desktopBridge?.getLocalEnvironmentBootstrap(); return typeof bootstrap?.bootstrapToken === "string" && bootstrap.bootstrapToken.length > 0 ? bootstrap.bootstrapToken : null; } export async function fetchSessionState(): Promise { return retryTransientBootstrap(async () => { const response = await fetch(resolvePrimaryEnvironmentHttpUrl("/api/auth/session"), { credentials: "include", }); if (!response.ok) { throw new BootstrapHttpError({ message: `Failed to load server auth session state (${response.status}).`, status: response.status, }); } return (await response.json()) as AuthSessionState; }); } async function readErrorMessage(response: Response, fallbackMessage: string): Promise { const text = await response.text(); return text || fallbackMessage; } const INVALID_BOOTSTRAP_CREDENTIAL_MESSAGES = new Set([ "Invalid bootstrap credential.", "Unknown bootstrap credential.", ]); function parseBootstrapErrorMessage(message: string): string { const trimmed = message.trim(); if (trimmed.length === 0) { return ""; } try { const parsed = JSON.parse(trimmed) as { error?: unknown; }; if (typeof parsed.error === "string" && parsed.error.trim().length > 0) { return parsed.error.trim(); } } catch { // Not JSON; fall back to plain text. } return trimmed; } function toFriendlyBootstrapErrorMessage(status: number, message: string): string { const parsedMessage = parseBootstrapErrorMessage(message); if (status === 401 && INVALID_BOOTSTRAP_CREDENTIAL_MESSAGES.has(parsedMessage)) { return "Invalid pairing token. Check the token and try again."; } return parsedMessage; } async function exchangeBootstrapCredential(credential: string): Promise { return retryTransientBootstrap(async () => { const payload: AuthBootstrapInput = { credential }; const response = await fetch(resolvePrimaryEnvironmentHttpUrl("/api/auth/bootstrap"), { body: JSON.stringify(payload), credentials: "include", headers: { "content-type": "application/json", }, method: "POST", }); if (!response.ok) { const message = toFriendlyBootstrapErrorMessage(response.status, await response.text()); throw new BootstrapHttpError({ message: message || `Failed to bootstrap auth session (${response.status}).`, status: response.status, }); } return (await response.json()) as AuthBootstrapResult; }); } async function waitForAuthenticatedSessionAfterBootstrap(): Promise { const startedAt = Date.now(); while (true) { const session = await fetchSessionState(); if (session.authenticated) { return session; } if (Date.now() - startedAt >= AUTH_SESSION_ESTABLISH_TIMEOUT_MS) { throw new Error("Timed out waiting for authenticated session after bootstrap."); } await waitForBootstrapRetry(AUTH_SESSION_ESTABLISH_STEP_MS); } } const TRANSIENT_BOOTSTRAP_STATUS_CODES = new Set([502, 503, 504]); const BOOTSTRAP_RETRY_TIMEOUT_MS = 15_000; const BOOTSTRAP_RETRY_STEP_MS = 500; export async function retryTransientBootstrap(operation: () => Promise): Promise { const startedAt = Date.now(); while (true) { try { return await operation(); } catch (error) { if (!isTransientBootstrapError(error)) { throw error; } if (Date.now() - startedAt >= BOOTSTRAP_RETRY_TIMEOUT_MS) { throw error; } await waitForBootstrapRetry(BOOTSTRAP_RETRY_STEP_MS); } } } function waitForBootstrapRetry(delayMs: number): Promise { return new Promise((resolve) => { setTimeout(resolve, delayMs); }); } function isTransientBootstrapError(error: unknown): boolean { if (isBootstrapHttpError(error)) { return TRANSIENT_BOOTSTRAP_STATUS_CODES.has(error.status); } if (error instanceof TypeError) { return true; } return error instanceof DOMException && error.name === "AbortError"; } async function bootstrapServerAuth(): Promise { const bootstrapCredential = getDesktopBootstrapCredential(); const currentSession = await fetchSessionState(); if (currentSession.authenticated) { return { status: "authenticated" }; } if (!bootstrapCredential) { return { status: "requires-auth", auth: currentSession.auth, }; } try { await exchangeBootstrapCredential(bootstrapCredential); await waitForAuthenticatedSessionAfterBootstrap(); return { status: "authenticated" }; } catch (error) { return { status: "requires-auth", auth: currentSession.auth, errorMessage: error instanceof Error ? error.message : "Authentication failed.", }; } } export async function submitServerAuthCredential(credential: string): Promise { const trimmedCredential = credential.trim(); if (!trimmedCredential) { throw new Error("Enter a pairing token to continue."); } resolvedAuthenticatedGateState = null; await exchangeBootstrapCredential(trimmedCredential); bootstrapPromise = null; stripPairingTokenFromUrl(); } export async function createServerPairingCredential( label?: string, ): Promise { const trimmedLabel = label?.trim(); const payload: AuthCreatePairingCredentialInput = trimmedLabel ? { label: trimmedLabel } : {}; const response = await fetch(resolvePrimaryEnvironmentHttpUrl("/api/auth/pairing-token"), { body: JSON.stringify(payload), credentials: "include", headers: { "content-type": "application/json", }, method: "POST", }); if (!response.ok) { throw new Error( await readErrorMessage(response, `Failed to create pairing credential (${response.status}).`), ); } return (await response.json()) as AuthPairingCredentialResult; } export async function listServerPairingLinks(): Promise> { const response = await fetch(resolvePrimaryEnvironmentHttpUrl("/api/auth/pairing-links"), { credentials: "include", }); if (!response.ok) { throw new Error( await readErrorMessage(response, `Failed to load pairing links (${response.status}).`), ); } return (await response.json()) as ReadonlyArray; } export async function revokeServerPairingLink(id: string): Promise { const payload: AuthRevokePairingLinkInput = { id }; const response = await fetch(resolvePrimaryEnvironmentHttpUrl("/api/auth/pairing-links/revoke"), { body: JSON.stringify(payload), credentials: "include", headers: { "content-type": "application/json", }, method: "POST", }); if (!response.ok) { throw new Error( await readErrorMessage(response, `Failed to revoke pairing link (${response.status}).`), ); } } export async function listServerClientSessions(): Promise< ReadonlyArray > { const response = await fetch(resolvePrimaryEnvironmentHttpUrl("/api/auth/clients"), { credentials: "include", }); if (!response.ok) { throw new Error( await readErrorMessage(response, `Failed to load paired clients (${response.status}).`), ); } return (await response.json()) as ReadonlyArray; } export async function revokeServerClientSession(sessionId: AuthSessionId): Promise { const payload: AuthRevokeClientSessionInput = { sessionId }; const response = await fetch(resolvePrimaryEnvironmentHttpUrl("/api/auth/clients/revoke"), { body: JSON.stringify(payload), credentials: "include", headers: { "content-type": "application/json", }, method: "POST", }); if (!response.ok) { throw new Error( await readErrorMessage(response, `Failed to revoke client session (${response.status}).`), ); } } export async function revokeOtherServerClientSessions(): Promise { const response = await fetch( resolvePrimaryEnvironmentHttpUrl("/api/auth/clients/revoke-others"), { credentials: "include", method: "POST", }, ); if (!response.ok) { throw new Error( await readErrorMessage( response, `Failed to revoke other client sessions (${response.status}).`, ), ); } const result = (await response.json()) as { revokedCount?: number }; return result.revokedCount ?? 0; } export async function resolveInitialServerAuthGateState(): Promise { if (resolvedAuthenticatedGateState?.status === "authenticated") { return resolvedAuthenticatedGateState; } if (bootstrapPromise) { return bootstrapPromise; } const nextPromise = bootstrapServerAuth(); bootstrapPromise = nextPromise; return nextPromise .then((result) => { if (result.status === "authenticated") { resolvedAuthenticatedGateState = result; } return result; }) .finally(() => { if (bootstrapPromise === nextPromise) { bootstrapPromise = null; } }); } export function __resetServerAuthBootstrapForTests() { bootstrapPromise = null; resolvedAuthenticatedGateState = null; }