1742 lines · 57.4 KB · TypeScript
RawDownload
import type {  DesktopSshEnvironmentBootstrap,  DesktopSshEnvironmentTarget,} from "@t3tools/contracts";import * as NetService from "@t3tools/shared/Net";import { extractJsonObject, fromLenientJson } from "@t3tools/shared/schemaJson";import { satisfiesSemverRange } from "@t3tools/shared/semver";import { getUrlDiagnostics } from "@t3tools/shared/urlDiagnostics";import * as Cause from "effect/Cause";import * as Context from "effect/Context";import * as Deferred from "effect/Deferred";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as Ref from "effect/Ref";import * as Schedule from "effect/Schedule";import * as Schema from "effect/Schema";import * as Scope from "effect/Scope";import * as Stream from "effect/Stream";import * as HttpClient from "effect/unstable/http/HttpClient";import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";import * as ChildProcess from "effect/unstable/process/ChildProcess";import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; import * as SshAuth from "./auth.ts";import {  baseSshArgs,  buildSshHostSpecEffect,  collectProcessOutput,  getLastNonEmptyOutputLine,  remoteStateKey,  resolveSshCommand,  resolveSshTarget,  runSshCommand,  targetConnectionKey,  utf8ByteLength,} from "./command.ts";import {  SshAuthenticationHelperError,  SshCommandCancelledError,  SshCommandError,  SshHttpBridgeInvalidUrlError,  SshHttpBridgeMissingUrlError,  SshHttpBridgeNonLoopbackUrlError,  SshHttpBridgeError,  SshInvalidTargetError,  SshLaunchInvalidPortError,  SshLaunchOutputParseError,  SshLaunchPortMissingError,  SshLaunchError,  SshPairingCredentialMissingError,  SshPairingInvalidCredentialError,  SshPairingOutputParseError,  SshPairingError,  SshPasswordPromptCancelledError,  SshPasswordPromptUnavailableError,  SshPasswordPromptError,  SshReadinessProbeError,  SshReadinessProbeTimeoutError,  SshReadinessTimeoutError,  SshReadinessError,  SshTunnelExitError,  SshTunnelMonitorError,  SshTunnelSpawnError,} from "./errors.ts"; const isSshReadinessError = Schema.is(SshReadinessError); export const DEFAULT_REMOTE_PORT = 3773;const REMOTE_PORT_SCAN_WINDOW = 200;const SSH_READY_TIMEOUT_MS = 20_000;const SSH_READY_PROBE_TIMEOUT_MS = 1_000;const TUNNEL_SHUTDOWN_TIMEOUT_MS = 2_000;const REMOTE_READY_TIMEOUT_MS = 15_000;const REMOTE_REUSE_READY_TIMEOUT_MS = 2_000; export interface RemoteT3RunnerOptions {  readonly packageSpec?: string;  readonly nodeScriptPath?: string | null;  readonly nodeEngineRange?: string | null;} export interface SshEnvironmentManagerOptions {  readonly resolveCliPackageSpec?: () => string;  readonly resolveCliRunner?: Effect.Effect<RemoteT3RunnerOptions>;} interface SshTunnelEntry {  readonly key: string;  readonly target: DesktopSshEnvironmentTarget;  readonly remotePort: number;  readonly remoteServerKind: "external" | "managed" | null;  readonly localPort: number;  readonly httpBaseUrl: string;  readonly wsBaseUrl: string;  readonly process: ChildProcessSpawner.ChildProcessHandle;  readonly scope: Scope.Scope;} type SshEnvironmentEffectContext =  | ChildProcessSpawner.ChildProcessSpawner  | FileSystem.FileSystem  | Path.Path  | HttpClient.HttpClient  | NetService.NetService  | SshAuth.SshPasswordPrompt; type SshEnvironmentEffectError =  | SshCommandError  | SshInvalidTargetError  | SshLaunchError  | SshPairingError  | SshReadinessError  | SshPasswordPromptError  | NetService.NetError; function sshTargetLogFields(target: DesktopSshEnvironmentTarget) {  return {    alias: target.alias,    hostname: target.hostname,    username: target.username,    port: target.port,  };} function sshRunnerLogFields(runner: RemoteT3RunnerOptions | undefined) {  if (runner?.nodeScriptPath?.trim()) {    return { runner: "node-script", nodeScriptPath: runner.nodeScriptPath.trim() };  }  if (runner?.packageSpec?.trim()) {    return { runner: "package", packageSpec: runner.packageSpec.trim() };  }  return { runner: "default" };} interface SshAuthOperationInput<T> {  readonly key: string;  readonly target: DesktopSshEnvironmentTarget;  readonly operation: (    authOptions: SshAuth.SshAuthOptions,  ) => Effect.Effect<T, SshEnvironmentEffectError, SshEnvironmentEffectContext>;} interface SshAuthAttemptInput<T> extends SshAuthOperationInput<T> {  readonly promptCount: number;  readonly authSecret: string | null;} /** * @effect-expect-leaking ChildProcessSpawner | FileSystem | HttpClient | NetService | Path | SshAuth.SshPasswordPrompt */export class SshEnvironmentManager extends Context.Service<  SshEnvironmentManager,  {    readonly ensureEnvironment: (      target: DesktopSshEnvironmentTarget,      options?: { readonly issuePairingToken?: boolean },    ) => Effect.Effect<      DesktopSshEnvironmentBootstrap,      SshEnvironmentEffectError,      SshEnvironmentEffectContext    >;    readonly disconnectEnvironment: (      target: DesktopSshEnvironmentTarget,    ) => Effect.Effect<void, SshEnvironmentEffectError, SshEnvironmentEffectContext>;  }>()("@t3tools/ssh/tunnel/SshEnvironmentManager") {} const RemoteLaunchResult = Schema.Struct({  remotePort: Schema.Number,  serverKind: Schema.optional(Schema.Literals(["external", "managed"])),}); const RemotePairingResult = Schema.Struct({  credential: Schema.String,}); const decodeRemoteLaunchResult = Schema.decodeEffect(fromLenientJson(RemoteLaunchResult));const decodeRemotePairingResult = Schema.decodeEffect(fromLenientJson(RemotePairingResult)); const decodeRemoteJsonOutput = <A, E>(  stdout: string,  decode: (input: string) => Effect.Effect<A, E>,): Effect.Effect<A, E> =>  decode(stdout).pipe(    Effect.catch((error) =>      Effect.gen(function* () {        const jsonObject = extractJsonObject(stdout);        if (jsonObject === stdout.trim()) {          return yield* Effect.fail(error);        }        const exit = yield* Effect.exit(decode(jsonObject));        if (Exit.isSuccess(exit)) {          return exit.value;        }        return yield* Effect.fail(error);      }),    ),  ); const decodeRemoteLaunchOutput = (stdout: string) =>  decodeRemoteJsonOutput(stdout, decodeRemoteLaunchResult); const decodeRemotePairingOutput = (stdout: string) =>  decodeRemoteJsonOutput(stdout, decodeRemotePairingResult); const remoteNodeEngineCheckMain = function remoteNodeEngineCheckMain() {  const range = process.argv[2] || "";  const rawVersion =    process.versions && process.versions.node ? process.versions.node : process.version;   if (!satisfiesSemverRange(rawVersion, range)) {    process.stderr.write(      "Remote node " + rawVersion + " does not satisfy required range " + range + ".\n",    );    process.exit(1);  }}; function buildRemoteNodeEngineCheckScript(): string {  return `${satisfiesSemverRange.toString()}(${remoteNodeEngineCheckMain.toString()})();`;} function stripTrailingNewlines(value: string): string {  return value.replace(/\n+$/u, "");} function shellSingleQuote(value: string): string {  return `'${value.replaceAll("'", "'\\''")}'`;} function applyScriptPlaceholders(  template: string,  replacements: Readonly<Record<string, string>>,): string {  let result = template;  for (const [token, value] of Object.entries(replacements)) {    result = result.replaceAll(`@@${token}@@`, value);  }  return result;} const SSH_FAILURE_DIAGNOSTIC_DEPTH = 6; function describeReadinessCauseAtDepth(cause: unknown, depth: number): unknown {  if (depth >= SSH_FAILURE_DIAGNOSTIC_DEPTH) return { truncated: true };  const describeNested = (nested: unknown) => describeReadinessCauseAtDepth(nested, depth + 1);  if (Cause.isCause(cause)) {    return {      reasons: cause.reasons.map((reason) => {        if (Cause.isFailReason(reason)) {          return { _tag: reason._tag, error: describeNested(reason.error) };        }        if (Cause.isDieReason(reason)) {          return { _tag: reason._tag, defect: describeNested(reason.defect) };        }        return { _tag: reason._tag, fiberId: reason.fiberId };      }),    };  }  if (isSshReadinessError(cause)) {    const { cause: nestedCause, ...attributes } = cause as SshReadinessError & {      readonly cause?: unknown;    };    return {      ...attributes,      message: cause.message,      ...(nestedCause === undefined ? {} : { cause: describeNested(nestedCause) }),    };  }  if (cause instanceof Error) {    const tagged = cause as Error & { readonly _tag?: unknown; readonly reason?: unknown };    return {      ...(typeof tagged._tag === "string" ? { _tag: tagged._tag } : { name: cause.name }),      ...(tagged.reason === undefined ? {} : { reason: describeNested(tagged.reason) }),      ...(cause.cause === undefined ? {} : { cause: describeNested(cause.cause) }),    };  }  if (typeof cause !== "object" || cause === null) {    return { type: cause === null ? "null" : typeof cause };  }   const record = cause as Readonly<Record<string, unknown>>;  return {    ...(typeof record._tag === "string" ? { _tag: record._tag } : {}),    ...(record.reason === undefined ? {} : { reason: describeNested(record.reason) }),    ...(record.cause === undefined ? {} : { cause: describeNested(record.cause) }),  };} export function describeReadinessCause(cause: unknown): unknown {  return describeReadinessCauseAtDepth(cause, 0);} function readinessUrlDiagnostics(input: string) {  const url = new URL(input);  const diagnostics = getUrlDiagnostics(input);  return {    protocol: diagnostics.protocol ?? url.protocol,    hostname: diagnostics.hostname ?? url.hostname,    ...(url.port === "" ? {} : { port: url.port }),    urlLength: diagnostics.inputLength,    pathnameLength: utf8ByteLength(url.pathname),    hasQuery: url.search !== "",    hasFragment: url.hash !== "",  };} export const REMOTE_PICK_PORT_SCRIPT = `const fs = require("node:fs");const net = require("node:net");const filePath = process.argv[2] ?? "";const defaultPort = Number.parseInt(process.argv[3] ?? "", 10);const scanWindow = Number.parseInt(process.argv[4] ?? "", 10);const raw = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8").trim() : "";const preferred = Number.parseInt(raw, 10);const start = Number.isInteger(preferred) ? preferred : defaultPort;const end = start + scanWindow; function tryPort(port) {  return new Promise((resolve) => {    const server = net.createServer();    server.unref();    server.once("error", () => resolve(false));    server.listen(port, "127.0.0.1", () => {      server.close((error) => resolve(error ? false : port));    });  });} (async () => {  for (let port = start; port < end; port += 1) {    const available = await tryPort(port);    if (available) {      process.stdout.write(String(port));      return;    }  }  process.exit(1);})().catch(() => process.exit(1));`; export const REMOTE_WAIT_READY_SCRIPT = `const http = require("node:http");const port = Number.parseInt(process.argv[2] ?? "", 10);const timeoutMs = Number.parseInt(process.argv[3] ?? "", 10);const probeTimeoutMs = Number.parseInt(process.argv[4] ?? "", 10);if (!Number.isInteger(port) || !Number.isInteger(timeoutMs) || !Number.isInteger(probeTimeoutMs)) {  process.exit(1);}const deadline = Date.now() + timeoutMs; function sleep(ms) {  return new Promise((resolve) => setTimeout(resolve, ms));} function probe() {  return new Promise((resolve) => {    const request = http.get(      {        hostname: "127.0.0.1",        port,        path: "/",        timeout: probeTimeoutMs,      },      (response) => {        response.resume();        response.once("end", () => {          resolve(response.statusCode >= 200 && response.statusCode < 300);        });      },    );    request.once("timeout", () => {      request.destroy();      resolve(false);    });    request.once("error", () => resolve(false));  });} (async () => {  while (Date.now() < deadline) {    if (await probe()) {      process.exit(0);    }    await sleep(100);  }  process.exit(1);})().catch(() => process.exit(1));`; export const REMOTE_NODE_ENV_SCRIPT = `prepend_path_if_dir() {  if [ -d "$1" ]; then    case ":$PATH:" in      *":$1:"*) ;;      *) PATH="$1:$PATH" ;;    esac  fi} remote_node_satisfies_engine() {  T3_NODE_ENGINE_RANGE=@@T3_NODE_ENGINE_RANGE@@  if [ -z "$T3_NODE_ENGINE_RANGE" ]; then    return 0  fi  node - "$T3_NODE_ENGINE_RANGE" <<'NODE'@@T3_NODE_ENGINE_CHECK_SCRIPT@@NODE} ensure_remote_node_path() {  if command -v node >/dev/null 2>&1 && remote_node_satisfies_engine >/dev/null 2>&1; then    return 0  fi   prepend_path_if_dir "$HOME/.local/bin"  prepend_path_if_dir "$HOME/bin"  prepend_path_if_dir "/opt/homebrew/bin"  prepend_path_if_dir "/usr/local/bin"  prepend_path_if_dir "/usr/bin"  prepend_path_if_dir "/bin"   if [ -z "\${VOLTA_HOME:-}" ]; then    VOLTA_HOME="$HOME/.volta"  fi  export VOLTA_HOME  prepend_path_if_dir "$VOLTA_HOME/bin"   prepend_path_if_dir "$HOME/.asdf/shims"  prepend_path_if_dir "$HOME/.asdf/bin"  if [ ! -x "$HOME/.asdf/shims/node" ] && [ -s "$HOME/.asdf/asdf.sh" ]; then    # shellcheck disable=SC1090    . "$HOME/.asdf/asdf.sh"  fi   prepend_path_if_dir "$HOME/.local/share/mise/shims"  prepend_path_if_dir "$HOME/.mise/shims"  if ! command -v node >/dev/null 2>&1 && command -v mise >/dev/null 2>&1; then    eval "$(mise activate sh)" >/dev/null 2>&1 || true  fi   if [ -z "\${FNM_DIR:-}" ]; then    FNM_DIR="$HOME/.local/share/fnm"  fi  export FNM_DIR  prepend_path_if_dir "$FNM_DIR"  prepend_path_if_dir "$HOME/.fnm"  if ! command -v node >/dev/null 2>&1 && command -v fnm >/dev/null 2>&1; then    eval "$(fnm env --shell bash)" >/dev/null 2>&1 || true    fnm use --silent-if-unchanged >/dev/null 2>&1 || fnm use default >/dev/null 2>&1 || true  fi   prepend_path_if_dir "$HOME/.nodenv/bin"  prepend_path_if_dir "$HOME/.nodenv/shims"  if ! command -v node >/dev/null 2>&1 && command -v nodenv >/dev/null 2>&1; then    eval "$(nodenv init -)" >/dev/null 2>&1 || true  fi   if [ -z "\${NVM_DIR:-}" ]; then    NVM_DIR="$HOME/.nvm"  fi  export NVM_DIR   if [ -s "$NVM_DIR/nvm.sh" ]; then    # shellcheck disable=SC1090    . "$NVM_DIR/nvm.sh"    if ! command -v node >/dev/null 2>&1 && command -v nvm >/dev/null 2>&1; then      nvm use --silent default >/dev/null 2>&1 || nvm use --silent node >/dev/null 2>&1 || nvm use --silent --lts >/dev/null 2>&1 || true    fi  fi   if ! command -v node >/dev/null 2>&1 && [ -d "$NVM_DIR/versions/node" ]; then    for T3_NODE_BIN in "$NVM_DIR"/versions/node/*/bin; do      if [ -x "$T3_NODE_BIN/node" ]; then        PATH="$T3_NODE_BIN:$PATH"        export PATH      fi    done  fi   command -v node >/dev/null 2>&1 && remote_node_satisfies_engine}`; export const REMOTE_RUNNER_SCRIPT = `#!/bin/shset -eu@@T3_NODE_ENV_SCRIPT@@ensure_remote_node_path || trueT3_NODE_SCRIPT_PATH=@@T3_NODE_SCRIPT_PATH@@if [ -n "$T3_NODE_SCRIPT_PATH" ]; then  if ! command -v node >/dev/null 2>&1; then    printf 'Remote host is missing node on PATH. Install Node or configure a supported version manager for non-interactive shells.\\n' >&2    exit 1  fi  exec node "$T3_NODE_SCRIPT_PATH" "$@"fiif command -v t3 >/dev/null 2>&1; then

Lines 1–500 of 1742.