@miraj181/ipingyou 2.0.13 → 2.1.0
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 +8 -1
- package/package.json +7 -6
- package/src/cli.js +216 -0
- package/src/lib/ai/groq.js +116 -0
- package/src/lib/ai/safety.js +114 -0
- package/src/lib/allowlist.js +35 -0
- package/src/lib/broker.js +139 -13
- package/src/lib/cleanup.js +14 -6
- package/src/lib/config.js +8 -3
- package/src/lib/path-browser.js +26 -4
- package/src/lib/platform.js +0 -31
- package/src/lib/session-log.js +93 -0
- package/src/modes/ai.js +627 -0
- package/src/modes/client.js +327 -35
- package/src/modes/doctor.js +293 -0
- package/src/modes/host.js +486 -39
- package/src/server.js +226 -14
package/src/lib/broker.js
CHANGED
|
@@ -2,16 +2,45 @@ import chalk from 'chalk';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import { decrypt, encrypt } from './crypto.js';
|
|
4
4
|
import { createSpinner, cryptoSpinner, networkSpinner } from './animations.js';
|
|
5
|
+
import { logSessionEvent } from './session-log.js';
|
|
6
|
+
|
|
7
|
+
async function fetchWithLog(action, endpoint, options = {}) {
|
|
8
|
+
const method = options.method || 'GET';
|
|
9
|
+
const startedAt = Date.now();
|
|
10
|
+
logSessionEvent('broker_request', { action, method, endpoint });
|
|
11
|
+
try {
|
|
12
|
+
const res = await fetch(endpoint, options);
|
|
13
|
+
logSessionEvent('broker_response', {
|
|
14
|
+
action,
|
|
15
|
+
method,
|
|
16
|
+
endpoint,
|
|
17
|
+
status: res.status,
|
|
18
|
+
ok: res.ok,
|
|
19
|
+
durationMs: Date.now() - startedAt,
|
|
20
|
+
});
|
|
21
|
+
return res;
|
|
22
|
+
} catch (err) {
|
|
23
|
+
logSessionEvent('broker_error', {
|
|
24
|
+
action,
|
|
25
|
+
method,
|
|
26
|
+
endpoint,
|
|
27
|
+
error: err.message,
|
|
28
|
+
durationMs: Date.now() - startedAt,
|
|
29
|
+
}, 'error');
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
5
33
|
|
|
6
34
|
export async function pingBroker(url) {
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const id = setTimeout(() => controller.abort(), 3000);
|
|
7
37
|
try {
|
|
8
|
-
const
|
|
9
|
-
const id = setTimeout(() => controller.abort(), 3000);
|
|
10
|
-
const res = await fetch(`${url}/health`, { signal: controller.signal });
|
|
11
|
-
clearTimeout(id);
|
|
38
|
+
const res = await fetchWithLog('health', `${url}/health`, { signal: controller.signal });
|
|
12
39
|
return res.ok;
|
|
13
40
|
} catch {
|
|
14
41
|
return false;
|
|
42
|
+
} finally {
|
|
43
|
+
clearTimeout(id);
|
|
15
44
|
}
|
|
16
45
|
}
|
|
17
46
|
|
|
@@ -25,7 +54,7 @@ export async function registerWithBroker(brokerUrl, uid, tunnelUrl, password, se
|
|
|
25
54
|
|
|
26
55
|
spinner.text = 'Registering with broker...';
|
|
27
56
|
|
|
28
|
-
const res = await
|
|
57
|
+
const res = await fetchWithLog('register', `${brokerUrl}/register`, {
|
|
29
58
|
method: 'POST',
|
|
30
59
|
headers: { 'Content-Type': 'application/json' },
|
|
31
60
|
body: JSON.stringify({
|
|
@@ -33,43 +62,134 @@ export async function registerWithBroker(brokerUrl, uid, tunnelUrl, password, se
|
|
|
33
62
|
iv: encrypted.iv,
|
|
34
63
|
ciphertext: encrypted.ciphertext,
|
|
35
64
|
salt: encrypted.salt,
|
|
65
|
+
approvalRequired: Boolean(serviceConfig.approvalRequired),
|
|
66
|
+
oneTime: Boolean(serviceConfig.oneTimeSharePath),
|
|
36
67
|
}),
|
|
37
68
|
});
|
|
38
69
|
|
|
39
70
|
if (!res.ok) {
|
|
40
71
|
const data = await res.json().catch(() => ({}));
|
|
72
|
+
logSessionEvent('broker_register_failed', { uid, status: res.status, error: data.error || 'unknown' }, 'error');
|
|
41
73
|
throw new Error(data.error || `HTTP ${res.status}`);
|
|
42
74
|
}
|
|
43
75
|
|
|
76
|
+
const result = await res.json();
|
|
44
77
|
spinner.succeed(`Registered with broker ${chalk.dim(`(${brokerUrl})`)} ${chalk.green('[E2E encrypted]')}`);
|
|
45
|
-
|
|
78
|
+
logSessionEvent('broker_registered', { uid, broker: brokerUrl });
|
|
79
|
+
// Return the host authentication token — needed for all host-only broker operations
|
|
80
|
+
return { success: true, hostToken: result.hostToken || null };
|
|
46
81
|
} catch (err) {
|
|
47
82
|
spinner.fail(`Broker registration failed: ${err.message}`);
|
|
48
83
|
console.error(chalk.red(` ❌ Error: ${err.message}`));
|
|
84
|
+
logSessionEvent('broker_register_error', { uid, broker: brokerUrl, error: err.message }, 'error');
|
|
49
85
|
console.log(chalk.yellow(' ⚠️ Remote clients won\'t be able to find you without the broker.'));
|
|
50
86
|
console.log(chalk.dim(' Share the tunnel URL directly if needed.'));
|
|
51
|
-
return false;
|
|
87
|
+
return { success: false, hostToken: null };
|
|
52
88
|
}
|
|
53
89
|
}
|
|
54
90
|
|
|
55
|
-
export async function
|
|
91
|
+
export async function requestHostApproval(brokerUrl, uid, password, details) {
|
|
92
|
+
const encrypted = encrypt(JSON.stringify(details), password);
|
|
93
|
+
const res = await fetchWithLog('approval_request', `${brokerUrl}/approval-request/${uid}`, {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: { 'Content-Type': 'application/json' },
|
|
96
|
+
body: JSON.stringify(encrypted),
|
|
97
|
+
});
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
const data = await res.json().catch(() => ({}));
|
|
100
|
+
logSessionEvent('broker_approval_request_failed', { uid, status: res.status, error: data.error || 'unknown' }, 'error');
|
|
101
|
+
throw new Error(data.error || `HTTP ${res.status}`);
|
|
102
|
+
}
|
|
103
|
+
return res.json();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function waitForApproval(brokerUrl, uid, requestId, timeoutMs = 300000) {
|
|
107
|
+
const startedAt = Date.now();
|
|
108
|
+
logSessionEvent('approval_wait_started', { uid, requestId, timeoutMs });
|
|
109
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
110
|
+
const res = await fetchWithLog('approval_status', `${brokerUrl}/approval-status/${uid}/${requestId}`);
|
|
111
|
+
if (res.ok) {
|
|
112
|
+
const data = await res.json();
|
|
113
|
+
if (data.status === 'approved') {
|
|
114
|
+
logSessionEvent('approval_granted', { uid, requestId });
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
if (data.status === 'denied') {
|
|
118
|
+
logSessionEvent('approval_denied', { uid, requestId });
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
123
|
+
}
|
|
124
|
+
logSessionEvent('approval_timeout', { uid, requestId, timeoutMs }, 'warn');
|
|
125
|
+
throw new Error('Timed out waiting for host approval');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function fetchApprovalRequests(brokerUrl, uid, hostToken) {
|
|
129
|
+
const res = await fetchWithLog('approval_requests', `${brokerUrl}/approval-requests/${uid}`, {
|
|
130
|
+
headers: hostToken ? { 'x-host-token': hostToken } : {},
|
|
131
|
+
});
|
|
132
|
+
if (!res.ok) {
|
|
133
|
+
const data = await res.json().catch(() => ({}));
|
|
134
|
+
logSessionEvent('broker_approval_fetch_failed', { uid, status: res.status, error: data.error || 'unknown' }, 'error');
|
|
135
|
+
throw new Error(data.error || `HTTP ${res.status}`);
|
|
136
|
+
}
|
|
137
|
+
return res.json();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function decideApprovalRequest(brokerUrl, uid, requestId, decision, hostToken) {
|
|
141
|
+
const res = await fetchWithLog('approval_decision', `${brokerUrl}/approval-requests/${uid}/${requestId}/${decision}`, {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers: hostToken ? { 'x-host-token': hostToken } : {},
|
|
144
|
+
});
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
const data = await res.json().catch(() => ({}));
|
|
147
|
+
logSessionEvent('broker_approval_decision_failed', { uid, requestId, decision, status: res.status, error: data.error || 'unknown' }, 'error');
|
|
148
|
+
throw new Error(data.error || `HTTP ${res.status}`);
|
|
149
|
+
}
|
|
150
|
+
logSessionEvent('approval_decision_sent', { uid, requestId, decision });
|
|
151
|
+
return res.json();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function revokeUID(brokerUrl, uid, hostToken) {
|
|
155
|
+
try {
|
|
156
|
+
await fetchWithLog('revoke', `${brokerUrl}/revoke/${uid}`, {
|
|
157
|
+
method: 'DELETE',
|
|
158
|
+
headers: hostToken ? { 'x-host-token': hostToken } : {},
|
|
159
|
+
});
|
|
160
|
+
} catch (err) {
|
|
161
|
+
logSessionEvent('broker_revoke_failed', { uid, broker: brokerUrl, error: err.message }, 'warn');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function resolveUID(brokerUrl, uid, password, silent = false, requestId = null) {
|
|
56
166
|
const spinner = !silent ? createSpinner(`Resolving UID ${chalk.cyan(uid)}...`, networkSpinner).start() : null;
|
|
57
167
|
|
|
58
168
|
try {
|
|
59
|
-
const
|
|
169
|
+
const suffix = requestId ? `?requestId=${encodeURIComponent(requestId)}` : '';
|
|
170
|
+
const res = await fetchWithLog('resolve', `${brokerUrl}/resolve/${uid}${suffix}`);
|
|
60
171
|
|
|
61
172
|
if (res.status === 404) {
|
|
62
173
|
if (spinner) spinner.fail('UID not found — the host may not be online or the session expired');
|
|
63
174
|
else console.error(chalk.red(' ❌ UID not found or expired.'));
|
|
175
|
+
logSessionEvent('broker_resolve_missing', { uid, status: 404 }, 'warn');
|
|
64
176
|
return null;
|
|
65
177
|
}
|
|
66
178
|
if (res.status === 410) {
|
|
67
179
|
if (spinner) spinner.fail('UID has expired — ask the host for a new session');
|
|
68
180
|
else console.error(chalk.red(' ❌ UID expired.'));
|
|
181
|
+
logSessionEvent('broker_resolve_expired', { uid, status: 410 }, 'warn');
|
|
69
182
|
return null;
|
|
70
183
|
}
|
|
184
|
+
if (res.status === 423) {
|
|
185
|
+
if (spinner) spinner.info('Host approval is required — submitting access request...');
|
|
186
|
+
else console.log(chalk.yellow(' ⏳ Host approval required.'));
|
|
187
|
+
logSessionEvent('broker_resolve_needs_approval', { uid, status: 423 }, 'warn');
|
|
188
|
+
return { needsApproval: true };
|
|
189
|
+
}
|
|
71
190
|
if (!res.ok) {
|
|
72
191
|
const data = await res.json().catch(() => ({}));
|
|
192
|
+
logSessionEvent('broker_resolve_failed', { uid, status: res.status, error: data.error || 'unknown' }, 'error');
|
|
73
193
|
throw new Error(data.error || `HTTP ${res.status}`);
|
|
74
194
|
}
|
|
75
195
|
|
|
@@ -77,6 +197,7 @@ export async function resolveUID(brokerUrl, uid, password, silent = false) {
|
|
|
77
197
|
|
|
78
198
|
if (!data.iv || !data.ciphertext || !data.salt) {
|
|
79
199
|
if (spinner) spinner.fail('Broker returned invalid response — missing encrypted data or salt');
|
|
200
|
+
logSessionEvent('broker_resolve_invalid', { uid }, 'error');
|
|
80
201
|
return null;
|
|
81
202
|
}
|
|
82
203
|
|
|
@@ -89,6 +210,7 @@ export async function resolveUID(brokerUrl, uid, password, silent = false) {
|
|
|
89
210
|
} catch {
|
|
90
211
|
if (spinner) spinner.fail('Decryption failed — incorrect password or corrupted data');
|
|
91
212
|
if (!spinner) console.error(chalk.red(' ❌ Error: Could not decrypt tunnel data. Incorrect password.'));
|
|
213
|
+
logSessionEvent('broker_decrypt_failed', { uid }, 'warn');
|
|
92
214
|
return null;
|
|
93
215
|
}
|
|
94
216
|
|
|
@@ -104,18 +226,21 @@ export async function resolveUID(brokerUrl, uid, password, silent = false) {
|
|
|
104
226
|
|
|
105
227
|
if (!payloadConfig.url.startsWith('https://')) {
|
|
106
228
|
if (spinner) spinner.fail('Decrypted data is not a valid tunnel URL (incorrect password)');
|
|
229
|
+
logSessionEvent('broker_decrypt_invalid_url', { uid }, 'warn');
|
|
107
230
|
return null;
|
|
108
231
|
}
|
|
109
232
|
|
|
110
233
|
if (spinner) spinner.succeed(`Resolved: ${chalk.dim(payloadConfig.url)} ${chalk.green('[decrypted locally]')}`);
|
|
234
|
+
logSessionEvent('broker_resolve_success', { uid, type: payloadConfig.type || 'ssh' });
|
|
111
235
|
return payloadConfig;
|
|
112
236
|
} catch (err) {
|
|
113
237
|
if (spinner) spinner.fail(`Broker lookup failed: ${err.message}`);
|
|
238
|
+
logSessionEvent('broker_resolve_error', { uid, error: err.message }, 'error');
|
|
114
239
|
return null;
|
|
115
240
|
}
|
|
116
241
|
}
|
|
117
242
|
|
|
118
|
-
export async function pushTelemetry(brokerUrl, uid, password, username) {
|
|
243
|
+
export async function pushTelemetry(brokerUrl, uid, password, username, action = 'connected') {
|
|
119
244
|
try {
|
|
120
245
|
let publicIp = 'Unknown';
|
|
121
246
|
try {
|
|
@@ -128,17 +253,18 @@ export async function pushTelemetry(brokerUrl, uid, password, username) {
|
|
|
128
253
|
os: `${os.type()} ${os.release()} (${os.arch()})`,
|
|
129
254
|
cpu: os.cpus()[0]?.model || 'Unknown CPU',
|
|
130
255
|
ram: `${Math.round(os.totalmem() / 1024 / 1024 / 1024)} GB`,
|
|
256
|
+
action,
|
|
131
257
|
time: new Date().toLocaleTimeString()
|
|
132
258
|
};
|
|
133
259
|
|
|
134
260
|
const { iv, ciphertext, salt } = encrypt(JSON.stringify(telemetry), password);
|
|
135
261
|
|
|
136
|
-
await
|
|
262
|
+
await fetchWithLog('telemetry', `${brokerUrl}/client-info/${uid}`, {
|
|
137
263
|
method: 'POST',
|
|
138
264
|
headers: { 'Content-Type': 'application/json' },
|
|
139
265
|
body: JSON.stringify({ iv, ciphertext, salt }),
|
|
140
266
|
});
|
|
141
|
-
} catch {
|
|
142
|
-
|
|
267
|
+
} catch (err) {
|
|
268
|
+
logSessionEvent('telemetry_failed', { uid, error: err.message }, 'warn');
|
|
143
269
|
}
|
|
144
270
|
}
|
package/src/lib/cleanup.js
CHANGED
|
@@ -24,6 +24,9 @@ let _revokeUID = null;
|
|
|
24
24
|
/** @type {string|null} — Broker URL for revocation */
|
|
25
25
|
let _brokerUrl = null;
|
|
26
26
|
|
|
27
|
+
/** @type {(() => string|null)|null} — Getter for host auth token */
|
|
28
|
+
let _getHostToken = null;
|
|
29
|
+
|
|
27
30
|
/** @type {Array<() => Promise<void>|void>} — Custom cleanup hooks */
|
|
28
31
|
const _cleanupHooks = [];
|
|
29
32
|
let cleanedUp = false;
|
|
@@ -56,10 +59,12 @@ export function untrackPID(pid) {
|
|
|
56
59
|
* Set UID + broker URL for automatic revocation on shutdown.
|
|
57
60
|
* @param {string} uid
|
|
58
61
|
* @param {string} brokerUrl
|
|
62
|
+
* @param {() => string|null} [getHostToken]
|
|
59
63
|
*/
|
|
60
|
-
export function setRevokeOnExit(uid, brokerUrl) {
|
|
64
|
+
export function setRevokeOnExit(uid, brokerUrl, getHostToken = null) {
|
|
61
65
|
_revokeUID = uid;
|
|
62
66
|
_brokerUrl = brokerUrl;
|
|
67
|
+
_getHostToken = getHostToken;
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
/**
|
|
@@ -110,7 +115,14 @@ export async function cleanupAll() {
|
|
|
110
115
|
// Revoke UID from broker
|
|
111
116
|
if (_revokeUID && _brokerUrl) {
|
|
112
117
|
try {
|
|
113
|
-
const
|
|
118
|
+
const headers = {};
|
|
119
|
+
const token = _getHostToken ? _getHostToken() : null;
|
|
120
|
+
if (token) headers['x-host-token'] = token;
|
|
121
|
+
|
|
122
|
+
const res = await fetch(`${_brokerUrl}/revoke/${_revokeUID}`, {
|
|
123
|
+
method: 'DELETE',
|
|
124
|
+
headers
|
|
125
|
+
});
|
|
114
126
|
if (res.ok) {
|
|
115
127
|
console.log(chalk.dim(` Revoked UID ${_revokeUID} from broker`));
|
|
116
128
|
}
|
|
@@ -164,10 +176,6 @@ export function installShutdownHandlers() {
|
|
|
164
176
|
* Get count of tracked PIDs.
|
|
165
177
|
* @returns {number}
|
|
166
178
|
*/
|
|
167
|
-
export function getTrackedCount() {
|
|
168
|
-
return trackedPIDs.size;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
179
|
/**
|
|
172
180
|
* Execute Panic Mode (Self-Destruct)
|
|
173
181
|
* Wipes all configs, keys, and forcefully kills associated processes.
|
package/src/lib/config.js
CHANGED
|
@@ -7,10 +7,14 @@ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
|
7
7
|
|
|
8
8
|
function ensureConfig() {
|
|
9
9
|
if (!fs.existsSync(CONFIG_DIR)) {
|
|
10
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
10
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
11
|
+
} else {
|
|
12
|
+
try { fs.chmodSync(CONFIG_DIR, 0o700); } catch { }
|
|
11
13
|
}
|
|
12
14
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
13
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ aliases: {}, settings: {} }, null, 2));
|
|
15
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ aliases: {}, settings: {} }, null, 2), { mode: 0o600 });
|
|
16
|
+
} else {
|
|
17
|
+
try { fs.chmodSync(CONFIG_FILE, 0o600); } catch { }
|
|
14
18
|
}
|
|
15
19
|
}
|
|
16
20
|
|
|
@@ -26,7 +30,8 @@ export function getConfig() {
|
|
|
26
30
|
export function saveConfig(config) {
|
|
27
31
|
try {
|
|
28
32
|
ensureConfig();
|
|
29
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
33
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
34
|
+
try { fs.chmodSync(CONFIG_FILE, 0o600); } catch { }
|
|
30
35
|
} catch (err) {
|
|
31
36
|
throw new Error(`Could not save config: ${err.message}`);
|
|
32
37
|
}
|
package/src/lib/path-browser.js
CHANGED
|
@@ -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', '
|
|
120
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
112
121
|
reject: false,
|
|
113
122
|
});
|
|
114
123
|
|
|
115
124
|
if (result.exitCode !== 0) {
|
|
116
|
-
|
|
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);
|
|
@@ -136,7 +146,7 @@ async function listRemoteDirectory(username, hostname, privateKeyPath, remoteDir
|
|
|
136
146
|
return { pwd, entries };
|
|
137
147
|
}
|
|
138
148
|
|
|
139
|
-
export async function promptRemotePath(username, hostname, privateKeyPath, purpose) {
|
|
149
|
+
export async function promptRemotePath(username, hostname, privateKeyPath, purpose, startDir = '~') {
|
|
140
150
|
const browseLabel = purpose === 'source'
|
|
141
151
|
? 'Browse host files interactively'
|
|
142
152
|
: 'Browse host folders interactively';
|
|
@@ -169,13 +179,25 @@ export async function promptRemotePath(username, hostname, privateKeyPath, purpo
|
|
|
169
179
|
return remotePath.trim();
|
|
170
180
|
}
|
|
171
181
|
|
|
172
|
-
let currentDir = '~';
|
|
182
|
+
let currentDir = startDir || '~';
|
|
173
183
|
while (true) {
|
|
174
184
|
let listing;
|
|
175
185
|
try {
|
|
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/platform.js
CHANGED
|
@@ -410,37 +410,6 @@ async function installOpenSSH(osInfo) {
|
|
|
410
410
|
|
|
411
411
|
// ─── Main Dependency Check ───────────────────────────────────
|
|
412
412
|
|
|
413
|
-
/**
|
|
414
|
-
* Install a dependency on the current platform.
|
|
415
|
-
* @param {string} pkg — package name (e.g. 'openssh-server', 'cloudflared')
|
|
416
|
-
* @param {'debian'|'arch'|'fedora'|'mac'|'windows'} distro
|
|
417
|
-
*/
|
|
418
|
-
export async function installDependency(pkg, distro) {
|
|
419
|
-
const spinner = ora(`Installing ${chalk.cyan(pkg)}...`).start();
|
|
420
|
-
|
|
421
|
-
const commands = {
|
|
422
|
-
debian: `sudo apt-get install -y ${pkg}`,
|
|
423
|
-
arch: `sudo pacman -S --noconfirm ${pkg}`,
|
|
424
|
-
fedora: `sudo dnf install -y ${pkg}`,
|
|
425
|
-
mac: `brew install ${pkg}`,
|
|
426
|
-
};
|
|
427
|
-
|
|
428
|
-
const cmd = commands[distro];
|
|
429
|
-
if (!cmd) {
|
|
430
|
-
spinner.fail(`No auto-install command for ${distro}. Please install ${pkg} manually.`);
|
|
431
|
-
return false;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
try {
|
|
435
|
-
await execaCommand(cmd, { stdio: 'inherit' });
|
|
436
|
-
spinner.succeed(`${chalk.green(pkg)} installed successfully`);
|
|
437
|
-
return true;
|
|
438
|
-
} catch (err) {
|
|
439
|
-
spinner.fail(`Failed to install ${pkg}: ${err.message}`);
|
|
440
|
-
return false;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
413
|
/**
|
|
445
414
|
* Run full dependency check with auto-bootstrap.
|
|
446
415
|
*
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { redactSensitive } from './ai/safety.js';
|
|
5
|
+
|
|
6
|
+
const LOG_DIR = path.join(os.homedir(), '.ipingyou', 'logs');
|
|
7
|
+
const LOG_FILE = path.join(LOG_DIR, 'session-events.jsonl');
|
|
8
|
+
const SESSION_LOG_DIR = path.join(os.tmpdir(), 'ipingyou-session-logs');
|
|
9
|
+
|
|
10
|
+
let sessionLogPath = null;
|
|
11
|
+
let sessionLogDisabled = false;
|
|
12
|
+
let cleanupRegistered = false;
|
|
13
|
+
|
|
14
|
+
function sanitize(value) {
|
|
15
|
+
if (typeof value === 'string') return redactSensitive(value);
|
|
16
|
+
if (Array.isArray(value)) return value.map(sanitize);
|
|
17
|
+
if (value && typeof value === 'object') {
|
|
18
|
+
return Object.fromEntries(
|
|
19
|
+
Object.entries(value)
|
|
20
|
+
.filter(([key]) => !/(password|privateKey|tunnelUrl|url|secret|token|apiKey)/i.test(key))
|
|
21
|
+
.map(([key, item]) => [key, sanitize(item)])
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function initSessionLog(scope = 'session') {
|
|
28
|
+
if (sessionLogPath || sessionLogDisabled) return sessionLogPath;
|
|
29
|
+
try {
|
|
30
|
+
fs.mkdirSync(SESSION_LOG_DIR, { recursive: true, mode: 0o700 });
|
|
31
|
+
sessionLogPath = path.join(
|
|
32
|
+
SESSION_LOG_DIR,
|
|
33
|
+
`ipingyou-${scope}-${Date.now()}-${process.pid}.log`
|
|
34
|
+
);
|
|
35
|
+
fs.writeFileSync(sessionLogPath, '', { mode: 0o600 });
|
|
36
|
+
if (!cleanupRegistered) {
|
|
37
|
+
process.on('exit', () => cleanupSessionLog());
|
|
38
|
+
cleanupRegistered = true;
|
|
39
|
+
}
|
|
40
|
+
logSessionEvent('session_start', { scope, pid: process.pid, node: process.version });
|
|
41
|
+
return sessionLogPath;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
sessionLogDisabled = true;
|
|
44
|
+
console.error(`Session log setup failed: ${err.message}`);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getSessionLogPath() {
|
|
50
|
+
return sessionLogPath;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function logSessionEvent(type, details = {}, level = 'info') {
|
|
54
|
+
if (!sessionLogPath || sessionLogDisabled) return;
|
|
55
|
+
const entry = {
|
|
56
|
+
time: new Date().toISOString(),
|
|
57
|
+
level,
|
|
58
|
+
type,
|
|
59
|
+
details: sanitize(details),
|
|
60
|
+
};
|
|
61
|
+
try {
|
|
62
|
+
fs.appendFileSync(sessionLogPath, `${JSON.stringify(entry)}\n`, { mode: 0o600 });
|
|
63
|
+
} catch (err) {
|
|
64
|
+
sessionLogDisabled = true;
|
|
65
|
+
console.error(`Session log write failed: ${err.message}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function cleanupSessionLog() {
|
|
70
|
+
if (!sessionLogPath) return;
|
|
71
|
+
const target = sessionLogPath;
|
|
72
|
+
sessionLogPath = null;
|
|
73
|
+
try {
|
|
74
|
+
if (fs.existsSync(target)) fs.unlinkSync(target);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error(`Session log cleanup failed: ${err.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function recordEvent(type, details = {}) {
|
|
81
|
+
try {
|
|
82
|
+
fs.mkdirSync(LOG_DIR, { recursive: true, mode: 0o700 });
|
|
83
|
+
const event = {
|
|
84
|
+
time: new Date().toISOString(),
|
|
85
|
+
type,
|
|
86
|
+
details: sanitize(details),
|
|
87
|
+
};
|
|
88
|
+
fs.appendFileSync(LOG_FILE, `${JSON.stringify(event)}\n`, { mode: 0o600 });
|
|
89
|
+
logSessionEvent(type, details);
|
|
90
|
+
} catch {
|
|
91
|
+
// Session recording is best-effort.
|
|
92
|
+
}
|
|
93
|
+
}
|