From 34185def784e36b7ba664417e8284ca324cddbb4 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 19 Oct 2026 08:52:09 +0200 Subject: [PATCH] fix(cli): preserve validated addresses in daemon command hints --- packages/cli/src/utils/client.ssh.test.ts | 2 +- packages/cli/src/utils/daemon-target.test.ts | 45 +++++++++++++++++++ packages/cli/src/utils/daemon-target.ts | 20 ++++++++- .../server/connected-hosts/hosts.e2e.test.ts | 1 + 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/utils/client.ssh.test.ts b/packages/cli/src/utils/client.ssh.test.ts index 6ae86a98e6..a4c08501f4 100644 --- a/packages/cli/src/utils/client.ssh.test.ts +++ b/packages/cli/src/utils/client.ssh.test.ts @@ -15,7 +15,7 @@ describe("CLI SSH transport", () => { expect(parseHostTarget("ssh://deploy@build-box:2222?daemonPort=7777")).toEqual({ kind: "ssh", ssh: { host: "deploy@build-box", sshPort: 2222, daemonPort: 7777 }, - description: "ssh://deploy@build-box:2222 (daemon port 7777)", + description: "ssh://build-box:2222 (daemon port 7777)", }); expect(() => parseHostTarget("ssh://deploy:secret@build-box")).toThrow( "Invalid host connection", diff --git a/packages/cli/src/utils/daemon-target.test.ts b/packages/cli/src/utils/daemon-target.test.ts index 77b297c493..966f23b071 100644 --- a/packages/cli/src/utils/daemon-target.test.ts +++ b/packages/cli/src/utils/daemon-target.test.ts @@ -1,3 +1,4 @@ +import { parseHostTarget } from "@getpaseo/protocol/host-target"; import { test, expect } from "vitest"; import { selectDaemonTarget, describeDaemonTarget } from "./daemon-target.js"; import { @@ -48,6 +49,19 @@ test("endpoint descriptions redact pairing material and credentials", () => { ).not.toContain("private"); }); +test("a pairing offer without a URL prefix still redacts its key", () => { + const offer = { + v: 2, + serverId: "srv_example", + daemonPublicKeyB64: "example-secret", + relay: { endpoint: "localhost:6767", useTls: false }, + }; + const host = `#offer=${Buffer.from(JSON.stringify(offer)).toString("base64url")}`; + expect(describeDaemonTarget({ kind: "endpoint", host })).toBe( + "relay://localhost:6767/srv_example", + ); +}); + test("malformed endpoint diagnostics never expose the supplied credential", () => { const target = { kind: "endpoint" as const, host: "alice:example-secret@localhost:6767" }; expect(describeDaemonTarget(target)).not.toContain("example-secret"); @@ -83,3 +97,34 @@ test("CLI selects an explicit password before a matching local credential and ne await rm(home, { recursive: true, force: true }); } }); + +test.each(["/tmp/paseo.sock", "\\\\.\\pipe\\paseo", "localhost:6767", "[::1]:6767", "6767"])( + "validated bare target %s keeps its spelling in command hints", + (host) => { + expect(describeDaemonTarget({ kind: "endpoint", host })).toBe(host); + }, +); + +test("validated URI command hints preserve their transport address without credentials", () => { + const ssh = describeDaemonTarget({ + kind: "endpoint", + host: "ssh://deploy@build-box:2222?daemonPort=7777&password=example-secret", + }); + expect(ssh).not.toContain("example-secret"); + expect(parseHostTarget(ssh)).toMatchObject({ + kind: "ssh", + ssh: { host: "deploy@build-box", sshPort: 2222, daemonPort: 7777 }, + }); + const ws = describeDaemonTarget({ + kind: "endpoint", + host: "wss://example.test/custom-ws?authorization=example-secret", + }); + expect(ws).not.toContain("example-secret"); + expect(new URL(ws).pathname).toBe("/custom-ws"); + expect( + describeDaemonTarget({ + kind: "endpoint", + host: "tcp://localhost:6767?daemonPort=example-secret&ssl=example-secret", + }), + ).not.toContain("example-secret"); +}); diff --git a/packages/cli/src/utils/daemon-target.ts b/packages/cli/src/utils/daemon-target.ts index 50b49353b6..0caba4e98f 100644 --- a/packages/cli/src/utils/daemon-target.ts +++ b/packages/cli/src/utils/daemon-target.ts @@ -40,7 +40,25 @@ export function selectDaemonTarget( export function describeDaemonTarget(target: DaemonTarget): string { if (target.kind === "instance") return `home ${target.home}`; try { - return parseHostTarget(target.host).description; + const input = target.host.trim(); + const parsed = parseHostTarget(input); + // Discovery descriptions are display text; command hints must preserve the address syntax. + if (parsed.kind === "ssh" || (!parsed.offer && !parsed.socketPath && input.includes("://"))) { + const url = new URL(input); + if (decodeURIComponent(url.username).includes(":")) url.username = "REDACTED"; + for (const key of new Set(url.searchParams.keys())) { + const routingOption = + (parsed.kind === "ssh" && key === "daemonPort") || + (key === "ssl" && + url.searchParams.getAll(key).every((value) => value === "true" || value === "false")); + if (!routingOption) url.searchParams.set(key, "REDACTED"); + } + return url.toString(); + } + // Pairing offers must always use safe display text, including prefix-free fragments. + return parsed.kind === "socket" && !parsed.offer && !input.includes("://") + ? input + : parsed.description; } catch { return ""; } diff --git a/packages/server/src/server/connected-hosts/hosts.e2e.test.ts b/packages/server/src/server/connected-hosts/hosts.e2e.test.ts index 5a29a195db..ef4189d2e6 100644 --- a/packages/server/src/server/connected-hosts/hosts.e2e.test.ts +++ b/packages/server/src/server/connected-hosts/hosts.e2e.test.ts @@ -445,6 +445,7 @@ test("CLI host commands manage the selected daemon and emit secret-free structur await promisify(execFile)( process.execPath, [ + "--conditions=source", "--import", "tsx", fileURLToPath(new URL("../../../../cli/src/index.ts", import.meta.url)),