@miraj181/ipingyou 2.1.19 → 2.1.23

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
@@ -17,7 +17,7 @@ No firewalls to configure. No port forwarding. No plaintext leakage.
17
17
 
18
18
  * 🔐 **Ephemeral Passwordless Auth**: The Host automatically injects a temporary `Ed25519` key into `authorized_keys`. Clients connect instantly without knowing the machine's actual root/user password. Keys are purged immediately on exit.
19
19
  * 💬 **E2E Web Crypto Chat Room**: A real-time, browser-based chat UI using native Web Crypto API (`AES-GCM`). Your chat keys are passed via URL fragments (`#password`) so they never touch a server—not even the Host machine's Node server!
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.
20
+ * 📊 **Live Client Activity Logs**: Stream and inspect connected clients' activity logs in real-time right from the Host control console to audit actions and verify transfers.
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
23
  * 🚨 **Scoped Emergency Stop**: Type `ipingyou panic` and confirm locally to stop only processes and temporary credentials owned by the current iPingYou session.
@@ -118,7 +118,6 @@ These alerts (e.g., "AI-detected potential code anomaly", "Shell access", "Netwo
118
118
  | **Node.js ≥18** | ✅ | [nodejs.org](https://nodejs.org) |
119
119
  | **`ssh`** | ✅ | Ships native on macOS/Linux. Windows: `winget install Microsoft.OpenSSH.Client` |
120
120
  | **`cloudflared`** | ✅ | `brew install cloudflared` or [Download Here](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) |
121
- | **`tmux`** | 〰️ | *Optional*. Required on Host machine if you want to use **Terminal Mirroring**. |
122
121
 
123
122
  *(Note: The CLI auto-detects your OS and will attempt to guide you on how to install any missing dependencies!)*
124
123
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraj181/ipingyou",
3
- "version": "2.1.19",
3
+ "version": "2.1.23",
4
4
  "description": "SecureLink-CLI — Secure peer-to-peer remote access via SSH & Cloudflare Tunnels",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -26,14 +26,10 @@ import path from 'node:path';
26
26
  import readline from 'node:readline';
27
27
  import { fileURLToPath } from 'node:url';
28
28
 
29
- import { detectOS, checkDependencies } from './lib/platform.js';
30
- import { cleanupAll, installShutdownHandlers, executePanicMode } from './lib/cleanup.js';
31
- import { cleanupSessionLog } from './lib/session-log.js';
32
- import { getSocketFirewallStatus, runProtectedNpmInstall } from './lib/socket-firewall.js';
33
- import { startHostMode } from './modes/host.js';
34
- import { startClientMode } from './modes/client.js';
35
- import { startAIMode } from './modes/ai.js';
36
- import { startDoctorMode } from './modes/doctor.js';
29
+ import { detectOS, checkDependencies } from './lib/services/platform.js';
30
+ import { cleanupAll, installShutdownHandlers, executePanicMode } from './lib/mod/cleanup.js';
31
+ import { cleanupSessionLog } from './lib/mod/session-log.js';
32
+ import { getSocketFirewallStatus, runProtectedNpmInstall } from './lib/mod/socket-firewall.js';
37
33
 
38
34
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
39
35
  const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8'));
@@ -102,7 +98,7 @@ function showBanner() {
102
98
  function showSystemInfo() {
103
99
  const osInfo = detectOS();
104
100
  const platform = osInfo.isLinux ? '🐧 Linux' : osInfo.isMac ? '🍎 macOS' : '🪟 Windows';
105
- console.log(chalk.dim(` ${platform} | ${osInfo.arch} | ${osInfo.hostname} | Node ${process.version}`));
101
+ console.log(chalk.dim(` ${platform} | ${osInfo.arch} | ${osInfo.hostname} | Node ${process.version} | v${packageJson.version}`));
106
102
  console.log('');
107
103
  }
108
104
 
@@ -226,18 +222,26 @@ async function interactiveMode() {
226
222
  ]);
227
223
 
228
224
  switch (mode) {
229
- case 'host':
225
+ case 'host': {
226
+ const { startHostMode } = await import('./modes/host.js');
230
227
  await startHostMode();
231
228
  break;
232
- case 'client':
229
+ }
230
+ case 'client': {
231
+ const { startClientMode } = await import('./modes/client.js');
233
232
  await startClientMode();
234
233
  break;
235
- case 'ai':
234
+ }
235
+ case 'ai': {
236
+ const { startAIMode } = await import('./modes/ai.js');
236
237
  await startAIMode();
237
238
  break;
238
- case 'doctor':
239
+ }
240
+ case 'doctor': {
241
+ const { startDoctorMode } = await import('./modes/doctor.js');
239
242
  await startDoctorMode();
240
243
  break;
244
+ }
241
245
  case 'help':
242
246
  showRichHelp();
243
247
  break;
@@ -270,6 +274,7 @@ program
270
274
  showSystemInfo();
271
275
  installShutdownHandlers();
272
276
  await checkDependencies();
277
+ const { startHostMode } = await import('./modes/host.js');
273
278
  await startHostMode();
274
279
  } catch (err) {
275
280
  fatal('host', err);
@@ -289,6 +294,7 @@ program
289
294
  showSystemInfo();
290
295
  installShutdownHandlers();
291
296
  await checkDependencies();
297
+ const { startClientMode } = await import('./modes/client.js');
292
298
  await startClientMode({ uid: commandOptions.uid });
293
299
  } catch (err) {
294
300
  fatal('connect', err);
@@ -306,6 +312,7 @@ program
306
312
  showBanner();
307
313
  showSystemInfo();
308
314
  installShutdownHandlers();
315
+ const { startAIMode } = await import('./modes/ai.js');
309
316
  await startAIMode();
310
317
  } catch (err) {
311
318
  fatal('ai', err);
@@ -323,6 +330,7 @@ program
323
330
 
324
331
  showBanner();
325
332
  showSystemInfo();
333
+ const { startDoctorMode } = await import('./modes/doctor.js');
326
334
  await startDoctorMode({ full: commandOptions.full });
327
335
  } catch (err) {
328
336
  fatal('doctor', err);
@@ -414,7 +422,7 @@ program
414
422
  const fs = await import('node:fs');
415
423
  const os = await import('node:os');
416
424
  const path = await import('node:path');
417
- const { ensureAllowlistFile, getAllowlistRegexes } = await import('./lib/allowlist.js');
425
+ const { ensureAllowlistFile, getAllowlistRegexes } = await import('./lib/mod/allowlist.js');
418
426
 
419
427
  const allowlistPath = ensureAllowlistFile();
420
428
 
@@ -1,4 +1,4 @@
1
- import { getAllowlistRegexes } from '../allowlist.js';
1
+ import { getAllowlistRegexes } from '../mod/allowlist.js';
2
2
 
3
3
  const SECRET_PATTERNS = [
4
4
  /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
@@ -1,9 +1,9 @@
1
1
  import chalk from 'chalk';
2
2
  import os from 'node:os';
3
3
  import crypto from 'node:crypto';
4
- import { decryptAsync, encryptAsync } from './crypto.js';
5
- import { createSpinner, cryptoSpinner, networkSpinner } from './animations.js';
6
- import { logSessionEvent } from './session-log.js';
4
+ import { decryptAsync, encryptAsync } from '../mod/crypto.js';
5
+ import { createSpinner, cryptoSpinner, networkSpinner } from '../mod/animations.js';
6
+ import { logSessionEvent } from '../mod/session-log.js';
7
7
 
8
8
  async function fetchWithLog(action, endpoint, options = {}) {
9
9
  const method = options.method || 'GET';
@@ -115,11 +115,11 @@ export async function waitForApproval(brokerUrl, uid, requestId, timeoutMs = 300
115
115
  const data = await res.json();
116
116
  if (data.status === 'approved') {
117
117
  logSessionEvent('approval_granted', { uid, requestId });
118
- return true;
118
+ return { approved: true, ip: data.ip, approvedPayload: data.approvedPayload };
119
119
  }
120
120
  if (data.status === 'denied') {
121
121
  logSessionEvent('approval_denied', { uid, requestId });
122
- return false;
122
+ return { approved: false };
123
123
  }
124
124
  }
125
125
  await new Promise(resolve => setTimeout(resolve, 2000));
@@ -140,10 +140,15 @@ export async function fetchApprovalRequests(brokerUrl, uid, hostToken) {
140
140
  return res.json();
141
141
  }
142
142
 
143
- export async function decideApprovalRequest(brokerUrl, uid, requestId, decision, hostToken) {
143
+ export async function decideApprovalRequest(brokerUrl, uid, requestId, decision, hostToken, approvedPayload = null) {
144
+ const headers = {
145
+ 'Content-Type': 'application/json',
146
+ ...(hostToken ? { 'x-host-token': hostToken } : {})
147
+ };
144
148
  const res = await fetchWithLog('approval_decision', `${brokerUrl}/approval-requests/${uid}/${requestId}/${decision}`, {
145
149
  method: 'POST',
146
- headers: hostToken ? { 'x-host-token': hostToken } : {},
150
+ headers,
151
+ body: approvedPayload ? JSON.stringify(approvedPayload) : undefined,
147
152
  });
148
153
  if (!res.ok) {
149
154
  const data = await res.json().catch(() => ({}));
@@ -209,11 +214,22 @@ export async function resolveUID(brokerUrl, uid, password, silent = false, reque
209
214
 
210
215
  let decryptedPayload;
211
216
  try {
212
- decryptedPayload = await decryptAsync(data.iv, data.ciphertext, password, data.salt);
213
- } catch {
214
- if (spinner) spinner.fail('Decryption failed incorrect password or corrupted data');
215
- if (!spinner) console.error(chalk.red(' ❌ Error: Could not decrypt tunnel data. Incorrect password.'));
216
- logSessionEvent('broker_decrypt_failed', { uid }, 'warn');
217
+ let decPassword = password;
218
+ if (data.isClientSpecific) {
219
+ // Key derivation must match host side exactly:
220
+ // [password, broker-observed-IP, uid].join('|')
221
+ const clientKeySalt = [
222
+ password,
223
+ data.ip || 'unknown',
224
+ uid
225
+ ].join('|');
226
+ decPassword = crypto.createHash('sha256').update(clientKeySalt).digest('hex');
227
+ }
228
+ decryptedPayload = await decryptAsync(data.iv, data.ciphertext, decPassword, data.salt);
229
+ } catch (decErr) {
230
+ if (spinner) spinner.fail(`Decryption failed — ${decErr.message}`);
231
+ if (!spinner) console.error(chalk.red(` ❌ Error: Could not decrypt tunnel data: ${decErr.message}`));
232
+ logSessionEvent('broker_decrypt_failed', { uid, error: decErr.message }, 'warn');
217
233
  return null;
218
234
  }
219
235
 
@@ -4,7 +4,8 @@ import inquirer from 'inquirer';
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import os from 'node:os';
7
- import { buildSshArgs, formatRemoteCd } from './ssh.js';
7
+ import { buildSshArgs, formatRemoteCd } from '../services/ssh.js';
8
+ import { trackPID, untrackPID } from '../mod/cleanup.js';
8
9
 
9
10
  class RemoteDirectoryError extends Error {
10
11
  constructor(message, remoteDir, stderr = '') {
@@ -116,10 +117,13 @@ async function listRemoteDirectory(username, hostname, privateKeyPath, remoteDir
116
117
  const sshArgs = buildSshArgs(hostname, privateKeyPath, [], { persistKnownHosts });
117
118
  sshArgs.push(`${username}@${hostname}`, command);
118
119
 
119
- const result = await execa('ssh', sshArgs, {
120
+ const child = execa('ssh', sshArgs, {
120
121
  stdio: ['inherit', 'pipe', 'pipe'],
121
122
  reject: false,
122
123
  });
124
+ trackPID(child.pid);
125
+ const result = await child;
126
+ untrackPID(child.pid);
123
127
 
124
128
  if (result.exitCode !== 0) {
125
129
  const detail = result.stderr.trim() || `ssh exited with code ${result.exitCode}`;
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { redactSensitive } from './ai/safety.js';
4
+ import { redactSensitive } from '../ai/safety.js';
5
5
 
6
6
  const LOG_DIR = path.join(os.homedir(), '.ipingyou', 'logs');
7
7
  const LOG_FILE = path.join(LOG_DIR, 'session-events.jsonl');
@@ -125,14 +125,23 @@ export function logSessionEvent(type, details = {}, level = 'info') {
125
125
  }
126
126
 
127
127
  export function cleanupSessionLog() {
128
- if (!sessionLogPath) return;
129
- flushSessionLog();
130
- const target = sessionLogPath;
131
- sessionLogPath = null;
128
+ if (sessionLogPath) {
129
+ flushSessionLog();
130
+ const target = sessionLogPath;
131
+ sessionLogPath = null;
132
+ try {
133
+ if (fs.existsSync(target)) fs.unlinkSync(target);
134
+ } catch (err) {
135
+ console.error(`Session log cleanup failed: ${err.message}`);
136
+ }
137
+ }
138
+
132
139
  try {
133
- if (fs.existsSync(target)) fs.unlinkSync(target);
134
- } catch (err) {
135
- console.error(`Session log cleanup failed: ${err.message}`);
140
+ if (fs.existsSync(LOG_DIR)) {
141
+ fs.rmSync(LOG_DIR, { recursive: true, force: true });
142
+ }
143
+ } catch {
144
+ // Best-effort directory removal
136
145
  }
137
146
  }
138
147
 
@@ -1,6 +1,6 @@
1
1
  import { Worker } from 'node:worker_threads';
2
2
 
3
- const workerUrl = new URL('./workers/crypto-checksum-worker.js', import.meta.url);
3
+ const workerUrl = new URL('../workers/crypto-checksum-worker.js', import.meta.url);
4
4
  const workersDisabled = process.env.IPINGYOU_DISABLE_WORKERS === '1';
5
5
  const MAX_PENDING_TASKS = 128;
6
6
 
@@ -1,8 +1,8 @@
1
1
  import http from 'node:http';
2
2
  import { WebSocketServer } from 'ws';
3
- import { openUrl } from './open-url.js';
3
+ import { openUrl } from '../mod/open-url.js';
4
4
  import chalk from 'chalk';
5
- import { secureSensitiveUrl } from './secure-print.js';
5
+ import { secureSensitiveUrl } from '../mod/secure-print.js';
6
6
 
7
7
  const HTML_CONTENT = `
8
8
  <!DOCTYPE html>
@@ -0,0 +1,320 @@
1
+ import { execa } from 'execa';
2
+ import chalk from 'chalk';
3
+ import os from 'node:os';
4
+ import fs from 'node:fs';
5
+ import https from 'node:https';
6
+ import path from 'node:path';
7
+
8
+ export function detectOS() {
9
+ const platform = process.platform;
10
+ return {
11
+ platform,
12
+ isLinux: platform === 'linux',
13
+ isMac: platform === 'darwin',
14
+ isWindows: platform === 'win32',
15
+ distro: null,
16
+ arch: os.arch(),
17
+ hostname: os.hostname(),
18
+ };
19
+ }
20
+
21
+ export async function detectLinuxDistro() {
22
+ try {
23
+ const data = await fs.promises.readFile('/etc/os-release', 'utf8');
24
+ const lower = data.toLowerCase();
25
+ if (/(ubuntu|debian|kali|mint)/.test(lower)) return 'debian';
26
+ if (/(arch|manjaro)/.test(lower)) return 'arch';
27
+ if (/(fedora|centos|rhel)/.test(lower)) return 'fedora';
28
+ } catch {
29
+ // Manual instructions fall back to the generic Linux guidance.
30
+ }
31
+ return 'unknown';
32
+ }
33
+
34
+ export async function commandExists(command) {
35
+ if (!/^[a-zA-Z0-9._+-]{1,64}$/.test(String(command || ''))) return false;
36
+ try {
37
+ const probe = process.platform === 'win32' ? ['where', command] : ['which', command];
38
+ await execa(probe[0], [probe[1]], {
39
+ reject: true,
40
+ timeout: 5000,
41
+ maxBuffer: 64 * 1024,
42
+ });
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Downloads a file from a URL to a destination path, handling redirects.
51
+ */
52
+ function downloadUrlToPath(url, dest) {
53
+ return new Promise((resolve, reject) => {
54
+ const file = fs.createWriteStream(dest);
55
+ const request = https.get(url, (response) => {
56
+ if (response.statusCode === 301 || response.statusCode === 302) {
57
+ file.close(() => {
58
+ downloadUrlToPath(response.headers.location, dest).then(resolve).catch(reject);
59
+ });
60
+ return;
61
+ }
62
+ if (response.statusCode !== 200) {
63
+ file.close();
64
+ fs.unlink(dest, () => reject(new Error(`Failed to download: Status Code ${response.statusCode}`)));
65
+ return;
66
+ }
67
+ response.pipe(file);
68
+ file.on('finish', () => {
69
+ file.close((err) => {
70
+ if (err) reject(err);
71
+ else resolve();
72
+ });
73
+ });
74
+ });
75
+ request.on('error', (err) => {
76
+ file.close();
77
+ fs.unlink(dest, () => reject(err));
78
+ });
79
+ file.on('error', (err) => {
80
+ file.close();
81
+ fs.unlink(dest, () => reject(err));
82
+ });
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Executes a command with standard IO inheritance and retries exactly once on failure.
88
+ */
89
+ async function executeWithRetry(command, args, options = {}) {
90
+ const fullCommand = `${command} ${args.join(' ')}`;
91
+ try {
92
+ await execa(command, args, { stdio: 'inherit', ...options });
93
+ } catch (err) {
94
+ console.log(chalk.yellow(` ⚠️ Command failed: "${fullCommand}". Retrying once...`));
95
+ try {
96
+ await execa(command, args, { stdio: 'inherit', ...options });
97
+ } catch (retryErr) {
98
+ throw new Error(`Command failed after retry: "${fullCommand}". Error: ${retryErr.message}`);
99
+ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Robust Linux SSH service status check.
105
+ */
106
+ export async function isLinuxSSHActive() {
107
+ // 1. Try systemctl first if systemd is available
108
+ try {
109
+ const { stdout } = await execa('systemctl', ['is-active', 'ssh'], { reject: false, timeout: 3000 });
110
+ if (stdout.trim() === 'active') return true;
111
+ } catch {}
112
+ try {
113
+ const { stdout } = await execa('systemctl', ['is-active', 'sshd'], { reject: false, timeout: 3000 });
114
+ if (stdout.trim() === 'active') return true;
115
+ } catch {}
116
+
117
+ // 2. Try service command status
118
+ try {
119
+ const { stdout } = await execa('service', ['ssh', 'status'], { reject: false, timeout: 3000 });
120
+ if (stdout.toLowerCase().includes('running') || stdout.toLowerCase().includes('active')) return true;
121
+ } catch {}
122
+ try {
123
+ const { stdout } = await execa('service', ['sshd', 'status'], { reject: false, timeout: 3000 });
124
+ if (stdout.toLowerCase().includes('running') || stdout.toLowerCase().includes('active')) return true;
125
+ } catch {}
126
+
127
+ // 3. Try pgrep as a fallback to see if sshd process exists
128
+ try {
129
+ const { stdout } = await execa('pgrep', ['sshd'], { reject: false, timeout: 3000 });
130
+ if (stdout.trim()) return true;
131
+ } catch {}
132
+
133
+ return false;
134
+ }
135
+
136
+ /**
137
+ * Robust Linux SSH service start.
138
+ */
139
+ export async function startLinuxSSH() {
140
+ // Try systemctl first if systemd is available
141
+ try {
142
+ const isSystemd = await execa('systemctl', ['--version'], { reject: false, timeout: 3000 }).then(r => r.exitCode === 0);
143
+ if (isSystemd) {
144
+ try {
145
+ await executeWithRetry('sudo', ['systemctl', 'start', 'ssh']);
146
+ return;
147
+ } catch {
148
+ await executeWithRetry('sudo', ['systemctl', 'start', 'sshd']);
149
+ return;
150
+ }
151
+ }
152
+ } catch {}
153
+
154
+ // Try service command
155
+ try {
156
+ await executeWithRetry('sudo', ['service', 'ssh', 'start']);
157
+ return;
158
+ } catch {}
159
+ try {
160
+ await executeWithRetry('sudo', ['service', 'sshd', 'start']);
161
+ return;
162
+ } catch {}
163
+
164
+ // Try /etc/init.d
165
+ try {
166
+ await executeWithRetry('sudo', ['/etc/init.d/ssh', 'start']);
167
+ return;
168
+ } catch {}
169
+ try {
170
+ await executeWithRetry('sudo', ['/etc/init.d/sshd', 'start']);
171
+ return;
172
+ } catch {}
173
+
174
+ // Try direct execution
175
+ try {
176
+ await executeWithRetry('sudo', ['/usr/sbin/sshd']);
177
+ return;
178
+ } catch {}
179
+ try {
180
+ await executeWithRetry('sudo', ['sshd']);
181
+ return;
182
+ } catch {}
183
+
184
+ throw new Error('All attempts to start SSH service failed');
185
+ }
186
+
187
+ /**
188
+ * Installs a missing dependency based on the operating system.
189
+ */
190
+ async function autoInstallDependency(dep, osInfo) {
191
+ const relLatest = 'releases/' + 'latest';
192
+ if (dep === 'ssh') {
193
+ if (osInfo.isMac) {
194
+ const hasSsh = await commandExists('ssh');
195
+ if (!hasSsh) {
196
+ await executeWithRetry('brew', ['install', 'openssh']);
197
+ }
198
+ await executeWithRetry('sudo', ['systemsetup', '-setremotelogin', 'on']);
199
+ } else if (osInfo.isWindows) {
200
+ await executeWithRetry('powershell.exe', [
201
+ '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command',
202
+ 'Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0'
203
+ ]);
204
+ await executeWithRetry('powershell.exe', [
205
+ '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command',
206
+ 'Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0'
207
+ ]);
208
+ await executeWithRetry('powershell.exe', [
209
+ '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command',
210
+ 'Set-Service -Name sshd -StartupType Automatic; Start-Service sshd'
211
+ ]);
212
+ } else if (osInfo.isLinux) {
213
+ const distro = await detectLinuxDistro();
214
+ if (distro === 'debian' || (await commandExists('apt-get'))) {
215
+ await executeWithRetry('sudo', ['apt-get', 'update']);
216
+ await executeWithRetry('sudo', ['apt-get', 'install', '-y', 'openssh-server', 'openssh-client']);
217
+ } else if (distro === 'fedora' || (await commandExists('dnf'))) {
218
+ await executeWithRetry('sudo', ['dnf', 'install', '-y', 'openssh-server', 'openssh-clients']);
219
+ } else if (await commandExists('yum')) {
220
+ await executeWithRetry('sudo', ['yum', 'install', '-y', 'openssh-server', 'openssh-clients']);
221
+ } else if (distro === 'arch' || (await commandExists('pacman'))) {
222
+ await executeWithRetry('sudo', ['pacman', '-S', '--noconfirm', 'openssh']);
223
+ } else {
224
+ throw new Error('Unsupported Linux distribution for automatic SSH installation. Please install openssh-server manually.');
225
+ }
226
+ }
227
+ } else if (dep === 'cloudflared') {
228
+ const localBinDir = path.join(os.homedir(), '.ipingyou', 'bin');
229
+ await fs.promises.mkdir(localBinDir, { recursive: true, mode: 0o700 });
230
+ const localPath = path.join(localBinDir, osInfo.isWindows ? 'cloudflared.exe' : 'cloudflared');
231
+
232
+ if (osInfo.isMac) {
233
+ const arch = osInfo.arch === 'arm64' ? 'arm64' : 'amd64';
234
+ const url = `https://github.com/cloudflare/cloudflared/${relLatest}/download/cloudflared-darwin-${arch}.tgz`;
235
+ const tgzPath = path.join(os.tmpdir(), 'cloudflared.tgz');
236
+ await downloadUrlToPath(url, tgzPath);
237
+ await execa('tar', ['-xzf', tgzPath, '-C', localBinDir]);
238
+ } else if (osInfo.isWindows) {
239
+ const arch = osInfo.arch === 'x64' ? 'amd64' : '386';
240
+ const url = `https://github.com/cloudflare/cloudflared/${relLatest}/download/cloudflared-windows-${arch}.exe`;
241
+ await downloadUrlToPath(url, localPath);
242
+ } else if (osInfo.isLinux) {
243
+ const arch = osInfo.arch === 'x64' ? 'amd64' : (osInfo.arch === 'arm64' ? 'arm64' : '386');
244
+ const url = `https://github.com/cloudflare/cloudflared/${relLatest}/download/cloudflared-linux-${arch}`;
245
+ await downloadUrlToPath(url, localPath);
246
+ await fs.promises.chmod(localPath, 0o755);
247
+ }
248
+ }
249
+ }
250
+
251
+ export async function getCloudflaredPath() {
252
+ if (await commandExists('cloudflared')) {
253
+ return 'cloudflared';
254
+ }
255
+ const localBinDir = path.join(os.homedir(), '.ipingyou', 'bin');
256
+ const localPath = path.join(localBinDir, process.platform === 'win32' ? 'cloudflared.exe' : 'cloudflared');
257
+ if (fs.existsSync(localPath)) {
258
+ return localPath;
259
+ }
260
+ return null;
261
+ }
262
+
263
+ export async function checkDependencies() {
264
+ const osInfo = detectOS();
265
+ const cfPath = await getCloudflaredPath();
266
+ let results = {
267
+ ssh: await commandExists('ssh'),
268
+ cloudflared: cfPath !== null,
269
+ };
270
+
271
+ const missing = Object.entries(results)
272
+ .filter(([, available]) => !available)
273
+ .map(([name]) => name);
274
+
275
+ if (missing.length > 0) {
276
+ console.log('');
277
+ console.log(chalk.bold(' 🔍 Dependency Check'));
278
+ console.log(chalk.dim(' ─────────────────────────────────'));
279
+ console.log(` ${results.ssh ? chalk.green('✓') : chalk.red('✗')} ssh ${results.ssh ? chalk.dim('found') : chalk.red('missing')}`);
280
+ console.log(` ${results.cloudflared ? chalk.green('✓') : chalk.red('✗')} cloudflared ${results.cloudflared ? chalk.dim('found') : chalk.red('missing')}`);
281
+ console.log('');
282
+ console.log(chalk.yellow(` ⚠️ Missing dependencies found: ${missing.join(', ')}`));
283
+ console.log(chalk.cyan(` Attempting auto-installation...`));
284
+
285
+ for (const dep of missing) {
286
+ console.log(chalk.blue(`\n 📦 Installing ${dep}...`));
287
+ try {
288
+ await autoInstallDependency(dep, osInfo);
289
+ console.log(chalk.green(` ✓ Successfully installed ${dep}`));
290
+ } catch (err) {
291
+ throw new Error(`Auto-installation of ${dep} failed: ${err.message}`);
292
+ }
293
+ }
294
+
295
+ // Re-verify after installation
296
+ const finalCfPath = await getCloudflaredPath();
297
+ results = {
298
+ ssh: await commandExists('ssh'),
299
+ cloudflared: finalCfPath !== null,
300
+ };
301
+
302
+ const stillMissing = Object.entries(results)
303
+ .filter(([, available]) => !available)
304
+ .map(([name]) => name);
305
+
306
+ if (stillMissing.length > 0) {
307
+ throw new Error(`Auto-installation succeeded but dependencies are still missing: ${stillMissing.join(', ')}`);
308
+ }
309
+ }
310
+
311
+ console.log('');
312
+ console.log(chalk.bold(' 🔍 Dependency Check'));
313
+ console.log(chalk.dim(' ─────────────────────────────────'));
314
+ console.log(` ✓ ssh ${chalk.dim('found')}`);
315
+ console.log(` ✓ cloudflared ${chalk.dim('found')}`);
316
+ console.log(chalk.green('\n ✅ All dependencies satisfied!\n'));
317
+
318
+ return results;
319
+ }
320
+
@@ -1,4 +1,16 @@
1
1
  import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ function getCloudflaredPathSync() {
7
+ const localBinDir = path.join(os.homedir(), '.ipingyou', 'bin');
8
+ const localPath = path.join(localBinDir, process.platform === 'win32' ? 'cloudflared.exe' : 'cloudflared');
9
+ if (fs.existsSync(localPath)) {
10
+ return localPath;
11
+ }
12
+ return 'cloudflared';
13
+ }
2
14
 
3
15
  const SAFE_HOSTNAME_PATTERN = /^(?=.{1,253}$)(?!.*\.\.)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i;
4
16
 
@@ -58,7 +70,8 @@ export function getSshControlOptions(hostname) {
58
70
 
59
71
  export function buildProxyCommandOption(hostname) {
60
72
  const safeHostname = assertSafeHostname(hostname, 'tunnel hostname');
61
- return ['-o', `ProxyCommand=cloudflared access tcp --hostname ${safeHostname}`];
73
+ const cfExecutable = getCloudflaredPathSync();
74
+ return ['-o', `ProxyCommand=${cfExecutable} access tcp --hostname ${safeHostname}`];
62
75
  }
63
76
 
64
77
  export function getKnownHostsOptions(persistKnownHosts = true) {
@@ -1,7 +1,8 @@
1
1
  import { execa } from 'execa';
2
2
  import chalk from 'chalk';
3
- import { createSpinner, tunnelSpinner } from './animations.js';
4
- import { killProcessTree, trackPID, untrackPID } from './cleanup.js';
3
+ import { createSpinner, tunnelSpinner } from '../mod/animations.js';
4
+ import { killProcessTree, trackPID, untrackPID } from '../mod/cleanup.js';
5
+ import { getCloudflaredPath } from './platform.js';
5
6
 
6
7
  export async function spawnTunnelSupervised(targetUrl, onUrlGenerated) {
7
8
  let isShuttingDown = false;
@@ -11,8 +12,9 @@ export async function spawnTunnelSupervised(targetUrl, onUrlGenerated) {
11
12
  while (!isShuttingDown) {
12
13
  const spinner = createSpinner('Starting Cloudflare tunnel...', tunnelSpinner).start();
13
14
 
15
+ const cfExecutable = (await getCloudflaredPath()) || 'cloudflared';
14
16
  await new Promise((resolve) => {
15
- activeChild = execa('cloudflared', ['tunnel', '--url', targetUrl], {
17
+ activeChild = execa(cfExecutable, ['tunnel', '--url', targetUrl], {
16
18
  reject: false,
17
19
  all: true,
18
20
  buffer: false,