@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/README.md
CHANGED
|
@@ -22,6 +22,11 @@ No firewalls to configure. No port forwarding. No plaintext leakage.
|
|
|
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
|
* ๐จ **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.
|
|
24
24
|
* ๐ป **Daemonization**: Run `ipingyou service install` to quietly install and run the Host listener in the background (survives system reboots using PM2).
|
|
25
|
+
* ๐งญ **Approval Gate**: Require the Host to explicitly approve clients before they receive tunnel/key material.
|
|
26
|
+
* ๐ฆ **One-Time File Share**: Serve a single file/folder over SCP and revoke after use.
|
|
27
|
+
* ๐ **HTTP & TCP Exposure**: Share a local web app or any TCP service (DB/RDP/VNC) beyond SSH.
|
|
28
|
+
* ๐ **Shared Drop Folder**: Auto-prepared dropbox folder for safe file transfers (macOS friendly).
|
|
29
|
+
* ๐งพ **Live Session Logs**: Host/client/broker write ephemeral per-session logs with actions and request/response status, removed on exit.
|
|
25
30
|
|
|
26
31
|
---
|
|
27
32
|
|
|
@@ -100,13 +105,15 @@ These alerts (e.g., "AI-detected potential code anomaly", "Shell access", "Netwo
|
|
|
100
105
|
| `ipingyou` | Interactive CLI dashboard wizard. |
|
|
101
106
|
| `ipingyou host` | Start hosting and exposing your local machine securely. |
|
|
102
107
|
| `ipingyou connect -u <UID>` | Connect directly to a specific UID. |
|
|
108
|
+
| `ipingyou ai` | Groq-powered task assistant with guarded local/remote tools. |
|
|
109
|
+
| `ipingyou doctor` | Diagnostics for dependencies, SSH, broker, SCP, AI, and tests. |
|
|
103
110
|
| `ipingyou panic` | ๐จ Self-destruct mode. Wipes configs, memory, and kills all processes. |
|
|
104
111
|
| `ipingyou service install` | ๐ป Installs Host mode as an always-on background daemon. |
|
|
105
112
|
| `ipingyou service stop` | Stops and removes the background daemon. |
|
|
113
|
+
| `ipingyou service status` | Shows background daemon status. |
|
|
106
114
|
|
|
107
115
|
---
|
|
108
116
|
|
|
109
117
|
## ๐ License
|
|
110
118
|
|
|
111
119
|
[MIT License](LICENSE) ยฉ Sk Mirajul Islam
|
|
112
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraj181/ipingyou",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "SecureLink-CLI โ Secure peer-to-peer remote access via SSH & Cloudflare Tunnels",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"start": "node src/server.js",
|
|
20
20
|
"dev:cli": "node src/cli.js",
|
|
21
|
+
"test": "node test/test_ai_helpers.js",
|
|
21
22
|
"prepublishOnly": "node -e \"require('fs').accessSync('LICENSE')\" && node src/cli.js --help"
|
|
22
23
|
},
|
|
23
24
|
"keywords": [
|
|
@@ -44,16 +45,16 @@
|
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
47
|
"chalk": "^5.3.0",
|
|
47
|
-
"commander": "^
|
|
48
|
+
"commander": "^14.0.3",
|
|
48
49
|
"execa": "^9.5.2",
|
|
49
|
-
"inquirer": "^
|
|
50
|
+
"inquirer": "^13.4.3",
|
|
50
51
|
"nanoid": "^5.0.9",
|
|
51
52
|
"open": "^11.0.0",
|
|
52
|
-
"ora": "^
|
|
53
|
+
"ora": "^9.4.0",
|
|
53
54
|
"tree-kill": "^1.2.2",
|
|
54
55
|
"ws": "^8.20.1",
|
|
55
|
-
"express": "^
|
|
56
|
-
"express-rate-limit": "^
|
|
56
|
+
"express": "^5.2.1",
|
|
57
|
+
"express-rate-limit": "^8.5.2",
|
|
57
58
|
"helmet": "^7.1.0"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
package/src/cli.js
CHANGED
|
@@ -29,6 +29,8 @@ import { detectOS, checkDependencies } from './lib/platform.js';
|
|
|
29
29
|
import { cleanupAll, installShutdownHandlers, executePanicMode } from './lib/cleanup.js';
|
|
30
30
|
import { startHostMode } from './modes/host.js';
|
|
31
31
|
import { startClientMode } from './modes/client.js';
|
|
32
|
+
import { startAIMode } from './modes/ai.js';
|
|
33
|
+
import { startDoctorMode } from './modes/doctor.js';
|
|
32
34
|
|
|
33
35
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
34
36
|
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8'));
|
|
@@ -112,6 +114,8 @@ function showRichHelp() {
|
|
|
112
114
|
console.log(` ${chalk.green('host')} : Generates a secure session UID and exposes your local machine.`);
|
|
113
115
|
console.log(` ${chalk.blue('connect')} : Prompts for a UID to connect to a remote host.`);
|
|
114
116
|
console.log(` ${chalk.dim('Supports Interactive SSH Shell & SCP File Transfers')}`);
|
|
117
|
+
console.log(` ${chalk.magenta('ai')} : Groq-powered task assistant with guarded local/remote tools.`);
|
|
118
|
+
console.log(` ${chalk.yellow('doctor')} : Run diagnostics for dependencies, SSH, broker, SCP, AI, and tests.`);
|
|
115
119
|
console.log('');
|
|
116
120
|
|
|
117
121
|
console.log(chalk.bold.white(' ๐ Security Architecture:'));
|
|
@@ -132,8 +136,12 @@ function showRichHelp() {
|
|
|
132
136
|
console.log(` $ npx ipingyou ${chalk.dim('# Interactive wizard (Recommended)')}`);
|
|
133
137
|
console.log(` $ npx ipingyou host ${chalk.dim('# Quick start as Host')}`);
|
|
134
138
|
console.log(` $ npx ipingyou connect ${chalk.dim('# Quick start as Client')}`);
|
|
139
|
+
console.log(` $ npx ipingyou ai ${chalk.dim('# Start AI task assistant')}`);
|
|
140
|
+
console.log(` $ npx ipingyou doctor ${chalk.dim('# Diagnose local setup')}`);
|
|
135
141
|
console.log(` $ npx ipingyou panic ${chalk.dim('# Self-destruct and wipe memory/traces')}`);
|
|
136
142
|
console.log(` $ npx ipingyou service install ${chalk.dim('# Install Host mode as a background daemon')}`);
|
|
143
|
+
console.log(` $ npx ipingyou allowlist ${chalk.dim('# Manage AI command allowlist')}`);
|
|
144
|
+
console.log(` $ npx ipingyou history ${chalk.dim('# View session event logs')}`);
|
|
137
145
|
console.log('');
|
|
138
146
|
}
|
|
139
147
|
|
|
@@ -192,6 +200,14 @@ async function interactiveMode() {
|
|
|
192
200
|
name: `${chalk.blue('๐ Access a Remote Machine')} ${chalk.dim('โ Connect to a host via their UID (SSH/SCP)')}`,
|
|
193
201
|
value: 'client',
|
|
194
202
|
},
|
|
203
|
+
{
|
|
204
|
+
name: `${chalk.magenta('๐ค AI Task Assistant')} ${chalk.dim('โ Use Groq LLMs with guarded local/remote tools')}`,
|
|
205
|
+
value: 'ai',
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: `${chalk.yellow('๐ฉบ Run Doctor')} ${chalk.dim('โ Diagnose setup and project health')}`,
|
|
209
|
+
value: 'doctor',
|
|
210
|
+
},
|
|
195
211
|
new inquirer.Separator(),
|
|
196
212
|
{
|
|
197
213
|
name: `${chalk.magenta('๐ Help / Information')} ${chalk.dim('โ Learn how iPingYou works')}`,
|
|
@@ -208,6 +224,12 @@ async function interactiveMode() {
|
|
|
208
224
|
case 'client':
|
|
209
225
|
await startClientMode();
|
|
210
226
|
break;
|
|
227
|
+
case 'ai':
|
|
228
|
+
await startAIMode();
|
|
229
|
+
break;
|
|
230
|
+
case 'doctor':
|
|
231
|
+
await startDoctorMode();
|
|
232
|
+
break;
|
|
211
233
|
case 'help':
|
|
212
234
|
showRichHelp();
|
|
213
235
|
break;
|
|
@@ -265,6 +287,40 @@ program
|
|
|
265
287
|
}
|
|
266
288
|
});
|
|
267
289
|
|
|
290
|
+
program
|
|
291
|
+
.command('ai')
|
|
292
|
+
.description('Start AI mode โ Groq-powered local/remote task assistant with guarded tools')
|
|
293
|
+
.action(async () => {
|
|
294
|
+
try {
|
|
295
|
+
const opts = program.opts();
|
|
296
|
+
if (opts.broker) process.env.BROKER_URL = opts.broker;
|
|
297
|
+
|
|
298
|
+
showBanner();
|
|
299
|
+
showSystemInfo();
|
|
300
|
+
installShutdownHandlers();
|
|
301
|
+
await startAIMode();
|
|
302
|
+
} catch (err) {
|
|
303
|
+
fatal('ai', err);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
program
|
|
308
|
+
.command('doctor')
|
|
309
|
+
.description('Run non-invasive diagnostics for dependencies, SSH, broker, SCP, AI, and tests')
|
|
310
|
+
.option('--full', 'Run full diagnostics, including broker integration if a local broker is running')
|
|
311
|
+
.action(async (commandOptions) => {
|
|
312
|
+
try {
|
|
313
|
+
const opts = program.opts();
|
|
314
|
+
if (opts.broker) process.env.BROKER_URL = opts.broker;
|
|
315
|
+
|
|
316
|
+
showBanner();
|
|
317
|
+
showSystemInfo();
|
|
318
|
+
await startDoctorMode({ full: commandOptions.full });
|
|
319
|
+
} catch (err) {
|
|
320
|
+
fatal('doctor', err);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
268
324
|
program
|
|
269
325
|
.command('panic')
|
|
270
326
|
.description('๐จ Self-destruct mode: wipe all configs, kill tunnels, and remove traces')
|
|
@@ -310,6 +366,166 @@ program
|
|
|
310
366
|
}
|
|
311
367
|
});
|
|
312
368
|
|
|
369
|
+
program
|
|
370
|
+
.command('allowlist')
|
|
371
|
+
.description('Manage AI command allowlist โ add, remove, or list safe regex patterns')
|
|
372
|
+
.argument('[action]', 'Action: list, add, remove', 'list')
|
|
373
|
+
.argument('[pattern]', 'Regex pattern to add or remove')
|
|
374
|
+
.action(async (action, pattern) => {
|
|
375
|
+
try {
|
|
376
|
+
showBanner();
|
|
377
|
+
const fs = await import('node:fs');
|
|
378
|
+
const os = await import('node:os');
|
|
379
|
+
const path = await import('node:path');
|
|
380
|
+
const { ensureAllowlistFile, getAllowlistRegexes } = await import('./lib/allowlist.js');
|
|
381
|
+
|
|
382
|
+
const allowlistPath = ensureAllowlistFile();
|
|
383
|
+
|
|
384
|
+
if (action === 'list' || !action) {
|
|
385
|
+
const patterns = getAllowlistRegexes();
|
|
386
|
+
console.log(chalk.bold.cyan(' ๐ AI Command Allowlist'));
|
|
387
|
+
console.log(chalk.dim(' โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'));
|
|
388
|
+
console.log(chalk.dim(` File: ${allowlistPath}`));
|
|
389
|
+
console.log('');
|
|
390
|
+
if (patterns.length === 0) {
|
|
391
|
+
console.log(chalk.dim(' No patterns configured.'));
|
|
392
|
+
} else {
|
|
393
|
+
try {
|
|
394
|
+
const raw = JSON.parse(fs.readFileSync(allowlistPath, 'utf8'));
|
|
395
|
+
raw.forEach((p, i) => {
|
|
396
|
+
console.log(` ${chalk.green(i + 1)}. ${chalk.white(p)}`);
|
|
397
|
+
});
|
|
398
|
+
} catch (e) {
|
|
399
|
+
console.log(chalk.red(' โ Allowlist file is corrupted.'));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
console.log('');
|
|
403
|
+
console.log(chalk.dim(' Usage: ipingyou allowlist add "^my-safe-command"'));
|
|
404
|
+
console.log(chalk.dim(' ipingyou allowlist remove "^my-safe-command"'));
|
|
405
|
+
} else if (action === 'add') {
|
|
406
|
+
if (!pattern) {
|
|
407
|
+
console.log(chalk.red(' โ Missing pattern. Usage: ipingyou allowlist add "^my-command"'));
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
// Validate regex
|
|
411
|
+
try { new RegExp(pattern); } catch (err) {
|
|
412
|
+
console.log(chalk.red(` โ Invalid regex: ${err.message}`));
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
let raw = [];
|
|
416
|
+
try { raw = JSON.parse(fs.readFileSync(allowlistPath, 'utf8')); } catch {}
|
|
417
|
+
if (raw.includes(pattern)) {
|
|
418
|
+
console.log(chalk.yellow(` โ ๏ธ Pattern already exists: ${pattern}`));
|
|
419
|
+
} else {
|
|
420
|
+
raw.push(pattern);
|
|
421
|
+
fs.writeFileSync(allowlistPath, JSON.stringify(raw, null, 2), { mode: 0o600 });
|
|
422
|
+
console.log(chalk.green(` โ
Added: ${chalk.white(pattern)}`));
|
|
423
|
+
}
|
|
424
|
+
} else if (action === 'remove') {
|
|
425
|
+
if (!pattern) {
|
|
426
|
+
console.log(chalk.red(' โ Missing pattern. Usage: ipingyou allowlist remove "^my-command"'));
|
|
427
|
+
process.exit(1);
|
|
428
|
+
}
|
|
429
|
+
let raw = [];
|
|
430
|
+
try { raw = JSON.parse(fs.readFileSync(allowlistPath, 'utf8')); } catch {}
|
|
431
|
+
const idx = raw.indexOf(pattern);
|
|
432
|
+
if (idx === -1) {
|
|
433
|
+
console.log(chalk.yellow(` โ ๏ธ Pattern not found: ${pattern}`));
|
|
434
|
+
} else {
|
|
435
|
+
raw.splice(idx, 1);
|
|
436
|
+
fs.writeFileSync(allowlistPath, JSON.stringify(raw, null, 2), { mode: 0o600 });
|
|
437
|
+
console.log(chalk.green(` โ
Removed: ${chalk.white(pattern)}`));
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
console.log(chalk.red(` โ Unknown action: ${action}. Use list, add, or remove.`));
|
|
441
|
+
}
|
|
442
|
+
} catch (err) {
|
|
443
|
+
fatal('allowlist', err);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
program
|
|
448
|
+
.command('history')
|
|
449
|
+
.description('View session event logs from ~/.ipingyou/logs')
|
|
450
|
+
.option('-n, --lines <count>', 'Number of recent events to show', '25')
|
|
451
|
+
.option('--type <type>', 'Filter by event type (e.g. ssh, scp, ai)')
|
|
452
|
+
.option('--json', 'Output raw JSON lines')
|
|
453
|
+
.action(async (commandOptions) => {
|
|
454
|
+
try {
|
|
455
|
+
showBanner();
|
|
456
|
+
const fs = await import('node:fs');
|
|
457
|
+
const os = await import('node:os');
|
|
458
|
+
const pathMod = await import('node:path');
|
|
459
|
+
|
|
460
|
+
const logFile = pathMod.default.join(os.default.homedir(), '.ipingyou', 'logs', 'session-events.jsonl');
|
|
461
|
+
|
|
462
|
+
console.log(chalk.bold.cyan(' ๐ Session Event History'));
|
|
463
|
+
console.log(chalk.dim(' โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'));
|
|
464
|
+
|
|
465
|
+
if (!fs.existsSync(logFile)) {
|
|
466
|
+
console.log(chalk.dim(' No session history found yet.'));
|
|
467
|
+
console.log(chalk.dim(` Events are recorded to: ${logFile}`));
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const raw = fs.readFileSync(logFile, 'utf8').trim();
|
|
472
|
+
if (!raw) {
|
|
473
|
+
console.log(chalk.dim(' Log file is empty.'));
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
let events = raw.split('\n').map(line => {
|
|
478
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
479
|
+
}).filter(Boolean);
|
|
480
|
+
|
|
481
|
+
// Filter by type if specified
|
|
482
|
+
if (commandOptions.type) {
|
|
483
|
+
const filter = commandOptions.type.toLowerCase();
|
|
484
|
+
events = events.filter(e => (e.type || '').toLowerCase().includes(filter));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Take last N events
|
|
488
|
+
const count = parseInt(commandOptions.lines) || 25;
|
|
489
|
+
events = events.slice(-count);
|
|
490
|
+
|
|
491
|
+
if (events.length === 0) {
|
|
492
|
+
console.log(chalk.dim(' No matching events found.'));
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
console.log(chalk.dim(` Showing last ${events.length} event(s)${commandOptions.type ? ` matching "${commandOptions.type}"` : ''}:`));
|
|
497
|
+
console.log('');
|
|
498
|
+
|
|
499
|
+
if (commandOptions.json) {
|
|
500
|
+
events.forEach(e => console.log(JSON.stringify(e)));
|
|
501
|
+
} else {
|
|
502
|
+
const levelColors = {
|
|
503
|
+
info: chalk.blue,
|
|
504
|
+
warn: chalk.yellow,
|
|
505
|
+
error: chalk.red,
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
events.forEach(e => {
|
|
509
|
+
const time = e.time ? new Date(e.time).toLocaleString() : '?';
|
|
510
|
+
const level = (e.level || 'info').toUpperCase().padEnd(5);
|
|
511
|
+
const colorFn = levelColors[e.level] || chalk.white;
|
|
512
|
+
const type = chalk.cyan((e.type || 'unknown').padEnd(35));
|
|
513
|
+
const details = e.details && Object.keys(e.details).length > 0
|
|
514
|
+
? chalk.dim(JSON.stringify(e.details))
|
|
515
|
+
: '';
|
|
516
|
+
|
|
517
|
+
console.log(` ${chalk.dim(time)} ${colorFn(level)} ${type} ${details}`);
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
console.log('');
|
|
522
|
+
console.log(chalk.dim(` Log file: ${logFile}`));
|
|
523
|
+
console.log(chalk.dim(` Total events in file: ${raw.split('\n').length}`));
|
|
524
|
+
} catch (err) {
|
|
525
|
+
fatal('history', err);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
313
529
|
// โโโ Default: interactive mode โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
314
530
|
program.action(async () => {
|
|
315
531
|
try {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const GROQ_API_BASE = 'https://api.groq.com/openai/v1';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_AI_MODEL = 'qwen/qwen3-32b';
|
|
4
|
+
|
|
5
|
+
export function getGroqApiKey() {
|
|
6
|
+
return process.env.GROQ_API_KEY || null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function listGroqModels(apiKey) {
|
|
10
|
+
const res = await fetch(`${GROQ_API_BASE}/models`, {
|
|
11
|
+
headers: {
|
|
12
|
+
Authorization: `Bearer ${apiKey}`,
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
const message = await res.text().catch(() => '');
|
|
19
|
+
throw new Error(`Groq model lookup failed: HTTP ${res.status} ${message}`.trim());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const data = await res.json();
|
|
23
|
+
return Array.isArray(data.data) ? data.data : [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function createGroqChatCompletion(apiKey, body) {
|
|
27
|
+
const res = await fetch(`${GROQ_API_BASE}/chat/completions`, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
Authorization: `Bearer ${apiKey}`,
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify(body),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
const message = await res.text().catch(() => '');
|
|
38
|
+
const err = new Error(`Groq chat completion failed: HTTP ${res.status} ${message}`.trim());
|
|
39
|
+
err.status = res.status;
|
|
40
|
+
err.rateLimit = parseGroqRateLimitHeaders(res.headers);
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const data = await res.json();
|
|
45
|
+
data._rateLimit = parseGroqRateLimitHeaders(res.headers);
|
|
46
|
+
return data;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseGroqRateLimitHeaders(headers) {
|
|
50
|
+
const readNumber = (name) => {
|
|
51
|
+
const value = headers.get(name);
|
|
52
|
+
if (!value) return null;
|
|
53
|
+
const parsed = Number(value);
|
|
54
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
limitRequests: readNumber('x-ratelimit-limit-requests'),
|
|
59
|
+
remainingRequests: readNumber('x-ratelimit-remaining-requests'),
|
|
60
|
+
resetRequests: headers.get('x-ratelimit-reset-requests') || null,
|
|
61
|
+
limitTokens: readNumber('x-ratelimit-limit-tokens'),
|
|
62
|
+
remainingTokens: readNumber('x-ratelimit-remaining-tokens'),
|
|
63
|
+
resetTokens: headers.get('x-ratelimit-reset-tokens') || null,
|
|
64
|
+
retryAfter: headers.get('retry-after') || null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Lightweight token estimator: approximate tokens by characters/4
|
|
69
|
+
export function estimateTokensForMessages(messages = [], completionText = '') {
|
|
70
|
+
try {
|
|
71
|
+
let chars = 0;
|
|
72
|
+
for (const m of messages || []) {
|
|
73
|
+
if (!m) continue;
|
|
74
|
+
chars += String(m).length;
|
|
75
|
+
}
|
|
76
|
+
chars += String(completionText || '').length;
|
|
77
|
+
return Math.max(0, Math.ceil(chars / 4));
|
|
78
|
+
} catch {
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getRateLimitWarnings(rateLimit, threshold = 0.8) {
|
|
84
|
+
if (!rateLimit) return [];
|
|
85
|
+
|
|
86
|
+
const warnings = [];
|
|
87
|
+
const checks = [
|
|
88
|
+
{
|
|
89
|
+
label: 'daily request',
|
|
90
|
+
limit: rateLimit.limitRequests,
|
|
91
|
+
remaining: rateLimit.remainingRequests,
|
|
92
|
+
reset: rateLimit.resetRequests,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
label: 'per-minute token',
|
|
96
|
+
limit: rateLimit.limitTokens,
|
|
97
|
+
remaining: rateLimit.remainingTokens,
|
|
98
|
+
reset: rateLimit.resetTokens,
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
for (const item of checks) {
|
|
103
|
+
if (!item.limit || item.remaining === null || item.remaining === undefined) continue;
|
|
104
|
+
const used = Math.max(0, item.limit - item.remaining);
|
|
105
|
+
const ratio = used / item.limit;
|
|
106
|
+
if (ratio >= threshold) {
|
|
107
|
+
warnings.push({
|
|
108
|
+
...item,
|
|
109
|
+
used,
|
|
110
|
+
percent: Math.round(ratio * 100),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return warnings;
|
|
116
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const SECRET_PATTERNS = [
|
|
2
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
3
|
+
/\b(gsk_[A-Za-z0-9_-]{20,})\b/g,
|
|
4
|
+
/\b(sk-[A-Za-z0-9_-]{20,})\b/g,
|
|
5
|
+
/\b(ghp_[A-Za-z0-9_]{20,})\b/g,
|
|
6
|
+
/\b(xox[baprs]-[A-Za-z0-9-]{20,})\b/g,
|
|
7
|
+
/(["']?(?:api[_-]?key|token|password|secret|private[_-]?key)["']?\s*[:=]\s*["']?)[^\s"',}]+/gi,
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const BLOCKED_PATH_PATTERNS = [
|
|
11
|
+
/(^|[\s~/])\.ssh(\/|$)/,
|
|
12
|
+
/(^|[\s~/])\.gnupg(\/|$)/,
|
|
13
|
+
/(^|[\s~/])\.aws(\/|$)/,
|
|
14
|
+
/(^|[\s~/])\.config\/gh(\/|$)/,
|
|
15
|
+
/(^|[\s~/])\.ipingyou(\/|$)/,
|
|
16
|
+
/(^|[\s~/])\.npmrc($|\s)/,
|
|
17
|
+
/(^|[\s~/])\.netrc($|\s)/,
|
|
18
|
+
/(^|[\s~/])\.pypirc($|\s)/,
|
|
19
|
+
/(^|[\s~/])\.env($|[\s.]|\/)/,
|
|
20
|
+
/id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const BLOCKED_COMMAND_PATTERNS = [
|
|
24
|
+
/^\s*(env|printenv|set)\b/,
|
|
25
|
+
/\b(export|declare)\s+-p\b/,
|
|
26
|
+
/\bsecurity\s+find-generic-password\b/,
|
|
27
|
+
/\bpass\s+show\b/,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const DANGEROUS_COMMAND_PATTERNS = [
|
|
31
|
+
/\brm\s+(-[^\s]*[rf][^\s]*|--recursive|--force)/,
|
|
32
|
+
/\bsudo\b/,
|
|
33
|
+
/\bchmod\b/,
|
|
34
|
+
/\bchown\b/,
|
|
35
|
+
/\bmv\b/,
|
|
36
|
+
/\bcp\b/,
|
|
37
|
+
/\binstall\b/,
|
|
38
|
+
/\bnpm\s+(i|install|uninstall|remove|rm)\b/,
|
|
39
|
+
/\b(pnpm|yarn)\s+(add|remove|install)\b/,
|
|
40
|
+
/\b(git\s+(reset|clean|checkout|switch|merge|rebase|push|commit))\b/,
|
|
41
|
+
/\b(curl|wget)\b/,
|
|
42
|
+
/>|>>|\btee\b/,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
import { getAllowlistRegexes } from '../allowlist.js';
|
|
46
|
+
|
|
47
|
+
const READ_ONLY_COMMAND_PATTERNS = [
|
|
48
|
+
/^\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/,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
export function redactSensitive(value) {
|
|
52
|
+
let output = String(value ?? '');
|
|
53
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
54
|
+
output = output.replace(pattern, (match, prefix) => prefix ? `${prefix}[REDACTED]` : '[REDACTED]');
|
|
55
|
+
}
|
|
56
|
+
return output;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function truncateForModel(value, maxChars = 12000) {
|
|
60
|
+
const text = redactSensitive(value);
|
|
61
|
+
if (text.length <= maxChars) return text;
|
|
62
|
+
return `${text.slice(0, maxChars)}\n[TRUNCATED ${text.length - maxChars} chars]`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function commandTouchesBlockedPath(command) {
|
|
66
|
+
return BLOCKED_PATH_PATTERNS.some(pattern => pattern.test(command));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function classifyCommand(command) {
|
|
70
|
+
const text = String(command || '').trim();
|
|
71
|
+
if (!text) return { blocked: true, needsApproval: true, reason: 'Empty command' };
|
|
72
|
+
|
|
73
|
+
if (commandTouchesBlockedPath(text)) {
|
|
74
|
+
return {
|
|
75
|
+
blocked: true,
|
|
76
|
+
needsApproval: true,
|
|
77
|
+
reason: 'Command references a protected secret/config path',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
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
|
+
if (BLOCKED_COMMAND_PATTERNS.some(pattern => pattern.test(text))) {
|
|
92
|
+
return {
|
|
93
|
+
blocked: true,
|
|
94
|
+
needsApproval: true,
|
|
95
|
+
reason: 'Command could dump environment variables or credential stores',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (DANGEROUS_COMMAND_PATTERNS.some(pattern => pattern.test(text))) {
|
|
100
|
+
return { blocked: false, needsApproval: true, reason: 'Command may modify files, install packages, or access the network' };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (READ_ONLY_COMMAND_PATTERNS.some(pattern => pattern.test(text))) {
|
|
104
|
+
return { blocked: false, needsApproval: false, reason: 'Read-only command' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { blocked: false, needsApproval: true, reason: 'Command is not known to be read-only' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function sanitizeUserTask(task) {
|
|
111
|
+
return redactSensitive(task)
|
|
112
|
+
.replace(/session password\s*[:=]\s*\S+/gi, 'session password: [REDACTED]')
|
|
113
|
+
.replace(/private key\s*[:=]\s*\S+/gi, 'private key: [REDACTED]');
|
|
114
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const ALLOWLIST_PATH = path.join(os.homedir(), '.ipingyou', 'allowlist.json');
|
|
6
|
+
|
|
7
|
+
export function getAllowlistRegexes() {
|
|
8
|
+
try {
|
|
9
|
+
if (!fs.existsSync(ALLOWLIST_PATH)) return [];
|
|
10
|
+
const raw = fs.readFileSync(ALLOWLIST_PATH, 'utf8');
|
|
11
|
+
const data = JSON.parse(raw || '[]');
|
|
12
|
+
if (!Array.isArray(data)) return [];
|
|
13
|
+
return data.map(s => {
|
|
14
|
+
try { return new RegExp(s); } catch { return null; }
|
|
15
|
+
}).filter(Boolean);
|
|
16
|
+
} catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ensureAllowlistFile() {
|
|
22
|
+
try {
|
|
23
|
+
if (!fs.existsSync(path.dirname(ALLOWLIST_PATH))) {
|
|
24
|
+
fs.mkdirSync(path.dirname(ALLOWLIST_PATH), { recursive: true, mode: 0o700 });
|
|
25
|
+
}
|
|
26
|
+
if (!fs.existsSync(ALLOWLIST_PATH)) {
|
|
27
|
+
fs.writeFileSync(ALLOWLIST_PATH, JSON.stringify([
|
|
28
|
+
'^\\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'
|
|
29
|
+
], null, 2), { mode: 0o600 });
|
|
30
|
+
}
|
|
31
|
+
return ALLOWLIST_PATH;
|
|
32
|
+
} catch {
|
|
33
|
+
return ALLOWLIST_PATH;
|
|
34
|
+
}
|
|
35
|
+
}
|