518 lines · 15.9 KB · TypeScript
RawDownload
import type {  AuthBrowserSessionResult,  AuthClientMetadata,  AuthEnvironmentScope,  AuthPairingCredentialResult,  ServerAuthSessionMethod,  AuthSessionId,  AuthSessionState,} from "@t3tools/contracts";import { EnvironmentHttpCommonError } from "@t3tools/contracts";import type { EnvironmentHttpCommonError as EnvironmentHttpCommonErrorType } from "@t3tools/contracts";import * as DateTime from "effect/DateTime";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.TaggedErrorClass<PrimaryEnvironmentRequestError>()(  "PrimaryEnvironmentRequestError",  {    operation: PrimaryEnvironmentRequestOperation,    status: Schema.Number,    detail: Schema.String,    pairingLinkId: Schema.optional(Schema.String),    sessionId: Schema.optional(Schema.String),    cause: Schema.Defect(),  },) {  static fromCause(input: {    readonly operation: PrimaryEnvironmentRequestOperation;    readonly cause: unknown;    readonly fallbackMessage: (status: number) => string;    readonly formatDetail?: (detail: string, status: number) => string;    readonly pairingLinkId?: string;    readonly sessionId?: string;  }): PrimaryEnvironmentRequestError {    const status = readHttpApiStatus(input.cause) ?? 500;    const rawDetail = readHttpApiErrorMessage(input.cause, input.fallbackMessage(status));    return new PrimaryEnvironmentRequestError({      operation: input.operation,      status,      detail: input.formatDetail?.(rawDetail, status) ?? rawDetail,      ...(input.pairingLinkId !== undefined ? { pairingLinkId: input.pairingLinkId } : {}),      ...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}),      cause: input.cause,    });  }   override get message(): string {    return this.detail;  }} export const isPrimaryEnvironmentRequestError = Schema.is(PrimaryEnvironmentRequestError);const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError); export interface ServerPairingLinkRecord {  readonly id: string;  readonly credential: 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 {  const bootstrap = window.desktopBridge?.getLocalEnvironmentBootstrap();  return typeof bootstrap?.bootstrapToken === "string" && bootstrap.bootstrapToken.length > 0    ? bootstrap.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,        fallbackMessage: (status) => `Failed to load server auth session state (${status}).`,      });    }  });} 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 "EnvironmentInternalError":      return 500;  }} function readHttpApiErrorMessage(error: unknown, fallbackMessage: string): string {  if (!isEnvironmentHttpCommonError(error)) {    return fallbackMessage;  }  switch (error._tag) {    case "EnvironmentAuthInvalidError":      return error.reason === "missing_credential"        ? "Authentication required."        : "Invalid bootstrap credential.";    case "EnvironmentRequestInvalidError":      return error.reason === "invalid_scope"        ? "Requested token scope is invalid."        : "Requested scope exceeds the bootstrap credential grant.";    case "EnvironmentScopeRequiredError":      return `The authenticated token is missing required scope: ${error.requiredScope}.`;    case "EnvironmentOperationForbiddenError":      return "This operation is not allowed for the current session.";    case "EnvironmentInternalError":      return fallbackMessage;  }} const INVALID_BOOTSTRAP_CREDENTIAL_MESSAGES = new Set([  "Invalid bootstrap credential.",  "Unknown bootstrap credential.",]); function toFriendlyBootstrapErrorMessage(status: number, message: string): string {  const trimmedMessage = message.trim();  if (status === 401 && INVALID_BOOTSTRAP_CREDENTIAL_MESSAGES.has(trimmedMessage)) {    return "Invalid pairing token. Check the token and try again.";  }   return trimmedMessage;} 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) {      throw PrimaryEnvironmentRequestError.fromCause({        operation: "exchange-bootstrap-credential",        cause: error,        fallbackMessage: (status) => `Failed to bootstrap auth session (${status}).`,        formatDetail: (detail, status) => toFriendlyBootstrapErrorMessage(status, detail),      });    }  });} async function waitForAuthenticatedSessionAfterBootstrap(): Promise<AuthSessionState> {  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<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 Error("Enter a pairing token to continue.");  }   resolvedAuthenticatedGateState = null;  await exchangeBootstrapCredential(trimmedCredential);  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,      fallbackMessage: (status) => `Failed to create pairing credential (${status}).`,    });  }} export async function listServerPairingLinks(): Promise<ReadonlyArray<ServerPairingLinkRecord>> {  try {    const pairingLinks = await runPrimaryHttp(      PrimaryEnvironmentHttpClient.pipe(        Effect.flatMap((client) => client.auth.pairingLinks({ headers: {} })),      ),    );    return pairingLinks.map((pairingLink) => {      const timestamps = {        createdAt: DateTime.formatIso(pairingLink.createdAt),        expiresAt: DateTime.formatIso(pairingLink.expiresAt),      };      if (pairingLink.label === undefined) {        return {          id: pairingLink.id,          credential: pairingLink.credential,          scopes: pairingLink.scopes,          subject: pairingLink.subject,          createdAt: timestamps.createdAt,          expiresAt: timestamps.expiresAt,        };      }      return {        id: pairingLink.id,        credential: pairingLink.credential,        scopes: pairingLink.scopes,        subject: pairingLink.subject,        label: pairingLink.label,        createdAt: timestamps.createdAt,        expiresAt: timestamps.expiresAt,      };    });  } catch (error) {    throw PrimaryEnvironmentRequestError.fromCause({      operation: "list-pairing-links",      cause: error,      fallbackMessage: (status) => `Failed to load pairing links (${status}).`,    });  }} 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,      fallbackMessage: (status) => `Failed to revoke pairing link (${status}).`,    });  }} export async function listServerClientSessions(): Promise<  ReadonlyArray<ServerClientSessionRecord>> {  try {    const clientSessions = await runPrimaryHttp(      PrimaryEnvironmentHttpClient.pipe(        Effect.flatMap((client) => client.auth.clients({ headers: {} })),      ),    );    return clientSessions.map((clientSession) => ({      sessionId: clientSession.sessionId,      subject: clientSession.subject,      scopes: clientSession.scopes,      method: clientSession.method,      client: clientSession.client,      issuedAt: DateTime.formatIso(clientSession.issuedAt),      expiresAt: DateTime.formatIso(clientSession.expiresAt),      lastConnectedAt:        clientSession.lastConnectedAt === null          ? null          : DateTime.formatIso(clientSession.lastConnectedAt),      connected: clientSession.connected,      current: clientSession.current,    }));  } catch (error) {    throw PrimaryEnvironmentRequestError.fromCause({      operation: "list-client-sessions",      cause: error,      fallbackMessage: (status) => `Failed to load paired clients (${status}).`,    });  }} 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,      fallbackMessage: (status) => `Failed to revoke client session (${status}).`,    });  }} 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,      fallbackMessage: (status) => `Failed to revoke other client sessions (${status}).`,    });  }} export async function resolveInitialServerAuthGateState(): Promise<ServerAuthGateState> {  if (resolvedAuthenticatedGateState?.status === "authenticated") {    return resolvedAuthenticatedGateState;  }   if (bootstrapPromise) {    return bootstrapPromise;  }   const nextPromise = bootstrapServerAuth();  bootstrapPromise = nextPromise;

Lines 1–500 of 518.