diff --git a/packages/adapter-utils/src/server-utils.ts b/packages/adapter-utils/src/server-utils.ts index 9de5b7cc..1c8b76bd 100644 --- a/packages/adapter-utils/src/server-utils.ts +++ b/packages/adapter-utils/src/server-utils.ts @@ -91,11 +91,21 @@ export function redactEnvForLogs(env: Record): Record { + const resolveHostForUrl = (rawHost: string): string => { + const host = rawHost.trim(); + if (!host || host === "0.0.0.0" || host === "::") return "localhost"; + if (host.includes(":") && !host.startsWith("[") && !host.endsWith("]")) return `[${host}]`; + return host; + }; const vars: Record = { PAPERCLIP_AGENT_ID: agent.id, PAPERCLIP_COMPANY_ID: agent.companyId, }; - const apiUrl = process.env.PAPERCLIP_API_URL ?? `http://localhost:${process.env.PORT ?? 3100}`; + const runtimeHost = resolveHostForUrl( + process.env.PAPERCLIP_LISTEN_HOST ?? process.env.HOST ?? "localhost", + ); + const runtimePort = process.env.PAPERCLIP_LISTEN_PORT ?? process.env.PORT ?? "3100"; + const apiUrl = process.env.PAPERCLIP_API_URL ?? `http://${runtimeHost}:${runtimePort}`; vars.PAPERCLIP_API_URL = apiUrl; return vars; } diff --git a/server/src/__tests__/paperclip-env.test.ts b/server/src/__tests__/paperclip-env.test.ts new file mode 100644 index 00000000..1a65d9e9 --- /dev/null +++ b/server/src/__tests__/paperclip-env.test.ts @@ -0,0 +1,58 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { buildPaperclipEnv } from "../adapters/utils.js"; + +const ORIGINAL_PAPERCLIP_API_URL = process.env.PAPERCLIP_API_URL; +const ORIGINAL_PAPERCLIP_LISTEN_HOST = process.env.PAPERCLIP_LISTEN_HOST; +const ORIGINAL_PAPERCLIP_LISTEN_PORT = process.env.PAPERCLIP_LISTEN_PORT; +const ORIGINAL_HOST = process.env.HOST; +const ORIGINAL_PORT = process.env.PORT; + +afterEach(() => { + if (ORIGINAL_PAPERCLIP_API_URL === undefined) delete process.env.PAPERCLIP_API_URL; + else process.env.PAPERCLIP_API_URL = ORIGINAL_PAPERCLIP_API_URL; + + if (ORIGINAL_PAPERCLIP_LISTEN_HOST === undefined) delete process.env.PAPERCLIP_LISTEN_HOST; + else process.env.PAPERCLIP_LISTEN_HOST = ORIGINAL_PAPERCLIP_LISTEN_HOST; + + if (ORIGINAL_PAPERCLIP_LISTEN_PORT === undefined) delete process.env.PAPERCLIP_LISTEN_PORT; + else process.env.PAPERCLIP_LISTEN_PORT = ORIGINAL_PAPERCLIP_LISTEN_PORT; + + if (ORIGINAL_HOST === undefined) delete process.env.HOST; + else process.env.HOST = ORIGINAL_HOST; + + if (ORIGINAL_PORT === undefined) delete process.env.PORT; + else process.env.PORT = ORIGINAL_PORT; +}); + +describe("buildPaperclipEnv", () => { + it("prefers an explicit PAPERCLIP_API_URL", () => { + process.env.PAPERCLIP_API_URL = "http://localhost:4100"; + process.env.PAPERCLIP_LISTEN_HOST = "127.0.0.1"; + process.env.PAPERCLIP_LISTEN_PORT = "3101"; + + const env = buildPaperclipEnv({ id: "agent-1", companyId: "company-1" }); + + expect(env.PAPERCLIP_API_URL).toBe("http://localhost:4100"); + }); + + it("uses runtime listen host/port when explicit URL is not set", () => { + delete process.env.PAPERCLIP_API_URL; + process.env.PAPERCLIP_LISTEN_HOST = "0.0.0.0"; + process.env.PAPERCLIP_LISTEN_PORT = "3101"; + process.env.PORT = "3100"; + + const env = buildPaperclipEnv({ id: "agent-1", companyId: "company-1" }); + + expect(env.PAPERCLIP_API_URL).toBe("http://localhost:3101"); + }); + + it("formats IPv6 hosts safely in fallback URL generation", () => { + delete process.env.PAPERCLIP_API_URL; + process.env.PAPERCLIP_LISTEN_HOST = "::1"; + process.env.PAPERCLIP_LISTEN_PORT = "3101"; + + const env = buildPaperclipEnv({ id: "agent-1", companyId: "company-1" }); + + expect(env.PAPERCLIP_API_URL).toBe("http://[::1]:3101"); + }); +}); diff --git a/server/src/index.ts b/server/src/index.ts index 2eef0c31..5cf414da 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -434,6 +434,15 @@ if (listenPort !== config.port) { logger.warn(`Requested port is busy; using next free port (requestedPort=${config.port}, selectedPort=${listenPort})`); } +const runtimeListenHost = config.host; +const runtimeApiHost = + runtimeListenHost === "0.0.0.0" || runtimeListenHost === "::" + ? "localhost" + : runtimeListenHost; +process.env.PAPERCLIP_LISTEN_HOST = runtimeListenHost; +process.env.PAPERCLIP_LISTEN_PORT = String(listenPort); +process.env.PAPERCLIP_API_URL = `http://${runtimeApiHost}:${listenPort}`; + setupLiveEventsWebSocketServer(server, db as any, { deploymentMode: config.deploymentMode, resolveSessionFromHeaders,