458 lines · 13.5 KB · TypeScript
RawDownload
import type {  AuthBrowserSessionResult,  AuthClientMetadata,  AuthEnvironmentScope,  AuthPairingCredentialResult,  ServerAuthSessionMethod,  AuthSessionId,  AuthSessionState,} from "@t3tools/contracts";import { EnvironmentHttpCommonError, PRIMARY_LOCAL_ENVIRONMENT_ID } from "@t3tools/contracts";import type { EnvironmentHttpCommonError as EnvironmentHttpCommonErrorType } from "@t3tools/contracts";import * as Effect from "effect/Effect";import * as Schema from "effect/Schema";import { HttpClientError } from "effect/unstable/http"; import {  getPairingTokenFromUrl,  stripPairingTokenFromUrl as stripPairingTokenUrl,} from "../../pairingUrl"; import { PrimaryEnvironmentHttpClient } from "./httpClient";import { runPrimaryHttp } from "../../lib/runtime"; const PrimaryEnvironmentRequestOperation = Schema.Literals([  "fetch-session-state",  "exchange-bootstrap-credential",  "fetch-environment-descriptor",  "create-pairing-credential",  "list-pairing-links",  "revoke-pairing-link",  "list-client-sessions",  "revoke-client-session",  "revoke-other-client-sessions",]);type PrimaryEnvironmentRequestOperation = typeof PrimaryEnvironmentRequestOperation.Type; export class PrimaryEnvironmentRequestError extends Schema.TaggedError<PrimaryEnvironmentRequestError>()(  "PrimaryEnvironmentRequestError",  {    operation: PrimaryEnvironmentRequestOperation,    status: Schema.Number,    pairingLinkId: Schema.optional(Schema.String),    sessionId: Schema.optional(Schema.String),    cause: Schema.Defect(),  },) {  static fromCause(input: {    readonly operation: PrimaryEnvironmentRequestOperation;    readonly cause: unknown;    readonly pairingLinkId?: string;    readonly sessionId?: string;  }): PrimaryEnvironmentRequestError {    const status = readHttpApiStatus(input.cause) ?? 500;    return new PrimaryEnvironmentRequestError({      operation: input.operation,      status,      ...(input.pairingLinkId !== undefined ? { pairingLinkId: input.pairingLinkId } : {}),      ...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}),      cause: input.cause,    });  }   override get message(): string {    return `Primary environment request failed during ${this.operation} (HTTP ${this.status}).`;  }} const isPrimaryEnvironmentRequestError = Schema.is(PrimaryEnvironmentRequestError); export class PrimaryEnvironmentPairingCredentialRejectedError extends Schema.TaggedError<PrimaryEnvironmentPairingCredentialRejectedError>()(  "PrimaryEnvironmentPairingCredentialRejectedError",  {    providedLength: Schema.Number,    cause: Schema.Defect(),  },) {  override get message(): string {    return "Invalid pairing token. Check the token and try again.";  }} export const isPrimaryEnvironmentPairingCredentialRejectedError = Schema.is(  PrimaryEnvironmentPairingCredentialRejectedError,); export class PrimaryEnvironmentAuthSessionTimeoutError extends Schema.TaggedError<PrimaryEnvironmentAuthSessionTimeoutError>()(  "PrimaryEnvironmentAuthSessionTimeoutError",  {    timeoutMs: Schema.Number,    elapsedMs: Schema.Number,  },) {  override get message(): string {    return "Timed out waiting for authenticated session after bootstrap.";  }} export class PrimaryEnvironmentPairingCredentialRequiredError extends Schema.TaggedError<PrimaryEnvironmentPairingCredentialRequiredError>()(  "PrimaryEnvironmentPairingCredentialRequiredError",  {    providedLength: Schema.Number,  },) {  override get message(): string {    return "Enter a pairing token to continue.";  }} const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError); export interface ServerPairingLinkRecord {  readonly id: string;  readonly scopes: ReadonlyArray<AuthEnvironmentScope>;  readonly subject: string;  readonly label?: string;  readonly createdAt: string;  readonly expiresAt: string;} export interface ServerClientSessionRecord {  readonly sessionId: AuthSessionId;  readonly subject: string;  readonly scopes: ReadonlyArray<AuthEnvironmentScope>;  readonly method: ServerAuthSessionMethod;  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<ServerAuthGateState> | 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 {  // Both backends share the same bootstrap token (DesktopBackendConfiguration  // mints one tokenRef and feeds it to both resolvers), so picking the  // primary entry is fine even when the WSL backend is also registered.  const bootstraps = window.desktopBridge?.getLocalEnvironmentBootstraps() ?? [];  const primary = bootstraps.find((entry) => entry.id === PRIMARY_LOCAL_ENVIRONMENT_ID);  return typeof primary?.bootstrapToken === "string" && primary.bootstrapToken.length > 0    ? primary.bootstrapToken    : null;} export async function fetchSessionState(): Promise<AuthSessionState> {  return retryTransientBootstrap(async () => {    try {      return await runPrimaryHttp(        PrimaryEnvironmentHttpClient.pipe(          Effect.flatMap((client) => client.auth.session({ headers: {} })),        ),      );    } catch (error) {      throw PrimaryEnvironmentRequestError.fromCause({        operation: "fetch-session-state",        cause: error,      });    }  });} function readHttpApiStatus(error: unknown): number | null {  if (isEnvironmentHttpCommonError(error)) {    return readEnvironmentHttpErrorStatus(error);  }  return HttpClientError.isHttpClientError(error) && error.response !== undefined    ? error.response.status    : null;} function readEnvironmentHttpErrorStatus(error: EnvironmentHttpCommonErrorType): number {  switch (error._tag) {    case "EnvironmentRequestInvalidError":      return 400;    case "EnvironmentAuthInvalidError":      return 401;    case "EnvironmentScopeRequiredError":    case "EnvironmentOperationForbiddenError":      return 403;    case "EnvironmentResourceNotFoundError":      return 404;    case "EnvironmentInternalError":      return 500;  }} async function exchangeBootstrapCredential(credential: string): Promise<AuthBrowserSessionResult> {  return retryTransientBootstrap(async () => {    try {      return await runPrimaryHttp(        PrimaryEnvironmentHttpClient.pipe(          Effect.flatMap((client) => client.auth.browserSession({ payload: { credential } })),        ),      );    } catch (error) {      if (        isEnvironmentHttpCommonError(error) &&        error._tag === "EnvironmentAuthInvalidError" &&        error.reason === "invalid_credential"      ) {        throw new PrimaryEnvironmentPairingCredentialRejectedError({          providedLength: credential.length,          cause: error,        });      }      throw PrimaryEnvironmentRequestError.fromCause({        operation: "exchange-bootstrap-credential",        cause: error,      });    }  });} async function waitForAuthenticatedSessionAfterBootstrap(): Promise<AuthSessionState> {  const startedAt = Date.now();   while (true) {    const session = await fetchSessionState();    if (session.authenticated) {      return session;    }     const elapsedMs = Date.now() - startedAt;    if (elapsedMs >= AUTH_SESSION_ESTABLISH_TIMEOUT_MS) {      throw new PrimaryEnvironmentAuthSessionTimeoutError({        timeoutMs: AUTH_SESSION_ESTABLISH_TIMEOUT_MS,        elapsedMs,      });    }     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<T>(operation: () => Promise<T>): Promise<T> {  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<void> {  return new Promise((resolve) => {    setTimeout(resolve, delayMs);  });} function isTransientBootstrapError(error: unknown): boolean {  if (isPrimaryEnvironmentRequestError(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<ServerAuthGateState> {  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<void> {  const trimmedCredential = credential.trim();  if (!trimmedCredential) {    throw new PrimaryEnvironmentPairingCredentialRequiredError({      providedLength: credential.length,    });  }   resolvedAuthenticatedGateState = null;  await exchangeBootstrapCredential(trimmedCredential);  await waitForAuthenticatedSessionAfterBootstrap();  resolvedAuthenticatedGateState = { status: "authenticated" };  bootstrapPromise = null;  stripPairingTokenFromUrl();} export async function createServerPairingCredential(input?: {  readonly label?: string;  readonly scopes?: ReadonlyArray<AuthEnvironmentScope>;}): Promise<AuthPairingCredentialResult> {  const trimmedLabel = input?.label?.trim();  try {    return await runPrimaryHttp(      PrimaryEnvironmentHttpClient.pipe(        Effect.flatMap((client) =>          client.auth.pairingCredential({            headers: {},            payload: {              ...(trimmedLabel ? { label: trimmedLabel } : {}),              ...(input?.scopes ? { scopes: input.scopes } : {}),            },          }),        ),      ),    );  } catch (error) {    throw PrimaryEnvironmentRequestError.fromCause({      operation: "create-pairing-credential",      cause: error,    });  }} export async function revokeServerPairingLink(id: string): Promise<void> {  try {    await runPrimaryHttp(      PrimaryEnvironmentHttpClient.pipe(        Effect.flatMap((client) => client.auth.revokePairingLink({ headers: {}, payload: { id } })),      ),    );  } catch (error) {    throw PrimaryEnvironmentRequestError.fromCause({      operation: "revoke-pairing-link",      pairingLinkId: id,      cause: error,    });  }} export async function revokeServerClientSession(sessionId: AuthSessionId): Promise<void> {  try {    await runPrimaryHttp(      PrimaryEnvironmentHttpClient.pipe(        Effect.flatMap((client) =>          client.auth.revokeClient({ headers: {}, payload: { sessionId } }),        ),      ),    );  } catch (error) {    throw PrimaryEnvironmentRequestError.fromCause({      operation: "revoke-client-session",      sessionId,      cause: error,    });  }} export async function revokeOtherServerClientSessions(): Promise<number> {  try {    const result = await runPrimaryHttp(      PrimaryEnvironmentHttpClient.pipe(        Effect.flatMap((client) => client.auth.revokeOtherClients({ headers: {} })),      ),    );    return result.revokedCount;  } catch (error) {    throw PrimaryEnvironmentRequestError.fromCause({      operation: "revoke-other-client-sessions",      cause: error,    });  }} export async function resolveInitialServerAuthGateState(): Promise<ServerAuthGateState> {  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;}