905 lines · 29.9 KB · TypeScript
RawDownload
/** * Server - HTTP/WebSocket server service interface. * * Owns startup and shutdown lifecycle of the HTTP server, static asset serving, * and WebSocket request routing. * * @module Server */import http from "node:http"; import Mime from "@effect/platform-node/Mime";import {  CommandId,  DEFAULT_PROVIDER_INTERACTION_MODE,  type ClientOrchestrationCommand,  type OrchestrationCommand,  ORCHESTRATION_WS_CHANNELS,  ORCHESTRATION_WS_METHODS,  PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,  ProjectId,  ThreadId,  WS_CHANNELS,  WS_METHODS,  WebSocketRequest,  type WsResponse as WsResponseMessage,  WsResponse,  type WsPushEnvelopeBase,} from "@t3tools/contracts";import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";import * as Socket from "effect/unstable/socket/Socket";import {  Cause,  Effect,  Exit,  FileSystem,  Layer,  Option,  Path,  Ref,  Result,  Schema,  Scope,  ServiceMap,  Stream,  Struct,} from "effect";import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"; import { createLogger } from "./logger";import { GitManager } from "./git/Services/GitManager.ts";import { TerminalManager } from "./terminal/Services/Manager.ts";import { Keybindings } from "./keybindings";import { searchWorkspaceEntries } from "./workspaceEntries";import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine";import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery";import { OrchestrationReactor } from "./orchestration/Services/OrchestrationReactor";import { ProviderService } from "./provider/Services/ProviderService";import { ProviderHealth } from "./provider/Services/ProviderHealth";import { CheckpointDiffQuery } from "./checkpointing/Services/CheckpointDiffQuery";import { clamp } from "effect/Number";import { Open, resolveAvailableEditors } from "./open";import { ServerConfig } from "./config";import { GitCore } from "./git/Services/GitCore.ts";import { handleProjectFaviconRequest } from "./projectFaviconRoute";import {  ATTACHMENTS_ROUTE_PREFIX,  normalizeAttachmentRelativePath,  resolveAttachmentRelativePath,} from "./attachmentPaths"; import {  createAttachmentId,  resolveAttachmentPath,  resolveAttachmentPathById,} from "./attachmentStore.ts";import { parseBase64DataUrl } from "./imageMime.ts";import { AnalyticsService } from "./telemetry/Services/AnalyticsService.ts";import { expandHomePath } from "./os-jank.ts";import { makeServerPushBus, type ServerPushClient } from "./wsServer/pushBus.ts";import { makeServerReadiness } from "./wsServer/readiness.ts";import { decodeJsonResult, formatSchemaError } from "@t3tools/shared/schemaJson"; /** * ServerShape - Service API for server lifecycle control. */export interface ServerShape {  /**   * Start HTTP and WebSocket listeners.   */  readonly start: Effect.Effect<    http.Server,    ServerLifecycleError,    Scope.Scope | ServerRuntimeServices | ServerConfig | FileSystem.FileSystem | Path.Path  >;   /**   * Wait for process shutdown signals.   */  readonly stopSignal: Effect.Effect<void, never>;} /** * Server - Service tag for HTTP/WebSocket lifecycle management. */export class Server extends ServiceMap.Service<Server, ServerShape>()("t3/wsServer/Server") {} function websocketRawToString(raw: string | Uint8Array): string {  if (typeof raw === "string") {    return raw;  }  return Buffer.from(raw).toString("utf8");} function toPosixRelativePath(input: string): string {  return input.replaceAll("\\", "/");} function resolveWorkspaceWritePath(params: {  workspaceRoot: string;  relativePath: string;  path: Path.Path;}): Effect.Effect<{ absolutePath: string; relativePath: string }, RouteRequestError> {  const normalizedInputPath = params.relativePath.trim();  if (params.path.isAbsolute(normalizedInputPath)) {    return Effect.fail(      new RouteRequestError({        message: "Workspace file path must be relative to the project root.",      }),    );  }   const absolutePath = params.path.resolve(params.workspaceRoot, normalizedInputPath);  const relativeToRoot = toPosixRelativePath(    params.path.relative(params.workspaceRoot, absolutePath),  );  if (    relativeToRoot.length === 0 ||    relativeToRoot === "." ||    relativeToRoot.startsWith("../") ||    relativeToRoot === ".." ||    params.path.isAbsolute(relativeToRoot)  ) {    return Effect.fail(      new RouteRequestError({        message: "Workspace file path must stay within the project root.",      }),    );  }   return Effect.succeed({    absolutePath,    relativePath: relativeToRoot,  });} function stripRequestTag<T extends { _tag: string }>(body: T) {  return Struct.omit(body, ["_tag"]);} const encodeWsResponse = Schema.encodeEffect(Schema.fromJsonString(WsResponse));const decodeWebSocketRequest = decodeJsonResult(WebSocketRequest); export type ServerCoreRuntimeServices =  | OrchestrationEngineService  | ProjectionSnapshotQuery  | CheckpointDiffQuery  | OrchestrationReactor  | ProviderService  | ProviderHealth; export type ServerRuntimeServices =  | ServerCoreRuntimeServices  | GitManager  | GitCore  | TerminalManager  | Keybindings  | Open  | AnalyticsService; export class ServerLifecycleError extends Schema.TaggedErrorClass<ServerLifecycleError>()(  "ServerLifecycleError",  {    operation: Schema.String,    cause: Schema.optional(Schema.Defect),  },) {} class RouteRequestError extends Schema.TaggedErrorClass<RouteRequestError>()("RouteRequestError", {  message: Schema.String,}) {} export const createServer = Effect.fn(function* (): Effect.fn.Return<  http.Server,  ServerLifecycleError,  Scope.Scope | ServerRuntimeServices | ServerConfig | FileSystem.FileSystem | Path.Path> {  const serverConfig = yield* ServerConfig;  const {    port,    cwd,    keybindingsConfigPath,    staticDir,    devUrl,    authToken,    host,    logWebSocketEvents,    autoBootstrapProjectFromCwd,  } = serverConfig;  const availableEditors = resolveAvailableEditors();   const gitManager = yield* GitManager;  const terminalManager = yield* TerminalManager;  const keybindingsManager = yield* Keybindings;  const providerHealth = yield* ProviderHealth;  const git = yield* GitCore;  const fileSystem = yield* FileSystem.FileSystem;  const path = yield* Path.Path;   yield* keybindingsManager.syncDefaultKeybindingsOnStartup.pipe(    Effect.catch((error) =>      Effect.logWarning("failed to sync keybindings defaults on startup", {        path: error.configPath,        detail: error.detail,        cause: error.cause,      }),    ),  );   const providerStatuses = yield* providerHealth.getStatuses;   const clients = yield* Ref.make(new Set<ServerPushClient>());  const logger = createLogger("ws");  const readiness = yield* makeServerReadiness;   function logOutgoingPush(push: WsPushEnvelopeBase, recipients: number) {    if (!logWebSocketEvents) return;    logger.event("outgoing push", {      channel: push.channel,      sequence: push.sequence,      recipients,      payload: push.data,    });  }   const pushBus = yield* makeServerPushBus({    clients,    logOutgoingPush,  });  yield* readiness.markPushBusReady;  yield* keybindingsManager.start.pipe(    Effect.mapError(      (cause) => new ServerLifecycleError({ operation: "keybindingsRuntimeStart", cause }),    ),  );  yield* readiness.markKeybindingsReady;   const normalizeDispatchCommand = Effect.fnUntraced(function* (input: {    readonly command: ClientOrchestrationCommand;  }) {    const normalizeProjectWorkspaceRoot = Effect.fnUntraced(function* (workspaceRoot: string) {      const normalizedWorkspaceRoot = path.resolve(yield* expandHomePath(workspaceRoot.trim()));      const workspaceStat = yield* fileSystem        .stat(normalizedWorkspaceRoot)        .pipe(Effect.catch(() => Effect.succeed(null)));      if (!workspaceStat) {        return yield* new RouteRequestError({          message: `Project directory does not exist: ${normalizedWorkspaceRoot}`,        });      }      if (workspaceStat.type !== "Directory") {        return yield* new RouteRequestError({          message: `Project path is not a directory: ${normalizedWorkspaceRoot}`,        });      }      return normalizedWorkspaceRoot;    });     if (input.command.type === "project.create") {      return {        ...input.command,        workspaceRoot: yield* normalizeProjectWorkspaceRoot(input.command.workspaceRoot),      } satisfies OrchestrationCommand;    }     if (input.command.type === "project.meta.update" && input.command.workspaceRoot !== undefined) {      return {        ...input.command,        workspaceRoot: yield* normalizeProjectWorkspaceRoot(input.command.workspaceRoot),      } satisfies OrchestrationCommand;    }     if (input.command.type !== "thread.turn.start") {      return input.command as OrchestrationCommand;    }    const turnStartCommand = input.command;     const normalizedAttachments = yield* Effect.forEach(      turnStartCommand.message.attachments,      (attachment) =>        Effect.gen(function* () {          const parsed = parseBase64DataUrl(attachment.dataUrl);          if (!parsed || !parsed.mimeType.startsWith("image/")) {            return yield* new RouteRequestError({              message: `Invalid image attachment payload for '${attachment.name}'.`,            });          }           const bytes = Buffer.from(parsed.base64, "base64");          if (bytes.byteLength === 0 || bytes.byteLength > PROVIDER_SEND_TURN_MAX_IMAGE_BYTES) {            return yield* new RouteRequestError({              message: `Image attachment '${attachment.name}' is empty or too large.`,            });          }           const attachmentId = createAttachmentId(turnStartCommand.threadId);          if (!attachmentId) {            return yield* new RouteRequestError({              message: "Failed to create a safe attachment id.",            });          }           const persistedAttachment = {            type: "image" as const,            id: attachmentId,            name: attachment.name,            mimeType: parsed.mimeType.toLowerCase(),            sizeBytes: bytes.byteLength,          };           const attachmentPath = resolveAttachmentPath({            stateDir: serverConfig.stateDir,            attachment: persistedAttachment,          });          if (!attachmentPath) {            return yield* new RouteRequestError({              message: `Failed to resolve persisted path for '${attachment.name}'.`,            });          }           yield* fileSystem.makeDirectory(path.dirname(attachmentPath), { recursive: true }).pipe(            Effect.mapError(              () =>                new RouteRequestError({                  message: `Failed to create attachment directory for '${attachment.name}'.`,                }),            ),          );          yield* fileSystem.writeFile(attachmentPath, bytes).pipe(            Effect.mapError(              () =>                new RouteRequestError({                  message: `Failed to persist attachment '${attachment.name}'.`,                }),            ),          );           return persistedAttachment;        }),      { concurrency: 1 },    );     return {      ...turnStartCommand,      message: {        ...turnStartCommand.message,        attachments: normalizedAttachments,      },    } satisfies OrchestrationCommand;  });   const serveHttpRequest = Effect.gen(function* () {    const request = yield* HttpServerRequest.HttpServerRequest;    const url = HttpServerRequest.toURL(request);    if (!url) {      return HttpServerResponse.text("Bad Request", { status: 400 });    }     if (url.pathname === "/api/project-favicon") {      return yield* handleProjectFaviconRequest(url.searchParams.get("cwd"));    }     if (url.pathname.startsWith(ATTACHMENTS_ROUTE_PREFIX)) {      const rawRelativePath = url.pathname.slice(ATTACHMENTS_ROUTE_PREFIX.length);      const normalizedRelativePath = normalizeAttachmentRelativePath(rawRelativePath);      if (!normalizedRelativePath) {        return HttpServerResponse.text("Invalid attachment path", { status: 400 });      }       const isIdLookup =        !normalizedRelativePath.includes("/") && !normalizedRelativePath.includes(".");      const filePath = isIdLookup        ? resolveAttachmentPathById({            stateDir: serverConfig.stateDir,            attachmentId: normalizedRelativePath,          })        : resolveAttachmentRelativePath({            stateDir: serverConfig.stateDir,            relativePath: normalizedRelativePath,          });      if (!filePath) {        return HttpServerResponse.text(isIdLookup ? "Not Found" : "Invalid attachment path", {          status: isIdLookup ? 404 : 400,        });      }       const fileInfo = yield* fileSystem        .stat(filePath)        .pipe(Effect.catch(() => Effect.succeed(null)));      if (!fileInfo || fileInfo.type !== "File") {        return HttpServerResponse.text("Not Found", { status: 404 });      }       const contentType = Mime.getType(filePath) ?? "application/octet-stream";      const data = yield* fileSystem        .readFile(filePath)        .pipe(Effect.catch(() => Effect.succeed(null)));      if (!data) {        return HttpServerResponse.text("Internal Server Error", { status: 500 });      }      return HttpServerResponse.uint8Array(data, {        contentType,        headers: { "Cache-Control": "public, max-age=31536000, immutable" },      });    }     if (devUrl) {      return HttpServerResponse.redirect(devUrl.href, { status: 302 });    }     if (!staticDir) {      return HttpServerResponse.text("No static directory configured and no dev URL set.", {        status: 503,      });    }     const staticRoot = path.resolve(staticDir);    const staticRequestPath = url.pathname === "/" ? "/index.html" : url.pathname;    const rawStaticRelativePath = staticRequestPath.replace(/^[/\\]+/, "");    const hasRawLeadingParentSegment = rawStaticRelativePath.startsWith("..");    const staticRelativePath = path.normalize(rawStaticRelativePath).replace(/^[/\\]+/, "");    const hasPathTraversalSegment = staticRelativePath.startsWith("..");    if (      staticRelativePath.length === 0 ||      hasRawLeadingParentSegment ||      hasPathTraversalSegment ||      staticRelativePath.includes("\0")    ) {      return HttpServerResponse.text("Invalid static file path", { status: 400 });    }     const isWithinStaticRoot = (candidate: string) =>      candidate === staticRoot ||      candidate.startsWith(staticRoot.endsWith(path.sep) ? staticRoot : `${staticRoot}${path.sep}`);     let filePath = path.resolve(staticRoot, staticRelativePath);    if (!isWithinStaticRoot(filePath)) {      return HttpServerResponse.text("Invalid static file path", { status: 400 });    }     const ext = path.extname(filePath);    if (!ext) {      filePath = path.resolve(filePath, "index.html");      if (!isWithinStaticRoot(filePath)) {        return HttpServerResponse.text("Invalid static file path", { status: 400 });      }    }     const fileInfo = yield* fileSystem      .stat(filePath)      .pipe(Effect.catch(() => Effect.succeed(null)));    if (!fileInfo || fileInfo.type !== "File") {      const indexPath = path.resolve(staticRoot, "index.html");      const indexData = yield* fileSystem        .readFile(indexPath)        .pipe(Effect.catch(() => Effect.succeed(null)));      if (!indexData) {        return HttpServerResponse.text("Not Found", { status: 404 });      }      return HttpServerResponse.uint8Array(indexData, {        contentType: "text/html; charset=utf-8",      });    }     const contentType = Mime.getType(filePath) ?? "application/octet-stream";    const data = yield* fileSystem      .readFile(filePath)      .pipe(Effect.catch(() => Effect.succeed(null)));    if (!data) {      return HttpServerResponse.text("Internal Server Error", { status: 500 });    }    return HttpServerResponse.uint8Array(data, { contentType });  });   const httpServer = http.createServer();   const listenOptions = host ? { host, port } : { port };   const orchestrationEngine = yield* OrchestrationEngineService;  const projectionReadModelQuery = yield* ProjectionSnapshotQuery;  const checkpointDiffQuery = yield* CheckpointDiffQuery;

Lines 1–500 of 905.