import { describe, expect, it, vi } from "vitest"; import { createWizardPrompter as buildWizardPrompter } from "../../test/helpers/wizard-prompter.js"; import { DEFAULT_DANGEROUS_NODE_COMMANDS } from "../gateway/node-command-policy.js"; import type { RuntimeEnv } from "../runtime.js"; import type { WizardPrompter, WizardSelectParams } from "./prompts.js"; const mocks = vi.hoisted(() => ({ randomToken: vi.fn(), getTailnetHostname: vi.fn(), })); vi.mock("../commands/onboard-helpers.js", async (importActual) => { const actual = await importActual(); return { ...actual, randomToken: mocks.randomToken, }; }); vi.mock("../infra/tailscale.js", () => ({ findTailscaleBinary: vi.fn(async () => undefined), getTailnetHostname: mocks.getTailnetHostname, })); import { configureGatewayForSetup } from "./setup.gateway-config.js"; describe("configureGatewayForSetup", () => { function createPrompter(params: { selectQueue: string[]; textQueue: Array }) { const selectQueue = [...params.selectQueue]; const textQueue = [...params.textQueue]; const select = vi.fn(async (params: WizardSelectParams) => { const next = selectQueue.shift(); if (next !== undefined) { return next; } return params.initialValue ?? params.options[0]?.value; }) as unknown as WizardPrompter["select"]; return buildWizardPrompter({ select, text: vi.fn(async () => textQueue.shift() as string), }); } function createRuntime(): RuntimeEnv { return { log: vi.fn(), error: vi.fn(), exit: vi.fn(), }; } function createQuickstartGateway(authMode: "token" | "password") { return { hasExisting: false, port: 18789, bind: "loopback" as const, authMode, tailscaleMode: "off" as const, token: undefined, password: undefined, customBindHost: undefined, tailscaleResetOnExit: false, }; } async function runGatewayConfig(params?: { flow?: "advanced" | "quickstart"; bindChoice?: string; authChoice?: "token" | "password"; tailscaleChoice?: "off" | "serve"; textQueue?: Array; nextConfig?: Record; }) { const authChoice = params?.authChoice ?? "token"; const prompter = createPrompter({ selectQueue: [params?.bindChoice ?? "loopback", authChoice, params?.tailscaleChoice ?? "off"], textQueue: params?.textQueue ?? ["18789", undefined], }); const runtime = createRuntime(); return configureGatewayForSetup({ flow: params?.flow ?? "advanced", baseConfig: {}, nextConfig: params?.nextConfig ?? {}, localPort: 18789, quickstartGateway: createQuickstartGateway(authChoice), prompter, runtime, }); } it("generates a token when the prompt returns undefined", async () => { mocks.randomToken.mockReturnValue("generated-token"); const result = await runGatewayConfig(); expect(result.settings.gatewayToken).toBe("generated-token"); expect(result.nextConfig.gateway?.nodes?.denyCommands).toEqual(DEFAULT_DANGEROUS_NODE_COMMANDS); }); it("prefers OPENCLAW_GATEWAY_TOKEN during quickstart token setup", async () => { const prevToken = process.env.OPENCLAW_GATEWAY_TOKEN; process.env.OPENCLAW_GATEWAY_TOKEN = "token-from-env"; mocks.randomToken.mockReturnValue("generated-token"); mocks.randomToken.mockClear(); try { const result = await runGatewayConfig({ flow: "quickstart", textQueue: [], }); expect(result.settings.gatewayToken).toBe("token-from-env"); } finally { if (prevToken === undefined) { delete process.env.OPENCLAW_GATEWAY_TOKEN; } else { process.env.OPENCLAW_GATEWAY_TOKEN = prevToken; } } }); it("enables insecure local control ui auth for fresh quickstart loopback setups", async () => { mocks.randomToken.mockReturnValue("generated-token"); const result = await runGatewayConfig({ flow: "quickstart", textQueue: [], }); expect(result.nextConfig.gateway?.controlUi?.allowInsecureAuth).toBe(true); }); it("preserves explicit control ui auth policy in quickstart", async () => { mocks.randomToken.mockReturnValue("generated-token"); const result = await runGatewayConfig({ flow: "quickstart", textQueue: [], nextConfig: { gateway: { controlUi: { allowInsecureAuth: false, }, }, }, }); expect(result.nextConfig.gateway?.controlUi?.allowInsecureAuth).toBe(false); }); it("enables insecure local control ui auth when quickstart reuses an existing loopback config", async () => { mocks.randomToken.mockReturnValue("generated-token"); const prompter = createPrompter({ selectQueue: [], textQueue: [], }); const runtime = createRuntime(); const result = await configureGatewayForSetup({ flow: "quickstart", baseConfig: {}, nextConfig: { gateway: { port: 18789, bind: "loopback", }, }, localPort: 18789, quickstartGateway: { ...createQuickstartGateway("token"), hasExisting: true, }, prompter, runtime, }); expect(result.nextConfig.gateway?.controlUi?.allowInsecureAuth).toBe(true); }); it("does not set password to literal 'undefined' when prompt returns undefined", async () => { mocks.randomToken.mockReturnValue("unused"); const result = await runGatewayConfig({ authChoice: "password", }); const authConfig = result.nextConfig.gateway?.auth as { mode?: string; password?: string }; expect(authConfig?.mode).toBe("password"); expect(authConfig?.password).toBe(""); expect(authConfig?.password).not.toBe("undefined"); }); it("seeds control UI allowed origins for non-loopback binds", async () => { mocks.randomToken.mockReturnValue("generated-token"); const result = await runGatewayConfig({ bindChoice: "lan", }); expect(result.nextConfig.gateway?.controlUi?.allowedOrigins).toEqual([ "http://localhost:18789", "http://127.0.0.1:18789", ]); }); it("honors secretInputMode=ref for gateway password prompts", async () => { const previous = process.env.OPENCLAW_GATEWAY_PASSWORD; process.env.OPENCLAW_GATEWAY_PASSWORD = "gateway-secret"; // pragma: allowlist secret try { const prompter = createPrompter({ selectQueue: ["loopback", "password", "off", "env"], textQueue: ["18789", "OPENCLAW_GATEWAY_PASSWORD"], }); const runtime = createRuntime(); const result = await configureGatewayForSetup({ flow: "advanced", baseConfig: {}, nextConfig: {}, localPort: 18789, quickstartGateway: createQuickstartGateway("password"), secretInputMode: "ref", // pragma: allowlist secret prompter, runtime, }); expect(result.nextConfig.gateway?.auth?.mode).toBe("password"); expect(result.nextConfig.gateway?.auth?.password).toEqual({ source: "env", provider: "default", id: "OPENCLAW_GATEWAY_PASSWORD", }); } finally { if (previous === undefined) { delete process.env.OPENCLAW_GATEWAY_PASSWORD; } else { process.env.OPENCLAW_GATEWAY_PASSWORD = previous; } } }); it("stores gateway token as SecretRef when secretInputMode=ref", async () => { const previous = process.env.OPENCLAW_GATEWAY_TOKEN; process.env.OPENCLAW_GATEWAY_TOKEN = "token-from-env"; try { const prompter = createPrompter({ selectQueue: ["loopback", "token", "off", "env"], textQueue: ["18789", "OPENCLAW_GATEWAY_TOKEN"], }); const runtime = createRuntime(); const result = await configureGatewayForSetup({ flow: "advanced", baseConfig: {}, nextConfig: {}, localPort: 18789, quickstartGateway: createQuickstartGateway("token"), secretInputMode: "ref", // pragma: allowlist secret prompter, runtime, }); expect(result.nextConfig.gateway?.auth?.mode).toBe("token"); expect(result.nextConfig.gateway?.auth?.token).toEqual({ source: "env", provider: "default", id: "OPENCLAW_GATEWAY_TOKEN", }); expect(result.settings.gatewayToken).toBe("token-from-env"); } finally { if (previous === undefined) { delete process.env.OPENCLAW_GATEWAY_TOKEN; } else { process.env.OPENCLAW_GATEWAY_TOKEN = previous; } } }); it("resolves quickstart exec SecretRefs for gateway token bootstrap", async () => { const quickstartGateway = { ...createQuickstartGateway("token"), token: { source: "exec" as const, provider: "gatewayTokens", id: "gateway/auth/token", }, }; const runtime = createRuntime(); const prompter = createPrompter({ selectQueue: [], textQueue: [], }); const result = await configureGatewayForSetup({ flow: "quickstart", baseConfig: {}, nextConfig: { secrets: { providers: { gatewayTokens: { source: "exec", command: process.execPath, allowInsecurePath: true, allowSymlinkCommand: true, args: [ "-e", "let input='';process.stdin.setEncoding('utf8');process.stdin.on('data',d=>input+=d);process.stdin.on('end',()=>{const req=JSON.parse(input||'{}');const values={};for(const id of req.ids||[]){values[id]='token-from-exec';}process.stdout.write(JSON.stringify({protocolVersion:1,values}));});", ], }, }, }, }, localPort: 18789, quickstartGateway, prompter, runtime, }); expect(result.nextConfig.gateway?.auth?.token).toEqual(quickstartGateway.token); expect(result.settings.gatewayToken).toBe("token-from-exec"); }); });