@miraj181/ipingyou 2.1.15 → 2.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,7 +20,7 @@ No firewalls to configure. No port forwarding. No plaintext leakage.
20
20
  * 📺 **Terminal Mirroring**: Wrap client SSH sessions in a multiplexed `tmux` terminal. The Host can spectate connected clients in real-time right from the dashboard to audit or assist.
21
21
  * 🔄 **Reverse Port Forwarding (`ssh -R`)**: Clients can expose their *local* `localhost` development ports back to the Host through the secure tunnel.
22
22
  * 📡 **Hardware Telemetry Verification**: Clients silently generate hardware footprint reports (OS, RAM, CPU, IP), encrypt them locally with the session password, and send them to the Host for authorization.
23
- * 🚨 **Panic Kill-Switch**: Type `ipingyou panic` to instantly vaporize all associated keys, wipe all alias configs, and send a `SIGKILL` to every active tunnel and SSH shell.
23
+ * 🚨 **Scoped Emergency Stop**: Type `ipingyou panic` and confirm locally to stop only processes and temporary credentials owned by the current iPingYou session.
24
24
  * 👻 **Daemonization**: Run `ipingyou service install` to quietly install and run the Host listener in the background (survives system reboots using PM2).
25
25
  * 🧭 **Approval Gate**: Require the Host to explicitly approve clients before they receive tunnel/key material.
26
26
  * 📦 **One-Time File Share**: Serve a single file/folder over SCP and revoke after use.
@@ -48,12 +48,17 @@ npx @miraj181/ipingyou connect
48
48
 
49
49
  ### Global Install
50
50
  ```bash
51
- npm install -g @miraj181/ipingyou
51
+ # Install Socket Firewall once, then scan iPingYou before installation
52
+ npm install -g sfw
53
+ sfw npm install -g @miraj181/ipingyou
52
54
 
53
55
  # Execute globally using aliases:
54
56
  ipingyou
55
57
  # or
56
58
  securelink
59
+
60
+ # Verify future in-app npm installs are firewall-protected
61
+ ipingyou security-status
57
62
  ```
58
63
 
59
64
  ---
@@ -102,7 +107,7 @@ sequenceDiagram
102
107
 
103
108
  ## 🛡️ Security Scanner Disclaimer
104
109
 
105
- Because **iPingYou** is a powerful remote administration tool with features like background daemonization (via PM2), secure shell execution (`execa`), and anti-forensics capabilities (`panic` mode), automated security scanners (such as Socket.dev or enterprise EDRs) may flag this package as a **potential risk** or **malware-like**.
110
+ Because **iPingYou** is a remote-access tool with background daemonization and SSH process management, automated security scanners may classify it as security-sensitive. Native dependencies are never downloaded or installed automatically, and emergency cleanup is limited to resources registered by the current process.
106
111
 
107
112
  These alerts (e.g., "AI-detected potential code anomaly", "Shell access", "Network access") are **expected behavior** for a peer-to-peer tunneling utility. The source code is entirely open-source, heavily documented, and uses zero-knowledge encryption to ensure your data is safe.
108
113
 
@@ -128,7 +133,7 @@ These alerts (e.g., "AI-detected potential code anomaly", "Shell access", "Netwo
128
133
  | `ipingyou connect -u <UID>` | Connect directly to a specific UID. |
129
134
  | `ipingyou ai` | Groq-powered task assistant with guarded local/remote tools. |
130
135
  | `ipingyou doctor` | Diagnostics for dependencies, SSH, broker, SCP, AI, and tests. |
131
- | `ipingyou panic` | 🚨 Self-destruct mode. Wipes configs, memory, and kills all processes. |
136
+ | `ipingyou panic` | 🚨 Confirmed emergency stop for the current iPingYou session only. |
132
137
  | `ipingyou service install` | 👻 Installs Host mode as an always-on background daemon. |
133
138
  | `ipingyou service stop` | Stops and removes the background daemon. |
134
139
  | `ipingyou service status` | Shows background daemon status. |
package/SECURITY.md ADDED
@@ -0,0 +1,21 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ Use this section to tell people about which versions of your project are
6
+ currently being supported with security updates.
7
+
8
+ | Version | Supported |
9
+ | ------- | ------------------ |
10
+ | 5.1.x | :white_check_mark: |
11
+ | 5.0.x | :x: |
12
+ | 4.0.x | :white_check_mark: |
13
+ | < 4.0 | :x: |
14
+
15
+ ## Reporting a Vulnerability
16
+
17
+ Use this section to tell people how to report a vulnerability.
18
+
19
+ Tell them where to go, how often they can expect to get an update on a
20
+ reported vulnerability, what to expect if the vulnerability is accepted or
21
+ declined, etc.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraj181/ipingyou",
3
- "version": "2.1.15",
3
+ "version": "2.1.18",
4
4
  "description": "SecureLink-CLI — Secure peer-to-peer remote access via SSH & Cloudflare Tunnels",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
@@ -13,12 +13,13 @@
13
13
  "src/lib/",
14
14
  "src/modes/",
15
15
  "README.md",
16
+ "SECURITY.md",
16
17
  "LICENSE"
17
18
  ],
18
19
  "scripts": {
19
20
  "start": "node src/server.js",
20
21
  "dev:cli": "node src/cli.js",
21
- "test": "node test/test_ai_helpers.js",
22
+ "test": "node test/test_ai_helpers.js && node test/test_security_static.js && node test/test_e2e_crypto.js",
22
23
  "prepublishOnly": "node -e \"require('fs').accessSync('LICENSE')\" && node src/cli.js --help"
23
24
  },
24
25
  "keywords": [
@@ -44,25 +45,21 @@
44
45
  "url": "https://github.com/skmirajulislam/ipingyou/issues"
45
46
  },
46
47
  "dependencies": {
47
- "chalk": "^5.3.0",
48
- "commander": "^15.0.0",
49
- "express": "^4.21.2",
50
- "express-rate-limit": "^7.5.0",
51
- "execa": "^9.5.2",
52
- "helmet": "^8.0.0",
53
- "inquirer": "^12.11.1",
54
- "nanoid": "^5.0.9",
55
- "open": "^11.0.0",
56
- "ora": "^9.4.0",
57
- "shell-quote": "^1.8.4",
58
- "tree-kill": "^1.2.2",
59
- "ws": "^8.20.1"
48
+ "chalk": "5.6.2",
49
+ "commander": "15.0.0",
50
+ "express": "4.22.2",
51
+ "express-rate-limit": "7.5.1",
52
+ "execa": "9.6.1",
53
+ "helmet": "8.2.0",
54
+ "inquirer": "12.11.1",
55
+ "ora": "9.4.1",
56
+ "ws": "8.21.0"
60
57
  },
61
58
  "devDependencies": {
62
59
  "nodemon": "^3.1.4"
63
60
  },
64
61
  "engines": {
65
- "node": ">=18.0.0"
62
+ "node": ">=22.12.0"
66
63
  },
67
64
  "type": "module"
68
65
  }
package/src/cli.js CHANGED
@@ -29,6 +29,7 @@ import { fileURLToPath } from 'node:url';
29
29
  import { detectOS, checkDependencies } from './lib/platform.js';
30
30
  import { cleanupAll, installShutdownHandlers, executePanicMode } from './lib/cleanup.js';
31
31
  import { cleanupSessionLog } from './lib/session-log.js';
32
+ import { getSocketFirewallStatus, runProtectedNpmInstall } from './lib/socket-firewall.js';
32
33
  import { startHostMode } from './modes/host.js';
33
34
  import { startClientMode } from './modes/client.js';
34
35
  import { startAIMode } from './modes/ai.js';
@@ -330,10 +331,19 @@ program
330
331
 
331
332
  program
332
333
  .command('panic')
333
- .description('🚨 Self-destruct mode: wipe all configs, kill tunnels, and remove traces')
334
+ .description('🚨 Stop resources owned by the current iPingYou session')
334
335
  .action(async () => {
335
336
  try {
336
337
  showBanner();
338
+ const { confirmation } = await inquirer.prompt([{
339
+ type: 'input',
340
+ name: 'confirmation',
341
+ message: 'Type STOP CURRENT SESSION to confirm:',
342
+ }]);
343
+ if (confirmation !== 'STOP CURRENT SESSION') {
344
+ console.log(chalk.yellow(' Emergency shutdown cancelled.'));
345
+ return;
346
+ }
337
347
  await executePanicMode();
338
348
  } catch (err) {
339
349
  fatal('panic', err);
@@ -352,8 +362,8 @@ program
352
362
  const { execa } = await import('execa');
353
363
 
354
364
  if (action === 'install') {
355
- console.log(chalk.dim(' Installing PM2 globally and starting host...'));
356
- await execa('npm', ['install', '-g', 'pm2'], { stdio: 'inherit' });
365
+ console.log(chalk.dim(' Scanning and installing PM2 through Socket Firewall...'));
366
+ await runProtectedNpmInstall(['-g', 'pm2'], { stdio: 'inherit' });
357
367
  await execa('pm2', ['start', 'ipingyou', '--name', 'ipingyou-host', '--', 'host'], { stdio: 'inherit' });
358
368
  await execa('pm2', ['save'], { stdio: 'inherit' });
359
369
  await execa('pm2', ['startup'], { stdio: 'inherit' });
@@ -373,6 +383,26 @@ program
373
383
  }
374
384
  });
375
385
 
386
+ program
387
+ .command('security-status')
388
+ .description('Check Socket Firewall protection for future npm installations')
389
+ .action(async () => {
390
+ try {
391
+ showBanner();
392
+ const status = await getSocketFirewallStatus();
393
+ if (!status.available) {
394
+ console.log(chalk.yellow(' ⚠️ Socket Firewall is not available.'));
395
+ console.log(chalk.dim(' Install it with: npm install -g sfw'));
396
+ process.exitCode = 1;
397
+ return;
398
+ }
399
+ console.log(chalk.green(` ✅ Socket Firewall active${status.version ? ` (${status.version})` : ''}`));
400
+ console.log(chalk.dim(' In-app npm installations are routed through: sfw npm install'));
401
+ } catch (err) {
402
+ fatal('security-status', err);
403
+ }
404
+ });
405
+
376
406
  program
377
407
  .command('allowlist')
378
408
  .description('Manage AI command allowlist — add, remove, or list safe regex patterns')
@@ -1,3 +1,5 @@
1
+ import { getAllowlistRegexes } from '../allowlist.js';
2
+
1
3
  const SECRET_PATTERNS = [
2
4
  /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
3
5
  /\b(gsk_[A-Za-z0-9_-]{20,})\b/g,
@@ -20,6 +22,31 @@ const BLOCKED_PATH_PATTERNS = [
20
22
  /id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/,
21
23
  ];
22
24
 
25
+ const BLOCKED_PATH_SEGMENTS = new Set([
26
+ '.ssh',
27
+ '.gnupg',
28
+ '.aws',
29
+ '.ipingyou',
30
+ '.kube',
31
+ '.docker',
32
+ ]);
33
+
34
+ const BLOCKED_PATH_BASENAMES = new Set([
35
+ '.npmrc',
36
+ '.netrc',
37
+ '.pypirc',
38
+ 'credentials',
39
+ 'credentials.json',
40
+ 'known_hosts',
41
+ 'authorized_keys',
42
+ 'shadow',
43
+ 'sudoers',
44
+ ]);
45
+
46
+ const PRIVATE_KEY_BASENAME = /^id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/i;
47
+ const ENV_BASENAME = /^\.env(?:\.|$)/i;
48
+ const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
49
+
23
50
  const BLOCKED_COMMAND_PATTERNS = [
24
51
  /^\s*(env|printenv|set)\b/,
25
52
  /\b(export|declare)\s+-p\b/,
@@ -41,8 +68,7 @@ const DANGEROUS_COMMAND_PATTERNS = [
41
68
  /\b(curl|wget)\b/,
42
69
  />|>>|\btee\b/,
43
70
  ];
44
-
45
- import { getAllowlistRegexes } from '../allowlist.js';
71
+ const SHELL_SYNTAX_PATTERN = /(?:[;&|`$()]|\\\r?\n)/;
46
72
 
47
73
  const READ_ONLY_COMMAND_PATTERNS = [
48
74
  /^\s*(pwd|ls|find|rg|grep|sed|cat|head|tail|wc|git status|git diff|git log|git show|node --version|npm --version|which|date|uname)\b/,
@@ -66,6 +92,33 @@ export function commandTouchesBlockedPath(command) {
66
92
  return BLOCKED_PATH_PATTERNS.some(pattern => pattern.test(command));
67
93
  }
68
94
 
95
+ export function assertSafeReadablePath(filePath) {
96
+ const value = String(filePath ?? '');
97
+ if (!value || value.length > 4096) {
98
+ throw new Error('Invalid file path');
99
+ }
100
+ if (CONTROL_CHARACTERS.test(value)) {
101
+ throw new Error('File path contains control characters');
102
+ }
103
+
104
+ const normalized = value.replace(/\\/g, '/').replace(/\/+/g, '/');
105
+ const segments = normalized.toLowerCase().split('/').filter(Boolean);
106
+ const basename = segments.at(-1) || '';
107
+
108
+ if (
109
+ segments.some(segment => BLOCKED_PATH_SEGMENTS.has(segment))
110
+ || BLOCKED_PATH_BASENAMES.has(basename)
111
+ || PRIVATE_KEY_BASENAME.test(basename)
112
+ || ENV_BASENAME.test(basename)
113
+ || normalized.toLowerCase().includes('/.config/gh/')
114
+ || normalized.toLowerCase().includes('/proc/') && basename === 'environ'
115
+ ) {
116
+ throw new Error('Path references a protected secret/config location');
117
+ }
118
+
119
+ return value;
120
+ }
121
+
69
122
  export function classifyCommand(command) {
70
123
  const text = String(command || '').trim();
71
124
  if (!text) return { blocked: true, needsApproval: true, reason: 'Empty command' };
@@ -78,16 +131,6 @@ export function classifyCommand(command) {
78
131
  };
79
132
  }
80
133
 
81
- // Check user-provided allowlist first (if present)
82
- try {
83
- const userPatterns = getAllowlistRegexes();
84
- if (Array.isArray(userPatterns) && userPatterns.some(p => p.test(text))) {
85
- return { blocked: false, needsApproval: false, reason: 'Matched user allowlist' };
86
- }
87
- } catch {
88
- // ignore allowlist errors and fall back to defaults
89
- }
90
-
91
134
  if (BLOCKED_COMMAND_PATTERNS.some(pattern => pattern.test(text))) {
92
135
  return {
93
136
  blocked: true,
@@ -100,6 +143,20 @@ export function classifyCommand(command) {
100
143
  return { blocked: false, needsApproval: true, reason: 'Command may modify files, install packages, or access the network' };
101
144
  }
102
145
 
146
+ if (SHELL_SYNTAX_PATTERN.test(text)) {
147
+ return { blocked: false, needsApproval: true, reason: 'Command uses shell syntax and requires explicit approval' };
148
+ }
149
+
150
+ // User allowlists may suppress prompts only after non-bypassable safety checks.
151
+ try {
152
+ const userPatterns = getAllowlistRegexes();
153
+ if (Array.isArray(userPatterns) && userPatterns.some(pattern => pattern.test(text))) {
154
+ return { blocked: false, needsApproval: false, reason: 'Matched user allowlist' };
155
+ }
156
+ } catch {
157
+ // Ignore allowlist errors and fall back to defaults.
158
+ }
159
+
103
160
  if (READ_ONLY_COMMAND_PATTERNS.some(pattern => pattern.test(text))) {
104
161
  return { blocked: false, needsApproval: false, reason: 'Read-only command' };
105
162
  }
package/src/lib/chat.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import http from 'node:http';
2
2
  import { WebSocketServer } from 'ws';
3
- import open from 'open';
3
+ import { openUrl } from './open-url.js';
4
4
  import chalk from 'chalk';
5
5
  import { secureSensitiveUrl } from './secure-print.js';
6
6
 
@@ -97,9 +97,17 @@ const HTML_CONTENT = `
97
97
  let username = isHost ? 'Host' : prompt('Enter your name for the chat:', 'Client_' + Math.floor(Math.random()*1000));
98
98
  if (!username) username = 'Anonymous';
99
99
 
100
+ function showBodyMessage(text, color) {
101
+ const message = document.createElement('h2');
102
+ message.style.cssText = 'text-align:center;margin-top:20vh';
103
+ if (color) message.style.color = color;
104
+ message.textContent = text;
105
+ document.body.replaceChildren(message);
106
+ }
107
+
100
108
  const sessionPassword = window.location.hash.substring(1);
101
109
  if (!sessionPassword) {
102
- document.body.innerHTML = '<h2 style="text-align:center; margin-top:20vh; color:red;">Fatal: Missing session password in URL hash. Cannot decrypt E2E chat.</h2>';
110
+ showBodyMessage('Fatal: Missing session password in URL hash. Cannot decrypt E2E chat.', 'red');
103
111
  throw new Error("Missing password");
104
112
  }
105
113
 
@@ -181,7 +189,7 @@ const HTML_CONTENT = `
181
189
  }
182
190
 
183
191
  function updateUsers(users) {
184
- usersList.innerHTML = '';
192
+ usersList.replaceChildren();
185
193
  users.forEach(u => {
186
194
  const li = document.createElement('li');
187
195
  li.className = 'user-item';
@@ -252,7 +260,7 @@ const HTML_CONTENT = `
252
260
  } else {
253
261
  ws.close();
254
262
  window.close();
255
- document.body.innerHTML = '<h2 style="text-align:center; margin-top:20vh;">You have left the chat. You can close this tab.</h2>';
263
+ showBodyMessage('You have left the chat. You can close this tab.');
256
264
  }
257
265
  };
258
266
  </script>
@@ -327,7 +335,7 @@ export async function startChatServer(onClose) {
327
335
  export async function openLocalChatUI(port, password) {
328
336
  try {
329
337
  const chatUrl = `http://localhost:${port}#${password}`;
330
- await open(chatUrl);
338
+ await openUrl(chatUrl);
331
339
  } catch {
332
340
  console.log(chalk.dim(` Unable to auto-open browser. Visit ${secureSensitiveUrl(`http://localhost:${port}`, password)}`));
333
341
  }
@@ -3,18 +3,13 @@
3
3
  * Graceful Cleanup & Process Killer
4
4
  * ============================================================
5
5
  * Tracks all spawned child processes (cloudflared, ssh, etc.)
6
- * and kills them on SIGINT/exit using tree-kill to ensure
6
+ * and kills them on SIGINT/exit to ensure
7
7
  * no orphan processes linger.
8
8
  * ============================================================
9
9
  */
10
10
 
11
- import treeKill from 'tree-kill';
12
11
  import chalk from 'chalk';
13
- import fs from 'node:fs';
14
- import os from 'node:os';
15
- import path from 'node:path';
16
12
  import { execa } from 'execa';
17
- import { TMUX_SESSION_NAME, TMUX_SESSION_PREFIX, tmuxSocketArgs } from './tmux.js';
18
13
 
19
14
  /** @type {Set<number>} — Active child PIDs we manage */
20
15
  const trackedPIDs = new Set();
@@ -74,15 +69,61 @@ export function setRevokeOnExit(uid, brokerUrl, getHostToken = null) {
74
69
  * @returns {Promise<void>}
75
70
  */
76
71
  export function killProcessTree(pid, signal = 'SIGTERM') {
77
- return new Promise((resolve) => {
78
- treeKill(pid, signal, (err) => {
79
- if (err) {
80
- treeKill(pid, 'SIGKILL', () => resolve());
81
- } else {
82
- resolve();
83
- }
72
+ return killProcessTreeSafely(pid, signal);
73
+ }
74
+
75
+ async function killProcessTreeSafely(pid, signal) {
76
+ const rootPid = Number.parseInt(pid, 10);
77
+ if (!Number.isSafeInteger(rootPid) || rootPid <= 0 || rootPid === process.pid) {
78
+ throw new Error('Invalid child process PID');
79
+ }
80
+
81
+ if (process.platform === 'win32') {
82
+ await execa('taskkill', ['/PID', String(rootPid), '/T', '/F'], {
83
+ reject: false,
84
+ timeout: 5000,
85
+ maxBuffer: 64 * 1024,
84
86
  });
85
- });
87
+ return;
88
+ }
89
+
90
+ const descendants = [];
91
+ const visited = new Set([rootPid]);
92
+ const pending = [rootPid];
93
+ while (pending.length > 0 && visited.size <= 1024) {
94
+ const parentPid = pending.pop();
95
+ const result = await execa('pgrep', ['-P', String(parentPid)], {
96
+ reject: false,
97
+ timeout: 2000,
98
+ maxBuffer: 64 * 1024,
99
+ }).catch(() => ({ stdout: '' }));
100
+ for (const value of String(result.stdout || '').split(/\s+/)) {
101
+ const childPid = Number.parseInt(value, 10);
102
+ if (!Number.isSafeInteger(childPid) || childPid <= 0 || visited.has(childPid)) continue;
103
+ visited.add(childPid);
104
+ descendants.push(childPid);
105
+ pending.push(childPid);
106
+ }
107
+ }
108
+
109
+ const targets = [...descendants.reverse(), rootPid];
110
+ for (const targetPid of targets) {
111
+ try {
112
+ process.kill(targetPid, signal);
113
+ } catch (err) {
114
+ if (err.code !== 'ESRCH') throw err;
115
+ }
116
+ }
117
+
118
+ await new Promise(resolve => setTimeout(resolve, 300));
119
+ for (const targetPid of targets) {
120
+ try {
121
+ process.kill(targetPid, 0);
122
+ process.kill(targetPid, 'SIGKILL');
123
+ } catch (err) {
124
+ if (err.code !== 'ESRCH') throw err;
125
+ }
126
+ }
86
127
  }
87
128
 
88
129
  /**
@@ -174,85 +215,11 @@ export function installShutdownHandlers() {
174
215
  }
175
216
 
176
217
  /**
177
- * Get count of tracked PIDs.
178
- * @returns {number}
179
- */
180
- /**
181
- * Execute Panic Mode (Self-Destruct)
182
- * Wipes all configs, keys, and forcefully kills associated processes.
218
+ * Execute a scoped emergency shutdown.
219
+ * Only resources registered by this process are touched.
183
220
  */
184
221
  export async function executePanicMode() {
185
- console.log(chalk.bold.red('\n 🚨 INITIATING SECURELINK PANIC MODE 🚨\n'));
186
-
187
- // 1. Force kill all cloudflared & ipingyou processes
188
- console.log(chalk.dim(' [1/4] Terminating all tunnel and host processes...'));
189
- try {
190
- if (process.platform === 'win32') {
191
- // taskkill expects a command string on Windows; pass args safely
192
- await execa('taskkill', ['/F', '/IM', 'cloudflared.exe'], { reject: false });
193
- } else {
194
- // Use argument arrays to avoid shell interpolation
195
- await execa('pkill', ['-9', '-f', 'cloudflared'], { reject: false });
196
- await execa('pkill', ['-9', '-f', 'sshd:.*@'], { reject: false });
197
-
198
- const socketArgs = Array.isArray(tmuxSocketArgs) ? tmuxSocketArgs() : [];
199
- // Ensure socketArgs are strings and safe-ish before passing to execa
200
- const safeSocketArgs = (socketArgs || []).map(a => String(a));
201
- await execa('tmux', [...safeSocketArgs, 'kill-server'], { reject: false });
202
-
203
- const { stdout } = await execa('tmux', ['list-sessions', '-F', '#{session_name}'], { reject: false });
204
- const legacyNames = stdout
205
- .split(/\r?\n/)
206
- .filter(Boolean)
207
- .filter(name => name === TMUX_SESSION_NAME || name.startsWith(TMUX_SESSION_PREFIX));
208
- for (const name of legacyNames) {
209
- await execa('tmux', ['kill-session', '-t', name], { reject: false });
210
- }
211
- }
212
- } catch (err) {
213
- // Best-effort cleanup; log debug info without exposing stack in normal flow
214
- // (keep behavior unchanged otherwise)
215
- }
216
-
217
- // 2. Delete configuration and aliases
218
- console.log(chalk.dim(' [2/4] Wiping configuration files...'));
219
- const configPath = path.join(os.homedir(), '.ipingyou', 'config.json');
220
- try {
221
- if (fs.existsSync(configPath)) {
222
- fs.unlinkSync(configPath);
223
- }
224
- const configDir = path.join(os.homedir(), '.ipingyou');
225
- if (fs.existsSync(configDir)) {
226
- fs.rmSync(configDir, { recursive: true, force: true });
227
- }
228
- } catch {}
229
-
230
- // 3. Delete ephemeral keys and temp files
231
- console.log(chalk.dim(' [3/4] Purging ephemeral keys and temporary files...'));
232
- try {
233
- const tmpDir = os.tmpdir();
234
- const files = fs.readdirSync(tmpDir);
235
- for (const file of files) {
236
- if (file.startsWith('ipingyou_') || file.startsWith('ipingyou-')) {
237
- fs.unlinkSync(path.join(tmpDir, file));
238
- }
239
- }
240
- } catch {}
241
-
242
- console.log(chalk.dim(' [4/4] Scrubbing injected SSH keys...'));
243
- try {
244
- const authKeysPath = path.join(os.homedir(), '.ssh', 'authorized_keys');
245
- if (fs.existsSync(authKeysPath)) {
246
- const current = fs.readFileSync(authKeysPath, 'utf8');
247
- const cleaned = current
248
- .split(/\r?\n/)
249
- .filter(line => !line.includes('ipingyou-ephemeral'))
250
- .join('\n')
251
- .replace(/\n{3,}/g, '\n\n');
252
- if (cleaned !== current) fs.writeFileSync(authKeysPath, cleaned);
253
- }
254
- } catch {}
255
-
256
- console.log(chalk.bold.green('\n ✅ Panic Mode Complete. All traces removed.\n'));
257
- process.exit(0);
222
+ console.log(chalk.bold.red('\n 🚨 INITIATING SCOPED EMERGENCY SHUTDOWN 🚨\n'));
223
+ await cleanupAll();
224
+ console.log(chalk.bold.green(' ✅ Current iPingYou session stopped safely.\n'));
258
225
  }
@@ -0,0 +1,28 @@
1
+ import { execa } from 'execa';
2
+
3
+ export async function openUrl(value) {
4
+ let url;
5
+ try {
6
+ url = new URL(String(value));
7
+ } catch {
8
+ throw new Error('Cannot open an invalid URL');
9
+ }
10
+ if (!['http:', 'https:'].includes(url.protocol) || url.href.length > 4096) {
11
+ throw new Error('Only HTTP(S) URLs can be opened');
12
+ }
13
+
14
+ const [command, args] = process.platform === 'darwin'
15
+ ? ['open', [url.href]]
16
+ : process.platform === 'win32'
17
+ ? ['explorer.exe', [url.href]]
18
+ : ['xdg-open', [url.href]];
19
+
20
+ const result = await execa(command, args, {
21
+ reject: false,
22
+ stdio: 'ignore',
23
+ timeout: 10000,
24
+ });
25
+ if (result.failed && result.exitCode !== 0) {
26
+ throw new Error(`Could not open URL with ${command}`);
27
+ }
28
+ }