@miraj181/ipingyou 2.1.2 โ†’ 2.1.4

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
@@ -25,7 +25,7 @@ No firewalls to configure. No port forwarding. No plaintext leakage.
25
25
  * ๐Ÿงญ **Approval Gate**: Require the Host to explicitly approve clients before they receive tunnel/key material.
26
26
  * ๐Ÿ“ฆ **One-Time File Share**: Serve a single file/folder over SCP and revoke after use.
27
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).
28
+ * ๐Ÿ“‚ **Shared Drop Folder**: Auto-prepared dropbox folder for safe file transfers (macOS friendly), removed on exit.
29
29
  * ๐Ÿงพ **Live Session Logs**: Host/client/broker write ephemeral per-session logs with actions and request/response status, removed on exit.
30
30
 
31
31
  ---
@@ -60,22 +60,43 @@ securelink
60
60
 
61
61
  ## ๐Ÿ”’ Zero-Knowledge Architecture
62
62
 
63
- The public broker server exists solely to rendezvous connections. It is fundamentally a **"Dumb Pipe"**.
63
+ The public broker server exists solely to rendezvous connections and approvals. It is fundamentally a **"Dumb Pipe"**.
64
+
65
+ ### Session Bootstrap & Data Path
64
66
 
65
67
  ```mermaid
66
68
  graph LR
67
- H[Host CLI] -->|AES-256-CBC Encrypted Payload| B((Broker Relay))
68
- B -->|Encrypted Payload| C[Client CLI]
69
+ H[Host CLI] -->|AES-256-CBC Encrypted Session Payload| B((Broker Relay))
70
+ H -->|Host Auth Token for approvals and telemetry| B
71
+ B -->|Encrypted Session Payload| C[Client CLI]
69
72
  C -->|Locally Decrypts Password| C
70
- C -->|Direct Cloudflare SSH| H
73
+ C -->|Direct Cloudflare SSH/TCP| H
71
74
  C -->|E2E AES-GCM WebSockets| H
72
75
  ```
73
76
 
74
- 1. **Host** starts up, spawns `cloudflared` tunnels for SSH and Chat, and generates a random, offline **AES-256 Session Password**.
75
- 2. **Host** encrypts the tunnel data with the password and sends the *ciphertext* to the Broker alongside a short UID.
77
+ 1. **Host** starts up, spawns `cloudflared` tunnels for SSH/HTTP/TCP and Chat, then generates a random **AES-256 Session Password** plus a **host-only auth token**.
78
+ 2. **Host** encrypts the session payload with the password and registers the ciphertext (plus the host token) with the Broker under a short UID.
76
79
  3. **Client** runs `ipingyou connect`, enters the UID and Password.
77
- 4. **Client** fetches the ciphertext, decrypts it locally, and connects directly via SSH and WebSockets.
78
- 5. On `Ctrl+C`, `tree-kill` initiates a graceful shutdown, revokes the UID from the broker, and scrubs `/tmp` memory.
80
+ 4. **Client** fetches the ciphertext, decrypts it locally, and connects directly via SSH or WebSockets.
81
+ 5. On `Ctrl+C`, `tree-kill` initiates a graceful shutdown, revokes the UID from the broker, and removes session artifacts.
82
+
83
+ ### Approval Gate Flow (Optional)
84
+
85
+ ```mermaid
86
+ sequenceDiagram
87
+ participant C as Client CLI
88
+ participant B as Broker Relay
89
+ participant H as Host CLI
90
+ C->>B: approval-request encrypted metadata
91
+ H->>B: fetch approvals with x-host-token
92
+ H->>B: approve/deny with x-host-token
93
+ C->>B: poll approval status
94
+ B-->>C: approved/denied
95
+ ```
96
+
97
+ 1. **Client** submits encrypted approval metadata (username, host, intent) to the Broker.
98
+ 2. **Host** lists and decides approvals using its host-only auth token; the Broker never shares this token with clients.
99
+ 3. **Client** polls for approval status and proceeds only when approved.
79
100
 
80
101
  ---
81
102
 
@@ -111,6 +132,8 @@ These alerts (e.g., "AI-detected potential code anomaly", "Shell access", "Netwo
111
132
  | `ipingyou service install` | ๐Ÿ‘ป Installs Host mode as an always-on background daemon. |
112
133
  | `ipingyou service stop` | Stops and removes the background daemon. |
113
134
  | `ipingyou service status` | Shows background daemon status. |
135
+ | `ipingyou allowlist` | Manage the AI command allowlist (list/add/remove). |
136
+ | `ipingyou history` | View session event logs from `~/.ipingyou/logs`. |
114
137
 
115
138
  ---
116
139
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraj181/ipingyou",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
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
@@ -122,6 +122,8 @@ function showRichHelp() {
122
122
  console.log(` โ€ข Cloudflare Tunnels punch through NAT/Firewalls securely.`);
123
123
  console.log(` โ€ข ${chalk.green('End-to-End Encryption')}: Tunnel URLs are AES-256 encrypted locally.`);
124
124
  console.log(` โ€ข The Broker never sees your plaintext URL, only ciphertext.`);
125
+ console.log(` โ€ข ${chalk.green('Host Auth Token')}: Host-only token gates approvals & telemetry access.`);
126
+ console.log(` โ€ข ${chalk.green('Approval Gate')}: Clients submit encrypted metadata; Host approves/denies.`);
125
127
  console.log('');
126
128
 
127
129
  console.log(chalk.bold.white(' ๐Ÿ”ฅ Advanced Features:'));
@@ -130,6 +132,8 @@ function showRichHelp() {
130
132
  console.log(` โ€ข ${chalk.green('E2E Chat Room')} : Real-time Web Crypto AES-GCM secure chat UI for Host & Clients.`);
131
133
  console.log(` โ€ข ${chalk.green('Daemonization')} : Run Host mode as a background service via PM2.`);
132
134
  console.log(` โ€ข ${chalk.green('Panic Kill-Switch')} : Instantly purge all processes, configurations, and traces.`);
135
+ console.log(` โ€ข ${chalk.green('Shared Drop Folder')} : Session dropbox auto-removed on exit.`);
136
+ console.log(` โ€ข ${chalk.green('Live Session Logs')} : Host/client/broker events written per session.`);
133
137
  console.log('');
134
138
 
135
139
  console.log(chalk.bold.white(' ๐Ÿ’ก Examples:'));
package/src/lib/broker.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import os from 'node:os';
3
+ import crypto from 'node:crypto';
3
4
  import { decrypt, encrypt } from './crypto.js';
4
5
  import { createSpinner, cryptoSpinner, networkSpinner } from './animations.js';
5
6
  import { logSessionEvent } from './session-log.js';
@@ -51,6 +52,7 @@ export async function registerWithBroker(brokerUrl, uid, tunnelUrl, password, se
51
52
  await new Promise(r => setTimeout(r, 600));
52
53
  const payload = JSON.stringify({ url: tunnelUrl, ...serviceConfig });
53
54
  const encrypted = encrypt(payload, password);
55
+ const localHostToken = crypto.randomBytes(32).toString('hex');
54
56
 
55
57
  spinner.text = 'Registering with broker...';
56
58
 
@@ -64,6 +66,7 @@ export async function registerWithBroker(brokerUrl, uid, tunnelUrl, password, se
64
66
  salt: encrypted.salt,
65
67
  approvalRequired: Boolean(serviceConfig.approvalRequired),
66
68
  oneTime: Boolean(serviceConfig.oneTimeSharePath),
69
+ hostToken: localHostToken,
67
70
  }),
68
71
  });
69
72
 
@@ -77,7 +80,7 @@ export async function registerWithBroker(brokerUrl, uid, tunnelUrl, password, se
77
80
  spinner.succeed(`Registered with broker ${chalk.dim(`(${brokerUrl})`)} ${chalk.green('[E2E encrypted]')}`);
78
81
  logSessionEvent('broker_registered', { uid, broker: brokerUrl });
79
82
  // Return the host authentication token โ€” needed for all host-only broker operations
80
- return { success: true, hostToken: result.hostToken || null };
83
+ return { success: true, hostToken: result.hostToken || localHostToken || null };
81
84
  } catch (err) {
82
85
  spinner.fail(`Broker registration failed: ${err.message}`);
83
86
  console.error(chalk.red(` โŒ Error: ${err.message}`));
package/src/modes/host.js CHANGED
@@ -193,6 +193,23 @@ async function prepareSharedDropFolder(uid) {
193
193
  return dropPath;
194
194
  }
195
195
 
196
+ async function cleanupSharedDropFolder(dropPath, uid) {
197
+ if (!dropPath) return;
198
+ const expectedPath = path.join(os.homedir(), `ipingyou-dropbox-${uid}`);
199
+ if (path.resolve(dropPath) !== path.resolve(expectedPath)) {
200
+ console.log(chalk.yellow(' Skipping drop folder cleanup (unexpected path).'));
201
+ return;
202
+ }
203
+ try {
204
+ const stat = await fs.promises.lstat(dropPath).catch(() => null);
205
+ if (!stat || !stat.isDirectory() || stat.isSymbolicLink()) return;
206
+ console.log(chalk.dim(' Removing shared drop folder...'));
207
+ await fs.promises.rm(dropPath, { recursive: true, force: true });
208
+ } catch (err) {
209
+ console.log(chalk.yellow(` Could not remove drop folder: ${err.message}`));
210
+ }
211
+ }
212
+
196
213
  function showMacPrivacyPreflight(sharedDropPath) {
197
214
  if (process.platform !== 'darwin') return;
198
215
 
@@ -221,7 +238,7 @@ async function promptOneTimeSharePath() {
221
238
  return sharePath.trim() === '~' ? os.homedir() : sharePath.trim().replace(/^~(?=\/)/, os.homedir());
222
239
  }
223
240
 
224
- async function startLocalHostDashboard(uid, password, serviceConfig, hostToken) {
241
+ async function startLocalHostDashboard(uid, password, serviceConfig, sessionState) {
225
242
  const { default: express } = await import('express');
226
243
  const { default: open } = await import('open');
227
244
  const app = express();
@@ -251,7 +268,7 @@ async function startLocalHostDashboard(uid, password, serviceConfig, hostToken)
251
268
  const push = async () => {
252
269
  if (closed) return;
253
270
  try {
254
- const data = await fetchApprovalRequests(BROKER_URL, uid, hostToken).catch(() => ({ approvals: [] }));
271
+ const data = await fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken).catch(() => ({ approvals: [] }));
255
272
  res.write(`event: approvals\n`);
256
273
  res.write(`data: ${JSON.stringify(data)}\n\n`);
257
274
  } catch { }
@@ -286,7 +303,7 @@ async function startLocalHostDashboard(uid, password, serviceConfig, hostToken)
286
303
  const { requestId, decision } = req.body || {};
287
304
  if (!requestId || !decision) return res.status(400).json({ error: 'requestId and decision required' });
288
305
  try {
289
- await decideApprovalRequest(BROKER_URL, uid, requestId, decision, hostToken);
306
+ await decideApprovalRequest(BROKER_URL, uid, requestId, decision, sessionState.hostToken);
290
307
  recordEvent('approval_decision', { uid, requestId, decision, via: 'dashboard' });
291
308
  res.json({ ok: true });
292
309
  } catch (err) {
@@ -296,7 +313,7 @@ async function startLocalHostDashboard(uid, password, serviceConfig, hostToken)
296
313
 
297
314
  app.post('/api/revoke', async (_req, res) => {
298
315
  try {
299
- await revokeUID(BROKER_URL, uid, hostToken);
316
+ await revokeUID(BROKER_URL, uid, sessionState.hostToken);
300
317
  recordEvent('uid_revoked', { uid, via: 'dashboard' });
301
318
  res.json({ ok: true });
302
319
  } catch (err) {
@@ -545,7 +562,7 @@ async function spawnPrivateBroker() {
545
562
  /**
546
563
  * Display the host dashboard and handle user input.
547
564
  */
548
- async function hostDashboard(uid, tunnelUrl, password, serviceConfig, tunnelProcess, hostToken) {
565
+ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessionState) {
549
566
  let chatServerInstance = null;
550
567
  let chatTunnelProcess = null;
551
568
  let dashboardInstance = null;
@@ -559,7 +576,7 @@ async function hostDashboard(uid, tunnelUrl, password, serviceConfig, tunnelProc
559
576
  console.log(` โ•‘ ${chalk.cyan('UID:')} ${chalk.bold.white(uid.padEnd(30))}โ•‘`);
560
577
  console.log(` โ•‘ ${chalk.cyan('Password:')} ${chalk.bold.white(password.padEnd(30))}โ•‘`);
561
578
  console.log(` โ•‘ ${chalk.cyan('Service:')} ${chalk.dim(serviceConfig.type.toUpperCase() + ' (Port ' + serviceConfig.port + ')').padEnd(30)}โ•‘`);
562
- console.log(` โ•‘ ${chalk.cyan('Tunnel:')} ${chalk.dim(tunnelUrl.substring(0, 40))} โ•‘`);
579
+ console.log(` โ•‘ ${chalk.cyan('Tunnel:')} ${chalk.dim(sessionState.tunnelUrl.substring(0, 40))} โ•‘`);
563
580
  if (serviceConfig.chatUrl) {
564
581
  console.log(` โ•‘ ${chalk.cyan('Chat URL:')} ${chalk.dim(serviceConfig.chatUrl.substring(0, 40))} โ•‘`);
565
582
  }
@@ -624,7 +641,7 @@ async function hostDashboard(uid, tunnelUrl, password, serviceConfig, tunnelProc
624
641
  switch (action) {
625
642
  case 'approvals': {
626
643
  try {
627
- const data = await fetchApprovalRequests(BROKER_URL, uid, hostToken);
644
+ const data = await fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken);
628
645
  const pending = (data.approvals || []).filter(item => item.status === 'pending');
629
646
  if (pending.length === 0) {
630
647
  console.log(chalk.yellow(' No pending approval requests.'));
@@ -656,7 +673,7 @@ async function hostDashboard(uid, tunnelUrl, password, serviceConfig, tunnelProc
656
673
  ],
657
674
  }]);
658
675
  if (decision !== 'skip') {
659
- await decideApprovalRequest(BROKER_URL, uid, request.id, decision, hostToken);
676
+ await decideApprovalRequest(BROKER_URL, uid, request.id, decision, sessionState.hostToken);
660
677
  recordEvent('approval_decision', { uid, requestId: request.id, decision, username: details.username });
661
678
  }
662
679
  }
@@ -675,8 +692,8 @@ async function hostDashboard(uid, tunnelUrl, password, serviceConfig, tunnelProc
675
692
  chatTunnelProcess = null;
676
693
  chatServerInstance = null;
677
694
  delete serviceConfig.chatUrl;
678
- const res = await registerWithBroker(BROKER_URL, uid, tunnelUrl, password, serviceConfig);
679
- if (res.success && res.hostToken) hostToken = res.hostToken;
695
+ const res = await registerWithBroker(BROKER_URL, uid, sessionState.tunnelUrl, password, serviceConfig);
696
+ if (res.success && res.hostToken) sessionState.hostToken = res.hostToken;
680
697
  renderDashboard();
681
698
  }
682
699
  });
@@ -684,8 +701,8 @@ async function hostDashboard(uid, tunnelUrl, password, serviceConfig, tunnelProc
684
701
  console.log(chalk.dim(' Provisioning Cloudflare tunnel for chat...'));
685
702
  chatTunnelProcess = await spawnTunnelSupervised(`http://localhost:${chatServerInstance.port}`, async (newUrl) => {
686
703
  serviceConfig.chatUrl = newUrl;
687
- const res = await registerWithBroker(BROKER_URL, uid, tunnelUrl, password, serviceConfig);
688
- if (res.success && res.hostToken) hostToken = res.hostToken;
704
+ const res = await registerWithBroker(BROKER_URL, uid, sessionState.tunnelUrl, password, serviceConfig);
705
+ if (res.success && res.hostToken) sessionState.hostToken = res.hostToken;
689
706
  renderDashboard();
690
707
  });
691
708
 
@@ -698,7 +715,7 @@ async function hostDashboard(uid, tunnelUrl, password, serviceConfig, tunnelProc
698
715
  }
699
716
 
700
717
  case 'dashboard': {
701
- dashboardInstance = await startLocalHostDashboard(uid, password, serviceConfig, hostToken);
718
+ dashboardInstance = await startLocalHostDashboard(uid, password, serviceConfig, sessionState);
702
719
  logSessionEvent('host_dashboard_opened');
703
720
  return waitForAction();
704
721
  }
@@ -748,7 +765,7 @@ async function hostDashboard(uid, tunnelUrl, password, serviceConfig, tunnelProc
748
765
  const spinner = createSpinner('Fetching secure client telemetry...', networkSpinner).start();
749
766
  try {
750
767
  const res = await fetch(`${BROKER_URL}/clients/${uid}`, {
751
- headers: hostToken ? { 'x-host-token': hostToken } : {}
768
+ headers: sessionState.hostToken ? { 'x-host-token': sessionState.hostToken } : {}
752
769
  });
753
770
  if (!res.ok) throw new Error('Failed to fetch from broker');
754
771
  const data = await res.json();
@@ -786,8 +803,8 @@ async function hostDashboard(uid, tunnelUrl, password, serviceConfig, tunnelProc
786
803
  }
787
804
 
788
805
  case 'reregister':
789
- const res = await registerWithBroker(BROKER_URL, uid, tunnelUrl, password, serviceConfig);
790
- if (res.success && res.hostToken) hostToken = res.hostToken;
806
+ const res = await registerWithBroker(BROKER_URL, uid, sessionState.tunnelUrl, password, serviceConfig);
807
+ if (res.success && res.hostToken) sessionState.hostToken = res.hostToken;
791
808
  logSessionEvent('host_broker_reregistered');
792
809
  return waitForAction();
793
810
 
@@ -953,6 +970,7 @@ export async function startHostMode() {
953
970
  serviceConfig.sharedDropPath = await prepareSharedDropFolder(uid);
954
971
  console.log(chalk.green(` โœ“ Shared drop folder ready: ${serviceConfig.sharedDropPath}`));
955
972
  showMacPrivacyPreflight(serviceConfig.sharedDropPath);
973
+ addCleanupHook(() => cleanupSharedDropFolder(serviceConfig.sharedDropPath, uid));
956
974
  } catch (err) {
957
975
  console.log(chalk.yellow(` โš ๏ธ Could not prepare shared drop folder: ${err.message}`));
958
976
  }
@@ -997,24 +1015,23 @@ export async function startHostMode() {
997
1015
  console.log(chalk.dim(` โ„น๏ธ Ensure your ${protocol.toUpperCase()} service is running on port ${targetPort}.`));
998
1016
  }
999
1017
 
1000
- let tunnelUrl = null;
1001
- let hostToken = null;
1018
+ const sessionState = { tunnelUrl: null, hostToken: null };
1002
1019
  const tunnelProcess = await spawnTunnelSupervised(targetUrl, async (newUrl) => {
1003
- tunnelUrl = newUrl;
1020
+ sessionState.tunnelUrl = newUrl;
1004
1021
  // Register or re-register with broker when tunnel is spawned/respawned
1005
- const res = await registerWithBroker(BROKER_URL, uid, tunnelUrl, password, serviceConfig);
1022
+ const res = await registerWithBroker(BROKER_URL, uid, sessionState.tunnelUrl, password, serviceConfig);
1006
1023
  if (!res.success) {
1007
1024
  console.error(chalk.red(`\n โŒ FATAL: Could not register with broker at ${BROKER_URL}`));
1008
1025
  logSessionEvent('host_broker_register_failed', { broker: BROKER_URL }, 'error');
1009
1026
  process.exit(1);
1010
1027
  }
1011
- if (res.hostToken) hostToken = res.hostToken;
1028
+ if (res.hostToken) sessionState.hostToken = res.hostToken;
1012
1029
  });
1013
1030
 
1014
- // Wait for the first URL to be generated before showing the dashboard
1015
- await waitForValue(() => tunnelUrl, 30000, 'Cloudflare tunnel startup');
1031
+ // Wait for the tunnel AND broker registration to complete before showing dashboard
1032
+ await waitForValue(() => sessionState.hostToken, 30000, 'Cloudflare tunnel and Broker startup');
1016
1033
 
1017
- setRevokeOnExit(uid, BROKER_URL, () => hostToken);
1034
+ setRevokeOnExit(uid, BROKER_URL, () => sessionState.hostToken);
1018
1035
 
1019
- await hostDashboard(uid, tunnelUrl, password, serviceConfig, tunnelProcess, hostToken);
1036
+ await hostDashboard(uid, password, serviceConfig, tunnelProcess, sessionState);
1020
1037
  }
package/src/server.js CHANGED
@@ -124,6 +124,7 @@ const store = new Map(); // uid โ†’ { iv, ciphertext, salt, createdAt, clients:
124
124
 
125
125
  // โ”€โ”€โ”€ Security Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
126
126
  const SAFE_PARAM = /^[a-zA-Z0-9_-]{1,64}$/;
127
+ const HOST_TOKEN_FORMAT = /^[a-f0-9]{64}$/i;
127
128
 
128
129
  function isSafeParam(val) {
129
130
  return typeof val === 'string' && SAFE_PARAM.test(val);
@@ -185,7 +186,7 @@ app.get('/health', (_req, res) => {
185
186
  */
186
187
  app.post('/register', strictLimiter, (req, res) => {
187
188
  try {
188
- const { uid, iv, ciphertext, salt, approvalRequired = false, oneTime = false } = req.body;
189
+ const { uid, iv, ciphertext, salt, approvalRequired = false, oneTime = false, hostToken: providedHostToken } = req.body;
189
190
 
190
191
  if (!uid || !iv || !ciphertext || !salt) {
191
192
  recordViolation(req);
@@ -219,8 +220,10 @@ app.post('/register', strictLimiter, (req, res) => {
219
220
  return res.status(503).json({ error: 'Broker is at maximum capacity. Please try again later.' });
220
221
  }
221
222
 
222
- // Generate a cryptographic host token โ€” only the host receives this
223
- const hostToken = generateHostToken(uid + Date.now().toString());
223
+ // Use provided host token if valid; otherwise generate a fresh one
224
+ const hostToken = (typeof providedHostToken === 'string' && HOST_TOKEN_FORMAT.test(providedHostToken))
225
+ ? providedHostToken
226
+ : generateHostToken(uid + Date.now().toString());
224
227
 
225
228
  // Store the encrypted blob as-is โ€” broker NEVER decrypts
226
229
  store.set(uid, {