@miraj181/ipingyou 2.0.13 → 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.13",
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',
@@ -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
@@ -154,7 +154,7 @@ async function generateEphemeralKey() {
154
154
 
155
155
  await execa('ssh-keygen', ['-t', 'ed25519', '-C', 'ipingyou-ephemeral', '-f', keyPath, '-N', '']);
156
156
 
157
- const privKey = (await fs.promises.readFile(keyPath, 'utf8')).trim();
157
+ const privKey = await fs.promises.readFile(keyPath, 'utf8');
158
158
  const pubKey = (await fs.promises.readFile(`${keyPath}.pub`, 'utf8')).trim();
159
159
 
160
160
  return { keyPath, privKey, pubKey };