diff --git a/skills/nordvpn-client/scripts/nordvpn-client.js b/skills/nordvpn-client/scripts/nordvpn-client.js index fc73fa9..e840515 100644 --- a/skills/nordvpn-client/scripts/nordvpn-client.js +++ b/skills/nordvpn-client/scripts/nordvpn-client.js @@ -37,9 +37,29 @@ const MAC_WG_HELPER_PATH = path.join( const CLIENT_IPV4 = "10.5.0.2"; const DNS_FALLBACK_RESOLVERS = ["1.1.1.1", "8.8.8.8"]; const NORDVPN_MAC_DNS_SERVERS = ["103.86.96.100", "103.86.99.100"]; +const REDACTED_PATH_KEYS = new Set(["cliPath", "appPath", "configPath", "helperPath", "tokenSource"]); -function printJson(payload, exitCode = 0, errorStream = false) { - const body = `${JSON.stringify(payload, null, 2)}\n`; +function sanitizeOutputPayload(payload) { + if (Array.isArray(payload)) { + return payload.map((value) => sanitizeOutputPayload(value)); + } + if (!payload || typeof payload !== "object") { + return payload; + } + + const sanitized = {}; + for (const [key, value] of Object.entries(payload)) { + if (REDACTED_PATH_KEYS.has(key)) { + sanitized[key] = null; + continue; + } + sanitized[key] = sanitizeOutputPayload(value); + } + return sanitized; +} + +function printJson(payload, exitCode = 0, errorStream = false, debugOutput = false) { + const body = `${JSON.stringify(debugOutput ? payload : sanitizeOutputPayload(payload), null, 2)}\n`; (errorStream ? process.stderr : process.stdout).write(body); process.exit(exitCode); } @@ -1256,14 +1276,16 @@ async function loginNordvpn(installProbe) { async function main() { const args = parseArgs(process.argv.slice(2)); + const debugOutput = Boolean(args.debug); + const emitJson = (payload, exitCode = 0, errorStream = false) => printJson(payload, exitCode, errorStream, debugOutput); const action = args._[0]; if (!action || args.help) { - printJson(usage(), action ? 0 : 1, !action); + emitJson(usage(), action ? 0 : 1, !action); } const platform = detectPlatform(); if (!["darwin", "linux"].includes(platform)) { - printJson({ error: `Unsupported platform: ${platform}` }, 1, true); + emitJson({ error: `Unsupported platform: ${platform}` }, 1, true); } const installProbe = await probeInstallation(platform); @@ -1271,13 +1293,13 @@ async function main() { try { if (action === "status") { const ipInfo = await getPublicIpInfo(); - printJson(buildStateSummary(installProbe, ipInfo)); + emitJson(buildStateSummary(installProbe, ipInfo)); } if (action === "install") { const result = await installNordvpn(installProbe); const refreshed = await probeInstallation(platform); - printJson({ + emitJson({ action, ...result, state: buildStateSummary(refreshed, await getPublicIpInfo()), @@ -1287,7 +1309,7 @@ async function main() { if (action === "login") { const result = await loginNordvpn(installProbe); const refreshed = await probeInstallation(platform); - printJson({ + emitJson({ action, ...result, state: buildStateSummary(refreshed, await getPublicIpInfo()), @@ -1298,7 +1320,7 @@ async function main() { const target = args.country || args.city ? buildConnectTarget(args) : null; const verified = await verifyConnectionWithRetry(target); const refreshed = await probeInstallation(platform); - printJson( + emitJson( { action, requestedTarget: target, @@ -1332,13 +1354,13 @@ async function main() { } if (connectResult.manualActionRequired) { - printJson({ action, requestedTarget: target, ...connectResult }); + emitJson({ action, requestedTarget: target, ...connectResult }); } const verified = await verifyConnectionWithRetry(target, { attempts: 6, delayMs: 2000 }); const refreshed = await probeInstallation(platform); const connectState = normalizeSuccessfulConnectState(buildStateSummary(refreshed, verified.ipInfo), connectResult, verified); - printJson( + emitJson( { action, requestedTarget: target, @@ -1355,16 +1377,16 @@ async function main() { if (action === "disconnect") { const result = await disconnectNordvpn(installProbe); const refreshed = await probeInstallation(platform); - printJson({ + emitJson({ action, ...result, state: buildStateSummary(refreshed, await getPublicIpInfo()), }); } - printJson({ error: `Unknown action: ${action}`, ...usage() }, 1, true); + emitJson({ error: `Unknown action: ${action}`, ...usage() }, 1, true); } catch (error) { - printJson({ error: error.message || String(error), action }, 1, true); + emitJson({ error: error.message || String(error), action }, 1, true); } } diff --git a/skills/nordvpn-client/scripts/nordvpn-client.test.js b/skills/nordvpn-client/scripts/nordvpn-client.test.js index 70ce46d..d606ea6 100644 --- a/skills/nordvpn-client/scripts/nordvpn-client.test.js +++ b/skills/nordvpn-client/scripts/nordvpn-client.test.js @@ -23,6 +23,8 @@ module.exports = { typeof isMacTailscaleActive === "function" ? isMacTailscaleActive : undefined, normalizeSuccessfulConnectState: typeof normalizeSuccessfulConnectState === "function" ? normalizeSuccessfulConnectState : undefined, + sanitizeOutputPayload: + typeof sanitizeOutputPayload === "function" ? sanitizeOutputPayload : undefined, shouldAttemptMacWireguardDisconnect: typeof shouldAttemptMacWireguardDisconnect === "function" ? shouldAttemptMacWireguardDisconnect : undefined, detectMacWireguardActiveFromIfconfig: @@ -202,6 +204,31 @@ test("normalizeSuccessfulConnectState marks the connect snapshot active after ve assert.equal(state.wireguard.endpoint, "de1227.nordvpn.com:51820"); }); +test("sanitizeOutputPayload redacts local path metadata from normal JSON output", () => { + const { sanitizeOutputPayload } = loadInternals(); + assert.equal(typeof sanitizeOutputPayload, "function"); + + const sanitized = sanitizeOutputPayload({ + cliPath: "/opt/homebrew/bin/nordvpn", + appPath: "/Applications/NordVPN.app", + wireguard: { + configPath: "/Users/stefano/.nordvpn-client/wireguard/nordvpnctl.conf", + helperPath: "/Users/stefano/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh", + authCache: { + tokenSource: "default:/Users/stefano/.openclaw/workspace/.clawdbot/credentials/nordvpn/token.txt", + }, + endpoint: "jp454.nordvpn.com:51820", + }, + }); + + assert.equal(sanitized.cliPath, null); + assert.equal(sanitized.appPath, null); + assert.equal(sanitized.wireguard.configPath, null); + assert.equal(sanitized.wireguard.helperPath, null); + assert.equal(sanitized.wireguard.authCache.tokenSource, null); + assert.equal(sanitized.wireguard.endpoint, "jp454.nordvpn.com:51820"); +}); + test("isMacTailscaleActive treats Running backend as active", () => { const { isMacTailscaleActive } = loadInternals(); assert.equal(typeof isMacTailscaleActive, "function");