548 lines · 17.7 KB · TypeScript
RawDownload
import * as NodeOS from "node:os"; import {  createAdvertisedEndpoint,  type CreateAdvertisedEndpointInput,} from "@t3tools/client-runtime";import type {  AdvertisedEndpoint,  AdvertisedEndpointProvider,  DesktopServerExposureMode,  DesktopServerExposureState,} from "@t3tools/contracts";import * as Context from "effect/Context";import * as Data from "effect/Data";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Ref from "effect/Ref";import { HttpClient } from "effect/unstable/http";import { ChildProcessSpawner } from "effect/unstable/process"; import { DEFAULT_DESKTOP_SETTINGS, type DesktopSettings } from "../settings/DesktopAppSettings.ts";import * as DesktopConfig from "../app/DesktopConfig.ts";import { resolveTailscaleAdvertisedEndpoints } from "./tailscaleEndpointProvider.ts";import * as DesktopAppSettingsService from "../settings/DesktopAppSettings.ts"; export const DESKTOP_LOOPBACK_HOST = "127.0.0.1";const DESKTOP_LAN_BIND_HOST = "0.0.0.0"; export interface DesktopNetworkInterfaceInfo {  readonly address: string;  readonly family: string | number;  readonly internal: boolean;  readonly netmask?: string;  readonly mac?: string;  readonly cidr?: string | null;  readonly scopeid?: number;} export type DesktopNetworkInterfaces = Readonly<  Record<string, readonly DesktopNetworkInterfaceInfo[] | undefined>>; interface ResolvedDesktopServerExposure {  readonly mode: DesktopServerExposureMode;  readonly bindHost: string;  readonly localHttpUrl: string;  readonly localWsUrl: string;  readonly endpointUrl: string | null;  readonly advertisedHost: string | null;} interface DesktopAdvertisedEndpointInput {  readonly port: number;  readonly exposure: ResolvedDesktopServerExposure;  readonly customHttpsEndpointUrls?: readonly string[];} const DESKTOP_CORE_ENDPOINT_PROVIDER: AdvertisedEndpointProvider = {  id: "desktop-core",  label: "Desktop",  kind: "core",  isAddon: false,}; const DESKTOP_MANUAL_ENDPOINT_PROVIDER: AdvertisedEndpointProvider = {  id: "manual",  label: "Manual",  kind: "manual",  isAddon: false,}; const normalizeOptionalHost = (value: string | undefined): string | undefined => {  const normalized = value?.trim();  return normalized && normalized.length > 0 ? normalized : undefined;}; const isUsableLanIpv4Address = (address: string): boolean =>  !address.startsWith("127.") && !address.startsWith("169.254."); const isHttpsEndpointUrl = (value: string): boolean => {  try {    return new URL(value).protocol === "https:";  } catch {    return false;  }}; const resolveLanAdvertisedHost = (  networkInterfaces: DesktopNetworkInterfaces,  explicitHost: string | undefined,): string | null => {  const normalizedExplicitHost = normalizeOptionalHost(explicitHost);  if (normalizedExplicitHost) {    return normalizedExplicitHost;  }   for (const interfaceAddresses of Object.values(networkInterfaces)) {    if (!interfaceAddresses) continue;     for (const address of interfaceAddresses) {      if (address.internal) continue;      if (address.family !== "IPv4") continue;      if (!isUsableLanIpv4Address(address.address)) continue;      return address.address;    }  }   return null;}; const resolveDesktopServerExposure = (input: {  readonly mode: DesktopServerExposureMode;  readonly port: number;  readonly networkInterfaces: DesktopNetworkInterfaces;  readonly advertisedHostOverride?: string;}): ResolvedDesktopServerExposure => {  const localHttpUrl = `http://${DESKTOP_LOOPBACK_HOST}:${input.port}`;  const localWsUrl = `ws://${DESKTOP_LOOPBACK_HOST}:${input.port}`;   if (input.mode === "local-only") {    return {      mode: input.mode,      bindHost: DESKTOP_LOOPBACK_HOST,      localHttpUrl,      localWsUrl,      endpointUrl: null,      advertisedHost: null,    };  }   const advertisedHost = resolveLanAdvertisedHost(    input.networkInterfaces,    input.advertisedHostOverride,  );   return {    mode: input.mode,    bindHost: DESKTOP_LAN_BIND_HOST,    localHttpUrl,    localWsUrl,    endpointUrl: advertisedHost ? `http://${advertisedHost}:${input.port}` : null,    advertisedHost,  };}; const createDesktopEndpoint = (  input: Omit<CreateAdvertisedEndpointInput, "provider" | "source">,): AdvertisedEndpoint =>  createAdvertisedEndpoint({    ...input,    provider: DESKTOP_CORE_ENDPOINT_PROVIDER,    source: "desktop-core",  }); const createManualEndpoint = (  input: Omit<CreateAdvertisedEndpointInput, "provider" | "source">,): AdvertisedEndpoint =>  createAdvertisedEndpoint({    ...input,    provider: DESKTOP_MANUAL_ENDPOINT_PROVIDER,    source: "user",  }); const resolveDesktopCoreAdvertisedEndpoints = (  input: DesktopAdvertisedEndpointInput,): readonly AdvertisedEndpoint[] => {  const endpoints: AdvertisedEndpoint[] = [    createDesktopEndpoint({      id: `desktop-loopback:${input.port}`,      label: "This machine",      httpBaseUrl: input.exposure.localHttpUrl,      reachability: "loopback",      status: "available",      description: "Loopback endpoint for this desktop app.",    }),  ];   if (input.exposure.endpointUrl) {    endpoints.push(      createDesktopEndpoint({        id: `desktop-lan:${input.exposure.endpointUrl}`,        label: "Local network",        httpBaseUrl: input.exposure.endpointUrl,        reachability: "lan",        status: "available",        isDefault: true,        description: "Reachable from devices on the same network.",      }),    );  }   for (const customEndpointUrl of input.customHttpsEndpointUrls ?? []) {    try {      const isHttpsEndpoint = isHttpsEndpointUrl(customEndpointUrl);      endpoints.push(        createManualEndpoint({          id: `manual:${customEndpointUrl}`,          label: isHttpsEndpoint ? "Custom HTTPS" : "Custom endpoint",          httpBaseUrl: customEndpointUrl,          reachability: "public",          ...(isHttpsEndpoint ? ({ hostedHttpsCompatibility: "compatible" } as const) : {}),          status: "unknown",          description: isHttpsEndpoint            ? "User-configured HTTPS endpoint for this desktop backend."            : "User-configured endpoint for this desktop backend.",        }),      );    } catch {      // Ignore malformed user-configured endpoints without dropping valid endpoints.    }  }   return endpoints;}; type DesktopServerExposurePersistenceOperation = "server-exposure-mode" | "tailscale-serve"; export class DesktopServerExposureNoNetworkAddressError extends Data.TaggedError(  "DesktopServerExposureNoNetworkAddressError",)<{  readonly port: number;}> {  override get message() {    return `No reachable network address is available for desktop network access on port ${this.port}.`;  }} export class DesktopServerExposurePersistenceError extends Data.TaggedError(  "DesktopServerExposurePersistenceError",)<{  readonly operation: DesktopServerExposurePersistenceOperation;  readonly cause: DesktopAppSettingsService.DesktopSettingsWriteError;}> {  override get message() {    return `Failed to persist desktop ${this.operation} settings.`;  }} export type DesktopServerExposureSetModeError =  | DesktopServerExposureNoNetworkAddressError  | DesktopServerExposurePersistenceError; export type DesktopServerExposureError = DesktopServerExposureSetModeError; export interface DesktopServerExposureBackendConfig {  readonly port: number;  readonly bindHost: string;  readonly httpBaseUrl: URL;  readonly tailscaleServeEnabled: boolean;  readonly tailscaleServePort: number;} export interface DesktopServerExposureChange {  readonly state: DesktopServerExposureState;  readonly requiresRelaunch: boolean;} export interface DesktopServerExposureShape {  readonly getState: Effect.Effect<DesktopServerExposureState>;  readonly backendConfig: Effect.Effect<DesktopServerExposureBackendConfig>;  readonly configureFromSettings: (input: {    readonly port: number;  }) => Effect.Effect<DesktopServerExposureState>;  readonly setMode: (    mode: DesktopServerExposureMode,  ) => Effect.Effect<DesktopServerExposureChange, DesktopServerExposureSetModeError>;  readonly setTailscaleServeEnabled: (input: {    readonly enabled: boolean;    readonly port?: number;  }) => Effect.Effect<DesktopServerExposureChange, DesktopServerExposurePersistenceError>;  readonly getAdvertisedEndpoints: Effect.Effect<readonly AdvertisedEndpoint[]>;} export class DesktopServerExposure extends Context.Service<  DesktopServerExposure,  DesktopServerExposureShape>()("t3/desktop/ServerExposure") {} export interface DesktopNetworkInterfacesServiceShape {  readonly read: Effect.Effect<DesktopNetworkInterfaces>;} export class DesktopNetworkInterfacesService extends Context.Service<  DesktopNetworkInterfacesService,  DesktopNetworkInterfacesServiceShape>()("t3/desktop/ServerExposure/NetworkInterfaces") {} interface RuntimeState {  readonly requestedMode: DesktopServerExposureMode;  readonly mode: DesktopServerExposureMode;  readonly port: number;  readonly bindHost: string;  readonly localHttpUrl: string;  readonly localWsUrl: string;  readonly httpBaseUrl: URL;  readonly endpointUrl: Option.Option<string>;  readonly advertisedHost: Option.Option<string>;  readonly tailscaleServeEnabled: boolean;  readonly tailscaleServePort: number;} interface ResolvedRuntimeState {  readonly state: RuntimeState;  readonly unavailable: boolean;} const initialRuntimeState = (): RuntimeState =>  runtimeStateFromResolvedExposure({    requestedMode: DEFAULT_DESKTOP_SETTINGS.serverExposureMode,    settings: DEFAULT_DESKTOP_SETTINGS,    exposure: resolveDesktopServerExposure({      mode: DEFAULT_DESKTOP_SETTINGS.serverExposureMode,      port: 0,      networkInterfaces: {},    }),    port: 0,  }); const toContractState = (state: RuntimeState): DesktopServerExposureState => ({  mode: state.mode,  endpointUrl: Option.getOrNull(state.endpointUrl),  advertisedHost: Option.getOrNull(state.advertisedHost),  tailscaleServeEnabled: state.tailscaleServeEnabled,  tailscaleServePort: state.tailscaleServePort,}); const toBackendConfig = (state: RuntimeState): DesktopServerExposureBackendConfig => ({  port: state.port,  bindHost: state.bindHost,  httpBaseUrl: state.httpBaseUrl,  tailscaleServeEnabled: state.tailscaleServeEnabled,  tailscaleServePort: state.tailscaleServePort,}); const toResolvedExposure = (state: RuntimeState): ResolvedDesktopServerExposure => ({  mode: state.mode,  bindHost: state.bindHost,  localHttpUrl: state.localHttpUrl,  localWsUrl: state.localWsUrl,  endpointUrl: Option.getOrNull(state.endpointUrl),  advertisedHost: Option.getOrNull(state.advertisedHost),}); function runtimeStateFromResolvedExposure(input: {  readonly requestedMode: DesktopServerExposureMode;  readonly settings: DesktopSettings;  readonly exposure: ResolvedDesktopServerExposure;  readonly port: number;}): RuntimeState {  return {    requestedMode: input.requestedMode,    mode: input.exposure.mode,    port: input.port,    bindHost: input.exposure.bindHost,    localHttpUrl: input.exposure.localHttpUrl,    localWsUrl: input.exposure.localWsUrl,    httpBaseUrl: new URL(input.exposure.localHttpUrl),    endpointUrl: Option.fromNullishOr(input.exposure.endpointUrl),    advertisedHost: Option.fromNullishOr(input.exposure.advertisedHost),    tailscaleServeEnabled: input.settings.tailscaleServeEnabled,    tailscaleServePort: input.settings.tailscaleServePort,  };} function resolveRuntimeState(input: {  readonly requestedMode: DesktopServerExposureMode;  readonly settings: DesktopSettings;  readonly port: number;  readonly networkInterfaces: DesktopNetworkInterfaces;  readonly advertisedHostOverride: Option.Option<string>;}): ResolvedRuntimeState {  const advertisedHostOverride = Option.getOrUndefined(input.advertisedHostOverride);  const requestedExposure = resolveDesktopServerExposure({    mode: input.requestedMode,    port: input.port,    networkInterfaces: input.networkInterfaces,    ...(advertisedHostOverride ? { advertisedHostOverride } : {}),  });  const unavailable =    input.requestedMode === "network-accessible" && requestedExposure.endpointUrl === null;  const exposure = unavailable    ? resolveDesktopServerExposure({        mode: "local-only",        port: input.port,        networkInterfaces: input.networkInterfaces,        ...(advertisedHostOverride ? { advertisedHostOverride } : {}),      })    : requestedExposure;   return {    state: runtimeStateFromResolvedExposure({      requestedMode: input.requestedMode,      settings: input.settings,      exposure,      port: input.port,    }),    unavailable,  };} const requiresBackendRelaunch = (previous: RuntimeState, next: RuntimeState): boolean =>  previous.port !== next.port ||  previous.bindHost !== next.bindHost ||  previous.localHttpUrl !== next.localHttpUrl; const make = Effect.gen(function* () {  const config = yield* DesktopConfig.DesktopConfig;  const networkInterfaces = yield* DesktopNetworkInterfacesService;  const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;  const httpClient = yield* HttpClient.HttpClient;  const desktopSettings = yield* DesktopAppSettingsService.DesktopAppSettings;  const stateRef = yield* Ref.make(initialRuntimeState());   const readNetworkInterfaces = networkInterfaces.read;   const getState = Ref.get(stateRef).pipe(Effect.map(toContractState));  const backendConfig = Ref.get(stateRef).pipe(Effect.map(toBackendConfig));   const configureFromSettings = Effect.fn("desktop.serverExposure.configureFromSettings")(    function* ({ port }: { readonly port: number }) {      yield* Effect.annotateCurrentSpan({ port });      const settings = yield* desktopSettings.get;      const currentNetworkInterfaces = yield* readNetworkInterfaces;      const resolved = resolveRuntimeState({        requestedMode: settings.serverExposureMode,        settings,        port,        networkInterfaces: currentNetworkInterfaces,        advertisedHostOverride: config.desktopLanHostOverride,      });      yield* Ref.set(stateRef, resolved.state);      return toContractState(resolved.state);    },  );   const setMode = Effect.fn("desktop.serverExposure.setMode")(function* (    mode: DesktopServerExposureMode,  ) {    yield* Effect.annotateCurrentSpan({ mode });    const previous = yield* Ref.get(stateRef);    const currentSettings = yield* desktopSettings.get;    const nextSettings = {      ...currentSettings,      serverExposureMode: mode,    };    const currentNetworkInterfaces = yield* readNetworkInterfaces;    const resolved = resolveRuntimeState({      requestedMode: mode,      settings: nextSettings,      port: previous.port,      networkInterfaces: currentNetworkInterfaces,      advertisedHostOverride: config.desktopLanHostOverride,    });     if (resolved.unavailable) {      return yield* new DesktopServerExposureNoNetworkAddressError({ port: previous.port });    }     const change = yield* desktopSettings.setServerExposureMode(mode).pipe(      Effect.mapError(        (cause) =>          new DesktopServerExposurePersistenceError({            operation: "server-exposure-mode",            cause,          }),      ),    );     yield* Ref.set(stateRef, resolved.state);    return {      state: toContractState(resolved.state),      requiresRelaunch: change.changed || requiresBackendRelaunch(previous, resolved.state),    };  });   const setTailscaleServeEnabled = Effect.fn("desktop.serverExposure.setTailscaleServeEnabled")(    function* (input: { readonly enabled: boolean; readonly port?: number }) {      yield* Effect.annotateCurrentSpan({        enabled: input.enabled,        ...(input.port === undefined ? {} : { port: input.port }),      });      const result = yield* desktopSettings        .setTailscaleServe({          enabled: input.enabled,          port: Option.fromNullishOr(input.port),        })        .pipe(          Effect.mapError(            (cause) =>              new DesktopServerExposurePersistenceError({                operation: "tailscale-serve",                cause,              }),          ),        );       const nextState = yield* Ref.updateAndGet(stateRef, (current) => ({        ...current,        tailscaleServeEnabled: result.settings.tailscaleServeEnabled,

Lines 1–500 of 548.