import { Router, type Request } from "express"; import { resolve, sep } from "node:path"; import type { TaskStore } from "@fusion/core"; import type { ServerOptions } from "../server.js"; import { ApiError, internalError } from "../api-error.js"; import { getOrCreateProjectStore } from "../project-store-resolver.js"; import { createRuntimeLogger } from "../runtime-logger.js"; import type { RuntimeLogger } from "../runtime-logger.js"; import type { ApiRoutesContext, AuthSyncAuditLogInput, ProjectContext, RemoteRouteDiagnosticInput, RemoteRouteErrorClassification, ScopeValue, } from "./types.js"; function rethrowAsApiError(error: unknown, fallbackMessage = "Internal server error"): never { if (error instanceof ApiError) { throw error; } if (error instanceof Error) { throw internalError(error.message || fallbackMessage); } throw internalError(fallbackMessage); } export function classifyRemoteRouteError(error: unknown): RemoteRouteErrorClassification { const fallbackMessage = String(error); if (error instanceof Error) { const errorClass = error.constructor?.name || error.name || "Error"; const errorMessage = error.message || fallbackMessage; if (error.name === "AbortError") { return { classification: "timeout", errorClass, errorMessage }; } if (error instanceof TypeError) { return { classification: "transport", errorClass, errorMessage }; } return { classification: "unexpected", errorClass, errorMessage }; } if ((error as { name?: unknown } | null)?.name === "AbortError") { return { classification: "timeout", errorClass: "AbortError", errorMessage: fallbackMessage }; } return { classification: "unexpected", errorClass: typeof error, errorMessage: fallbackMessage, }; } export function getProjectIdFromRequest(req: Request): string | undefined { if (req.query && typeof req.query.projectId === "string" && req.query.projectId.length > 0) { return req.query.projectId; } if (req.body && typeof req.body.projectId === "string" && req.body.projectId.length > 0) { return req.body.projectId; } return undefined; } export async function getScopedStore(req: Request, store: TaskStore): Promise { const projectId = getProjectIdFromRequest(req); if (!projectId) return store; return getOrCreateProjectStore(projectId); } export async function getProjectContext( req: Request, store: TaskStore, options?: ServerOptions, ): Promise { const projectId = getProjectIdFromRequest(req); const engineManager = options?.engineManager; if (projectId && engineManager) { let engine = engineManager.getEngine(projectId); if (!engine) { try { engine = await engineManager.ensureEngine(projectId); } catch { // fall through } } if (engine) { return { store: engine.getTaskStore(), engine, projectId }; } } if (!projectId && options?.engine) { try { return { store: options.engine.getTaskStore(), engine: options.engine, projectId }; } catch { // Fall back to scoped store resolution. } } const scopedStore = await getScopedStore(req, store); return { store: scopedStore, engine: undefined, projectId }; } export function emitRemoteRouteDiagnostic( runtimeLogger: RuntimeLogger, input: RemoteRouteDiagnosticInput, ): void { const logger = runtimeLogger.child("remote-route").child(input.route); const level = input.level ?? "error"; const context: Record = { ...(input.nodeId !== undefined ? { nodeId: input.nodeId } : {}), ...(input.upstreamPath !== undefined ? { upstreamPath: input.upstreamPath } : {}), ...(input.stage !== undefined ? { stage: input.stage } : {}), ...(input.operationStage !== undefined ? { operationStage: input.operationStage } : {}), ...(input.context ?? {}), }; if (input.error !== undefined) { const classified = classifyRemoteRouteError(input.error); context.transportClassification = classified.classification; context.errorClass = classified.errorClass; context.errorMessage = classified.errorMessage; } if (level === "info") { logger.info(input.message, context); return; } if (level === "warn") { logger.warn(input.message, context); return; } logger.error(input.message, context); } export function createApiRoutesContext(store: TaskStore, options?: ServerOptions): ApiRoutesContext { const router = Router(); const runtimeLogger = options?.runtimeLogger?.child("routes") ?? createRuntimeLogger("routes"); const planningLogger = runtimeLogger.child("planning"); const chatLogger = runtimeLogger.child("chat"); function prioritizeProjectsForCurrentDirectory(projects: T[]): T[] { const cwd = resolve(process.cwd()); const rankProject = (projectPath: string): number => { const normalizedProjectPath = resolve(projectPath); if (normalizedProjectPath === cwd) { return Number.MAX_SAFE_INTEGER; } const prefix = normalizedProjectPath.endsWith(sep) ? normalizedProjectPath : `${normalizedProjectPath}${sep}`; if (!cwd.startsWith(prefix)) { return -1; } return normalizedProjectPath.length; }; return [...projects].sort((a, b) => rankProject(b.path) - rankProject(a.path)); } const resolveScopedStore = (req: Request): Promise => getScopedStore(req, store); const resolveProjectContext = (req: Request): Promise => getProjectContext(req, store, options); const disposeCallbacks: Array<() => void> = []; function emitAuthSyncAuditLog(input: AuthSyncAuditLogInput): void { const logger = runtimeLogger.child("settings-sync").child("auth"); const level = input.level ?? "info"; const providerNames = input.providerNames.filter((provider) => typeof provider === "string"); const context: Record = { operation: input.operation, direction: input.direction, route: input.route, providerNames, providerCount: providerNames.length, ...(input.sourceNodeId !== undefined ? { sourceNodeId: input.sourceNodeId } : {}), ...(input.targetNodeId !== undefined ? { targetNodeId: input.targetNodeId } : {}), }; if (level === "warn") { logger.warn("Auth sync diagnostic event", context); return; } if (level === "error") { logger.error("Auth sync diagnostic event", context); return; } logger.info("Auth sync diagnostic event", context); } function parseScopeParam(req: Request): ScopeValue | undefined { const rawScope = (typeof req.query.scope === "string" ? req.query.scope : undefined) ?? (req.body && typeof req.body.scope === "string" ? req.body.scope : undefined); if (rawScope === undefined || rawScope === "") { return undefined; } if (rawScope !== "global" && rawScope !== "project") { throw new ApiError(400, `Invalid scope value "${rawScope}". Must be "global" or "project".`); } return rawScope; } function resolveAutomationStore(req: Request, scope: ScopeValue | undefined): import("@fusion/core").AutomationStore { const projectId = getProjectIdFromRequest(req); const engineManager = options?.engineManager; if (scope === "global" || scope === undefined) { const defaultStore = options?.automationStore; if (!defaultStore) { throw new ApiError(503, "Automation store not available"); } return defaultStore; } if (projectId && engineManager) { const engine = engineManager.getEngine(projectId); if (engine) { const engineStore = engine.getAutomationStore(); if (engineStore) { return engineStore; } } } const defaultStore = options?.automationStore; if (!defaultStore) { throw new ApiError(503, "Automation store not available"); } return defaultStore; } function resolveRoutineStore(req: Request, scope: ScopeValue | undefined): import("@fusion/core").RoutineStore { const projectId = getProjectIdFromRequest(req); const engineManager = options?.engineManager; if (scope === "global" || scope === undefined) { const defaultStore = options?.routineStore; if (!defaultStore) { throw new ApiError(503, "Routine store not available"); } return defaultStore; } if (projectId && engineManager) { const engine = engineManager.getEngine(projectId); if (engine) { const engineStore = engine.getRoutineStore(); if (engineStore) { return engineStore; } } } const defaultStore = options?.routineStore; if (!defaultStore) { throw new ApiError(503, "Routine store not available"); } return defaultStore; } function resolveRoutineRunner(req: Request, scope: ScopeValue | undefined): NonNullable { const projectId = getProjectIdFromRequest(req); const engineManager = options?.engineManager; if (scope === "project" && projectId && engineManager) { const engine = engineManager.getEngine(projectId); if (engine) { const engineRunner = engine.getRoutineRunner(); if (engineRunner) { return { triggerManual: engineRunner.triggerManual.bind(engineRunner), triggerWebhook: engineRunner.triggerWebhook.bind(engineRunner), }; } } } const runner = options?.routineRunner; if (!runner) { throw new ApiError(503, "Routine execution not available"); } return runner; } return { router, store, options, runtimeLogger, planningLogger, chatLogger, prioritizeProjectsForCurrentDirectory, getProjectIdFromRequest, getScopedStore: resolveScopedStore, getProjectContext: resolveProjectContext, emitRemoteRouteDiagnostic: (input) => emitRemoteRouteDiagnostic(runtimeLogger, input), emitAuthSyncAuditLog, parseScopeParam, resolveAutomationStore, resolveRoutineStore, resolveRoutineRunner, registerDispose: (callback) => { disposeCallbacks.push(callback); }, dispose: () => { while (disposeCallbacks.length > 0) { const callback = disposeCallbacks.pop(); if (!callback) continue; try { callback(); } catch { // best-effort cleanup } } }, rethrowAsApiError, }; }