@miraj181/ipingyou 2.0.12 → 2.0.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraj181/ipingyou",
3
- "version": "2.0.12",
3
+ "version": "2.0.14",
4
4
  "description": "SecureLink-CLI — Secure peer-to-peer remote access via SSH & Cloudflare Tunnels",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
@@ -6,6 +6,15 @@ import path from 'node:path';
6
6
  import os from 'node:os';
7
7
  import { buildSshArgs, formatRemoteCd } from './ssh.js';
8
8
 
9
+ class RemoteDirectoryError extends Error {
10
+ constructor(message, remoteDir, stderr = '') {
11
+ super(message);
12
+ this.name = 'RemoteDirectoryError';
13
+ this.remoteDir = remoteDir;
14
+ this.stderr = stderr;
15
+ }
16
+ }
17
+
9
18
  function expandLocalPath(inputPath) {
10
19
  const trimmed = inputPath.trim();
11
20
  if (trimmed === '~') return os.homedir();
@@ -108,12 +117,13 @@ async function listRemoteDirectory(username, hostname, privateKeyPath, remoteDir
108
117
  sshArgs.push(`${username}@${hostname}`, command);
109
118
 
110
119
  const result = await execa('ssh', sshArgs, {
111
- stdio: ['inherit', 'pipe', 'inherit'],
120
+ stdio: ['inherit', 'pipe', 'pipe'],
112
121
  reject: false,
113
122
  });
114
123
 
115
124
  if (result.exitCode !== 0) {
116
- throw new Error('Remote directory listing failed');
125
+ const detail = result.stderr.trim() || `ssh exited with code ${result.exitCode}`;
126
+ throw new RemoteDirectoryError(`Remote directory listing failed: ${detail}`, remoteDir, result.stderr);
117
127
  }
118
128
 
119
129
  const lines = result.stdout.split(/\r?\n/).filter(Boolean);
@@ -176,6 +186,18 @@ export async function promptRemotePath(username, hostname, privateKeyPath, purpo
176
186
  listing = await listRemoteDirectory(username, hostname, privateKeyPath, currentDir);
177
187
  } catch (err) {
178
188
  console.log(chalk.yellow(` ⚠️ Could not browse host files: ${err.message}`));
189
+ if (/Operation not permitted/i.test(err.stderr || err.message)) {
190
+ console.log(chalk.dim(' macOS privacy blocked this SSH session from listing that folder.'));
191
+ console.log(chalk.dim(' On the host, allow Full Disk Access for sshd/Remote Login, or choose a non-protected folder.'));
192
+ }
193
+
194
+ const parentDir = currentDir === '/' ? '/' : path.posix.dirname(currentDir);
195
+ if (parentDir && parentDir !== currentDir) {
196
+ console.log(chalk.dim(` Returning to ${parentDir}.`));
197
+ currentDir = parentDir;
198
+ continue;
199
+ }
200
+
179
201
  console.log(chalk.dim(' Falling back to manual host path entry.'));
180
202
  const { remotePath } = await inquirer.prompt([{
181
203
  type: 'input',
package/src/lib/ssh.js CHANGED
@@ -21,10 +21,7 @@ export function formatRemoteCd(remotePath) {
21
21
  export function formatScpRemotePath(remotePath) {
22
22
  const trimmed = String(remotePath || '').trim();
23
23
  if (!trimmed || trimmed === '~') return trimmed || '~';
24
- if (trimmed.startsWith('~/')) {
25
- return `~/${quoteRemoteShell(trimmed.slice(2))}`;
26
- }
27
- return quoteRemoteShell(trimmed);
24
+ return trimmed;
28
25
  }
29
26
 
30
27
  export function getSshControlOptions(hostname) {
@@ -42,6 +42,28 @@ async function promptUsername() {
42
42
  return username.trim();
43
43
  }
44
44
 
45
+ function normalizePrivateKey(privateKey) {
46
+ const normalized = String(privateKey || '').replace(/\\n/g, '\n').replace(/\r\n/g, '\n');
47
+ return normalized.endsWith('\n') ? normalized : `${normalized}\n`;
48
+ }
49
+
50
+ async function writeEphemeralPrivateKey(privateKey) {
51
+ const keyPath = path.join(os.tmpdir(), `ipingyou_client_${Date.now()}`);
52
+ fs.writeFileSync(keyPath, normalizePrivateKey(privateKey), { mode: 0o600 });
53
+
54
+ const result = await execa('ssh-keygen', ['-y', '-f', keyPath], {
55
+ reject: false,
56
+ stdio: ['ignore', 'pipe', 'pipe'],
57
+ });
58
+
59
+ if (result.exitCode !== 0) {
60
+ try { fs.unlinkSync(keyPath); } catch {}
61
+ throw new Error(result.stderr.trim() || 'OpenSSH could not parse the host-provided private key');
62
+ }
63
+
64
+ return keyPath;
65
+ }
66
+
45
67
  /**
46
68
  * Start SSH connection through the Cloudflare tunnel.
47
69
  */
@@ -162,6 +184,7 @@ async function performSCP(username, hostname, direction, privateKeyPath) {
162
184
  if (direction === 'upload' && localHash) {
163
185
  console.log(chalk.dim(' 🔍 Verifying remote SHA-256 checksum...'));
164
186
  try {
187
+ const remoteChecksumPath = joinRemotePath(remotePath, path.basename(localPath));
165
188
  const sshArgs = [
166
189
  '-o', `ProxyCommand=${proxyCommand}`,
167
190
  '-o', 'StrictHostKeyChecking=accept-new',
@@ -169,7 +192,7 @@ async function performSCP(username, hostname, direction, privateKeyPath) {
169
192
  ...getSshControlOptions(hostname)
170
193
  ];
171
194
  if (privateKeyPath) sshArgs.push('-i', privateKeyPath, '-o', 'IdentityAgent=none');
172
- sshArgs.push(`${username}@${hostname}`, `shasum -a 256 ${quoteRemoteShell(remotePath)} || sha256sum ${quoteRemoteShell(remotePath)}`);
195
+ sshArgs.push(`${username}@${hostname}`, `shasum -a 256 ${quoteRemoteShell(remoteChecksumPath)} 2>/dev/null || sha256sum ${quoteRemoteShell(remoteChecksumPath)} 2>/dev/null || shasum -a 256 ${quoteRemoteShell(remotePath)} 2>/dev/null || sha256sum ${quoteRemoteShell(remotePath)}`);
173
196
 
174
197
  const { stdout } = await execa('ssh', sshArgs, { reject: false });
175
198
  const remoteHash = stdout.split(' ')[0].trim();
@@ -198,6 +221,13 @@ async function performSCP(username, hostname, direction, privateKeyPath) {
198
221
  }
199
222
  }
200
223
 
224
+ function joinRemotePath(parent, child) {
225
+ const cleanParent = String(parent || '').replace(/\/+$/, '');
226
+ if (!cleanParent) return child;
227
+ if (cleanParent === '/') return `/${child}`;
228
+ return `${cleanParent}/${child}`;
229
+ }
230
+
201
231
  /**
202
232
  * Main Client Mode entry point.
203
233
  */
@@ -334,11 +364,16 @@ export async function startClientMode(options = {}) {
334
364
  let privateKeyPath = null;
335
365
  if (payload.privateKey) {
336
366
  console.log(chalk.green(' 🔑 Host provided an ephemeral SSH key for passwordless entry!'));
337
- privateKeyPath = path.join(os.tmpdir(), `ipingyou_client_${Date.now()}`);
338
- fs.writeFileSync(privateKeyPath, payload.privateKey, { mode: 0o600 });
339
- addCleanupHook(() => {
340
- try { fs.unlinkSync(privateKeyPath); } catch {}
341
- });
367
+ try {
368
+ privateKeyPath = await writeEphemeralPrivateKey(payload.privateKey);
369
+ addCleanupHook(() => {
370
+ try { fs.unlinkSync(privateKeyPath); } catch {}
371
+ });
372
+ } catch (err) {
373
+ console.log(chalk.yellow(` ⚠️ Could not use ephemeral SSH key: ${err.message}`));
374
+ console.log(chalk.dim(' Falling back to standard OS password.'));
375
+ privateKeyPath = null;
376
+ }
342
377
  }
343
378
 
344
379
  // Ask to save alias if we entered manually
package/src/modes/host.js CHANGED
@@ -146,20 +146,27 @@ async function ensureTmuxInstalled() {
146
146
 
147
147
  // ─── Ephemeral SSH Key Management ────────────────────────────
148
148
  async function generateEphemeralKey() {
149
- const tmpDir = os.tmpdir();
149
+ const tmpDir = os.tmpdir() || process.env.TMPDIR || process.env.TEMP || process.env.TMP;
150
+ if (!tmpDir) {
151
+ throw new Error('Could not resolve a temporary directory for SSH key generation');
152
+ }
150
153
  const keyPath = path.join(tmpDir, `ipingyou_${Date.now()}`);
151
154
 
152
155
  await execa('ssh-keygen', ['-t', 'ed25519', '-C', 'ipingyou-ephemeral', '-f', keyPath, '-N', '']);
153
156
 
154
- const privKey = (await fs.promises.readFile(keyPath, 'utf8')).trim();
157
+ const privKey = await fs.promises.readFile(keyPath, 'utf8');
155
158
  const pubKey = (await fs.promises.readFile(`${keyPath}.pub`, 'utf8')).trim();
156
159
 
157
160
  return { keyPath, privKey, pubKey };
158
161
  }
159
162
 
160
163
  async function injectPublicKey(pubKey) {
161
- const osInfo = detectOS();
162
- const sshDir = path.join(osInfo.homedir, '.ssh');
164
+ const homedir = os.homedir();
165
+ if (!homedir) {
166
+ throw new Error('Could not resolve the current user home directory for authorized_keys');
167
+ }
168
+
169
+ const sshDir = path.join(homedir, '.ssh');
163
170
 
164
171
  if (!fs.existsSync(sshDir)) {
165
172
  await fs.promises.mkdir(sshDir, { mode: 0o700, recursive: true });
@@ -537,7 +544,7 @@ export async function startHostMode() {
537
544
  });
538
545
  console.log(chalk.green(' ✓ Ephemeral key injected. Client will connect without system password!'));
539
546
  } catch (err) {
540
- console.log(chalk.yellow(` ⚠️ Could not generate ephemeral key: ${err.message}`));
547
+ console.log(chalk.yellow(` ⚠️ Could not prepare ephemeral SSH key: ${err.message}`));
541
548
  console.log(chalk.dim(' Client will need to use standard OS password.'));
542
549
  }
543
550
  } else {