From 55ec13f1539e5562e41288aa919558c651475b1c Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 6 May 2026 23:22:22 +0800 Subject: [PATCH] fix(cli): honor PASEO_PASSWORD env var (fix #776) (#782) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(cli): honor PASEO_PASSWORD env var (fix #776) The CLI only extracted passwords from `tcp://host?password=` URIs, so `PASEO_PASSWORD=xxxx paseo ls` connected without auth and was rejected by the daemon with "Password required". Fall back to the env var when the host (TCP or otherwise) carries no password. * docs: document PASEO_PASSWORD env var as a CLI auth source Pairs with the CLI fix in this branch — the "Connecting with a password" section now lists both the tcp URI query and the env-var fallback, with their precedence. Also clarify the dual role (daemon vs CLI) of PASEO_PASSWORD in the env var reference. --- packages/cli/src/utils/client.ts | 7 ++- .../cli/tests/28-client-ipc-targets.test.ts | 51 ++++++++++++++++--- public-docs/configuration.md | 21 ++++++-- 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/utils/client.ts b/packages/cli/src/utils/client.ts index 9cd7e89d5..7b48f4434 100644 --- a/packages/cli/src/utils/client.ts +++ b/packages/cli/src/utils/client.ts @@ -218,7 +218,12 @@ export function resolveDaemonTarget(host: string): DaemonTarget { export function resolveDaemonPassword(host: string): string | undefined { const trimmed = host.trim(); - return trimmed.startsWith("tcp://") ? parseConnectionUri(trimmed).password : undefined; + if (trimmed.startsWith("tcp://")) { + const fromUri = parseConnectionUri(trimmed).password; + if (fromUri) return fromUri; + } + const fromEnv = process.env.PASEO_PASSWORD; + return fromEnv && fromEnv.length > 0 ? fromEnv : undefined; } /** diff --git a/packages/cli/tests/28-client-ipc-targets.test.ts b/packages/cli/tests/28-client-ipc-targets.test.ts index 6607b69ca..7204935d7 100644 --- a/packages/cli/tests/28-client-ipc-targets.test.ts +++ b/packages/cli/tests/28-client-ipc-targets.test.ts @@ -143,14 +143,49 @@ console.log("=== CLI IPC Target Helpers ===\n"); } { - console.log("Test 10: daemon password resolution uses only the TCP URI query"); - assert.strictEqual( - resolveDaemonPassword("tcp://example.com:6767?ssl=true&password=query-secret"), - "query-secret", - ); - assert.strictEqual(resolveDaemonPassword("tcp://missing.example:6767"), undefined); - assert.strictEqual(resolveDaemonPassword("example.com:6767"), undefined); - console.log("✓ daemon password resolution uses only the TCP URI query\n"); + console.log("Test 10: daemon password resolution prefers TCP URI query, falls back to env"); + const previousEnv = process.env.PASEO_PASSWORD; + try { + delete process.env.PASEO_PASSWORD; + assert.strictEqual( + resolveDaemonPassword("tcp://example.com:6767?ssl=true&password=query-secret"), + "query-secret", + ); + assert.strictEqual(resolveDaemonPassword("tcp://missing.example:6767"), undefined); + assert.strictEqual(resolveDaemonPassword("example.com:6767"), undefined); + + process.env.PASEO_PASSWORD = "env-secret"; + assert.strictEqual( + resolveDaemonPassword("tcp://example.com:6767?ssl=true&password=query-secret"), + "query-secret", + "URI password should take precedence over env var", + ); + assert.strictEqual( + resolveDaemonPassword("tcp://missing.example:6767"), + "env-secret", + "TCP host without query password should fall back to env var", + ); + assert.strictEqual( + resolveDaemonPassword("example.com:6767"), + "env-secret", + "Bare host should pick up env var password", + ); + assert.strictEqual(resolveDaemonPassword("localhost:6767"), "env-secret"); + + process.env.PASEO_PASSWORD = ""; + assert.strictEqual( + resolveDaemonPassword("localhost:6767"), + undefined, + "Empty env var should be treated as unset", + ); + } finally { + if (previousEnv === undefined) { + delete process.env.PASEO_PASSWORD; + } else { + process.env.PASEO_PASSWORD = previousEnv; + } + } + console.log("✓ daemon password resolution prefers TCP URI query, falls back to env\n"); } console.log("=== All CLI IPC target tests passed ==="); diff --git a/public-docs/configuration.md b/public-docs/configuration.md index e242de7c2..162bb7fd6 100644 --- a/public-docs/configuration.md +++ b/public-docs/configuration.md @@ -123,18 +123,29 @@ After setting a password, restart the daemon for the change to take effect. ### Connecting with a password -The CLI reads the password from the TCP URI automatically: +The CLI picks up a password from, in order: -```bash -paseo --host "tcp://192.168.1.10:6767?password=my-secret" ls -``` +1. The `password` query parameter on a `tcp://` host URI: + + ```bash + paseo --host "tcp://192.168.1.10:6767?password=my-secret" ls + ``` + +2. The `PASEO_PASSWORD` environment variable, used as a fallback when the host carries no embedded password (works for `localhost:6767`, bare `host:port`, or `tcp://` hosts without a `password=` query): + + ```bash + PASEO_PASSWORD=my-secret paseo ls + PASEO_PASSWORD=my-secret paseo --host 192.168.1.10:6767 ls + ``` + +A `password=` in the URI always wins over the env var, so you can keep `PASEO_PASSWORD` set globally and still target a different daemon by spelling its password into the URI. In the mobile app, enter the password in the direct connection setup screen. ## Common env vars - `PASEO_HOME` — set Paseo home directory -- `PASEO_PASSWORD` — require a password to connect (plaintext, hashed at startup) +- `PASEO_PASSWORD` — on the daemon, the password to require (plaintext, hashed at startup); on the CLI, the password used to connect when the host URI doesn't include one - `PASEO_LISTEN` — override `daemon.listen` - `PASEO_HOSTNAMES` — override/extend `daemon.hostnames` - `PASEO_ALLOWED_HOSTS` — deprecated alias for `PASEO_HOSTNAMES`