@miraj181/ipingyou 1.0.2 β†’ 2.0.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 CHANGED
@@ -1,62 +1,53 @@
1
1
  # πŸ”— iPingYou β€” SecureLink CLI
2
2
 
3
- Secure peer-to-peer remote access via SSH & Cloudflare Tunnels. A Node.js CLI tool that lets two machines establish an encrypted SSH connection through Cloudflare's network, coordinated by a lightweight broker.
3
+ Secure peer-to-peer remote access via SSH & Cloudflare Tunnels. A zero-configuration Node.js CLI tool that lets two machines establish an encrypted SSH connection through Cloudflare's network, instantly.
4
4
 
5
5
  ## Architecture
6
6
 
7
7
  ```
8
- Host Machine Broker (Express.js) Client Machine
9
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” POST β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” GET β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
10
- β”‚ cloudflared │──/register──▢│ In-Memory Map │◀─/resolve──│ ipingyou β”‚
11
- β”‚ tunnel :22 β”‚ {uid,url} β”‚ AES-256-CBC β”‚ /:uid β”‚ connect β”‚
12
- β”‚ β”‚ β”‚ 1hr TTL expiry β”‚ β”‚ β”‚
13
- β”‚ SSH daemon │◀─────────────│──────────────────│─────────────│ SSH via β”‚
14
- β”‚ β”‚ cloudflared β”‚ β”‚ ProxyCmd β”‚ ProxyCmd β”‚
15
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ TCP proxy β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
8
+ Host Machine Client Machine
9
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” POST GET β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
10
+ β”‚ cloudflared │──/register──▢[Broker Server]◀─/resolve──│ ipingyou β”‚
11
+ β”‚ tunnel :22 β”‚ {uid,url} (Render Hosted) /:uid β”‚ connect β”‚
12
+ β”‚ β”‚ AES-256-CBC β”‚ β”‚
13
+ β”‚ SSH daemon │◀───────────── ─────────────│ SSH via β”‚
14
+ β”‚ β”‚ cloudflared ProxyCmd β”‚ ProxyCmd β”‚
15
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ TCP proxy β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
16
16
  ```
17
17
 
18
18
  ### Flow
19
- 1. **Host** starts `ipingyou host` β†’ generates an 8-char UID, spins up `cloudflared` tunnel, registers with broker
20
- 2. **Host** shares UID with the client (verbally, chat, etc.)
21
- 3. **Client** runs `ipingyou connect` β†’ enters UID, broker resolves it to tunnel URL β†’ SSH connects via cloudflared proxy
22
- 4. On `Ctrl+C`, all spawned processes are killed via `tree-kill` and the UID is revoked from the broker
19
+ 1. **Host** starts `ipingyou host` β†’ generates an 8-char UID, spins up `cloudflared` tunnel, and registers with the broker.
20
+ 2. **Host** shares the UID with the client.
21
+ 3. **Client** runs `ipingyou connect` β†’ enters UID, broker resolves it to tunnel URL β†’ SSH connects via cloudflared proxy.
22
+ 4. On `Ctrl+C`, all spawned processes are killed via `tree-kill` and the UID is automatically revoked from the broker.
23
23
 
24
- ## Quick Start
24
+ ## Usage
25
25
 
26
- ### 1. Start the Broker (self-hosted or Render)
26
+ You do not need to clone the repository or configure any `.env` files. `iPingYou` is purely published on npm and handles the backend API automatically!
27
+
28
+ ### The "On-the-Fly" Way (Recommended)
29
+ Run it anywhere using `npx` (make sure you aren't inside the project source code folder):
27
30
 
28
31
  ```bash
29
- cd ipingyou
30
- cp .env.example .env
31
- npm install
32
- npm run broker
33
- # Broker: http://localhost:4000
34
- ```
32
+ # Start the interactive wizard
33
+ npx @miraj181/ipingyou
35
34
 
36
- ### 2. Host Mode (Machine being accessed)
35
+ # Or instantly start as a Host
36
+ npx @miraj181/ipingyou host
37
37
 
38
- ```bash
39
- npx ipingyou host
40
- # or interactively:
41
- npx ipingyou
38
+ # Or instantly connect as a Client
39
+ npx @miraj181/ipingyou connect
42
40
  ```
43
41
 
44
- ### 3. Client Mode (Machine accessing)
42
+ ### Global Install
43
+ If you use the tool frequently, install it globally:
45
44
 
46
45
  ```bash
47
- npx ipingyou connect
48
- # or interactively:
49
- npx ipingyou
50
- ```
51
-
52
- ## CLI Commands
46
+ npm install -g @miraj181/ipingyou
53
47
 
54
- ```
55
- ipingyou Interactive mode (prompts for host/client)
56
- ipingyou host Start as host β€” allow remote access
57
- ipingyou connect Start as client β€” access a remote machine
58
- ipingyou broker Start the central broker server
59
- ipingyou broker -p 5000 Start broker on custom port
48
+ # Run as native commands:
49
+ ipingyou
50
+ securelink
60
51
  ```
61
52
 
62
53
  ## Prerequisites
@@ -67,66 +58,15 @@ ipingyou broker -p 5000 Start broker on custom port
67
58
  | `ssh` | βœ… | Ships with macOS/Linux; `winget install Microsoft.OpenSSH.Client` on Windows |
68
59
  | `cloudflared` | βœ… | `brew install cloudflared` / [download](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) |
69
60
 
70
- The CLI auto-detects your OS and will attempt to install missing dependencies on Linux (apt/pacman) and guide you on macOS/Windows.
71
-
72
- ## Project Structure
73
-
74
- ```
75
- ipingyou/
76
- β”œβ”€β”€ src/
77
- β”‚ β”œβ”€β”€ cli.js ← Main CLI entry (npx-ready, shebang)
78
- β”‚ β”œβ”€β”€ server.js ← Central Broker (Express.js)
79
- β”‚ β”œβ”€β”€ lib/
80
- β”‚ β”‚ β”œβ”€β”€ cleanup.js ← Graceful shutdown + tree-kill
81
- β”‚ β”‚ β”œβ”€β”€ crypto.js ← AES-256-CBC encrypt/decrypt
82
- β”‚ β”‚ β”œβ”€β”€ platform.js ← OS detection + dependency check
83
- β”‚ β”‚ └── uid.js ← Random 8-char UID generator (nanoid)
84
- β”‚ └── modes/
85
- β”‚ β”œβ”€β”€ host.js ← Host mode logic
86
- β”‚ └── client.js ← Client mode logic
87
- β”œβ”€β”€ .env.example
88
- β”œβ”€β”€ .gitignore
89
- β”œβ”€β”€ package.json
90
- β”œβ”€β”€ render.yaml ← One-click Render deploy
91
- └── README.md
92
- ```
61
+ *The CLI auto-detects your OS and will attempt to guide you on how to install missing dependencies!*
93
62
 
94
63
  ## Security
95
64
 
96
- - **AES-256-CBC** β€” Tunnel URLs are encrypted at rest in the broker's memory
97
- - **Random UIDs** β€” 8-char nanoid, not hardware-based; session dies = door locked forever
98
- - **1-hour TTL** β€” Broker auto-expires entries; no stale data
99
- - **Auto-revoke** β€” On `Ctrl+C`, UID is deleted from broker before exit
100
- - **tree-kill** β€” All child processes (cloudflared, SSH) are recursively killed on shutdown
101
- - **No persistence** β€” Broker uses in-memory Map only; server restart = clean slate
102
-
103
- ## Deploy Broker to Render (Free)
104
-
105
- 1. Push this repo to GitHub
106
- 2. Go to [render.com/new](https://render.com/new)
107
- 3. Click **"New Web Service"** β†’ connect your repo
108
- 4. Render auto-detects `render.yaml` β†’ click **Deploy**
109
- 5. Set `SECRET_KEY` in Render Dashboard β†’ Environment
110
- 6. Set `BROKER_URL` in your local `.env` to your Render URL
111
-
112
- ## Environment Variables
113
-
114
- | Variable | Default | Description |
115
- |----------|---------|-------------|
116
- | `SECRET_KEY` | Dev key | AES-256 hex key (64 chars) |
117
- | `BROKER_URL` | `http://localhost:4000` | Broker endpoint for CLI |
118
- | `BROKER_PORT` | `4000` | Port for broker server |
119
-
120
- ## Differences from remote-penitrator
121
-
122
- | Aspect | remote-penitrator | iPingYou |
123
- |--------|-------------------|----------|
124
- | Interface | Shell scripts + web dashboard | Interactive Node.js CLI |
125
- | Direction | One-way (runner β†’ C2) | Peer-to-peer (host ↔ client) |
126
- | UID System | Hardcoded PC IDs | Random per-session UIDs |
127
- | Process Mgmt | Manual | tree-kill auto-cleanup |
128
- | Install | Copy scripts | `npx ipingyou` |
129
- | Modes | Reporter only | Host + Client + Broker |
65
+ - **Zero-Knowledge Architecture** β€” The broker never sees the plaintext Cloudflare URL. It is strictly used as an encrypted key-value store.
66
+ - **AES-256-CBC** β€” Tunnel URLs are encrypted locally before being transmitted to the broker.
67
+ - **Random UIDs** β€” 8-char nanoid, not hardware-based. When the session dies, the door is locked forever.
68
+ - **Auto-revoke** β€” On `Ctrl+C`, UID is immediately deleted from the broker before exit.
69
+ - **No Persistence** β€” Broker uses an in-memory Map only with strict 1-hour TTLs.
130
70
 
131
71
  ## License
132
72
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraj181/ipingyou",
3
- "version": "1.0.2",
3
+ "version": "2.0.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": {
@@ -45,10 +45,15 @@
45
45
  "chalk": "^5.3.0",
46
46
  "commander": "^12.1.0",
47
47
  "execa": "^9.5.2",
48
+ "express": "^5.2.1",
49
+ "express-rate-limit": "^8.5.2",
50
+ "helmet": "^8.1.0",
48
51
  "inquirer": "^12.3.2",
49
52
  "nanoid": "^5.0.9",
53
+ "open": "^11.0.0",
50
54
  "ora": "^8.1.1",
51
- "tree-kill": "^1.2.2"
55
+ "tree-kill": "^1.2.2",
56
+ "ws": "^8.20.1"
52
57
  },
53
58
  "devDependencies": {
54
59
  "nodemon": "^3.1.4"
@@ -57,4 +62,4 @@
57
62
  "node": ">=18.0.0"
58
63
  },
59
64
  "type": "module"
60
- }
65
+ }
package/src/cli.js CHANGED
@@ -23,9 +23,14 @@ import inquirer from 'inquirer';
23
23
  import chalk from 'chalk';
24
24
 
25
25
  import { detectOS, checkDependencies } from './lib/platform.js';
26
- import { installShutdownHandlers } from './lib/cleanup.js';
26
+ import { installShutdownHandlers, executePanicMode } from './lib/cleanup.js';
27
27
  import { startHostMode } from './modes/host.js';
28
28
  import { startClientMode } from './modes/client.js';
29
+ import { execa } from 'execa';
30
+ import path from 'node:path';
31
+ import { fileURLToPath } from 'node:url';
32
+
33
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
29
34
 
30
35
  // ─── ASCII Banner ────────────────────────────────────────────
31
36
  function showBanner() {
@@ -67,10 +72,20 @@ function showRichHelp() {
67
72
  console.log(` β€’ The Broker never sees your plaintext URL, only ciphertext.`);
68
73
  console.log('');
69
74
 
75
+ console.log(chalk.bold.white(' πŸ”₯ Advanced Features:'));
76
+ console.log(` β€’ ${chalk.green('Terminal Mirroring')} : Host can spectate connected SSH clients in real-time.`);
77
+ console.log(` β€’ ${chalk.green('Reverse Forwarding')} : Clients can expose their local localhost ports back to the Host.`);
78
+ console.log(` β€’ ${chalk.green('E2E Chat Room')} : Real-time Web Crypto AES-GCM secure chat UI for Host & Clients.`);
79
+ console.log(` β€’ ${chalk.green('Daemonization')} : Run Host mode as a background service via PM2.`);
80
+ console.log(` β€’ ${chalk.green('Panic Kill-Switch')} : Instantly purge all processes, configurations, and traces.`);
81
+ console.log('');
82
+
70
83
  console.log(chalk.bold.white(' πŸ’‘ Examples:'));
71
- console.log(` $ npx ipingyou ${chalk.dim('# Interactive wizard (Recommended)')}`);
72
- console.log(` $ npx ipingyou host ${chalk.dim('# Quick start as Host')}`);
73
- console.log(` $ npx ipingyou connect ${chalk.dim('# Quick start as Client')}`);
84
+ console.log(` $ npx ipingyou ${chalk.dim('# Interactive wizard (Recommended)')}`);
85
+ console.log(` $ npx ipingyou host ${chalk.dim('# Quick start as Host')}`);
86
+ console.log(` $ npx ipingyou connect ${chalk.dim('# Quick start as Client')}`);
87
+ console.log(` $ npx ipingyou panic ${chalk.dim('# Self-destruct and wipe memory/traces')}`);
88
+ console.log(` $ npx ipingyou service install ${chalk.dim('# Install Host mode as a background daemon')}`);
74
89
  console.log('');
75
90
  }
76
91
 
@@ -91,6 +106,16 @@ function fatal(context, err) {
91
106
  process.exit(1);
92
107
  }
93
108
 
109
+ // ─── Process-Level Error Catching ────────────────────────────
110
+ process.on('uncaughtException', (err) => {
111
+ fatal('uncaughtException', err);
112
+ });
113
+
114
+ process.on('unhandledRejection', (reason) => {
115
+ const err = reason instanceof Error ? reason : new Error(String(reason));
116
+ fatal('unhandledRejection', err);
117
+ });
118
+
94
119
  // ─── Interactive Mode Selection ──────────────────────────────
95
120
  async function interactiveMode() {
96
121
  showBanner();
@@ -158,6 +183,7 @@ program
158
183
  .name('ipingyou')
159
184
  .description('SecureLink-CLI β€” Secure P2P remote access via SSH & Cloudflare Tunnels')
160
185
  .version('1.0.0')
186
+ .option('-b, --broker <url>', 'Override the central broker URL')
161
187
  .addHelpText('beforeAll', () => {
162
188
  showBanner();
163
189
  showRichHelp();
@@ -169,6 +195,9 @@ program
169
195
  .description('Start host mode β€” allow remote access to this machine')
170
196
  .action(async () => {
171
197
  try {
198
+ const opts = program.opts();
199
+ if (opts.broker) process.env.BROKER_URL = opts.broker;
200
+
172
201
  showBanner();
173
202
  showSystemInfo();
174
203
  installShutdownHandlers();
@@ -185,6 +214,9 @@ program
185
214
  .option('-u, --uid <uid>', 'The remote host UID')
186
215
  .action(async () => {
187
216
  try {
217
+ const opts = program.opts();
218
+ if (opts.broker) process.env.BROKER_URL = opts.broker;
219
+
188
220
  showBanner();
189
221
  showSystemInfo();
190
222
  installShutdownHandlers();
@@ -195,9 +227,57 @@ program
195
227
  }
196
228
  });
197
229
 
230
+ program
231
+ .command('panic')
232
+ .description('🚨 Self-destruct mode: wipe all configs, kill tunnels, and remove traces')
233
+ .action(async () => {
234
+ try {
235
+ showBanner();
236
+ await executePanicMode();
237
+ } catch (err) {
238
+ fatal('panic', err);
239
+ }
240
+ });
241
+
242
+ program
243
+ .command('service <action>')
244
+ .description('πŸ‘» Manage background daemon (actions: install, stop, status)')
245
+ .action(async (action) => {
246
+ try {
247
+ showBanner();
248
+ console.log(chalk.bold.cyan(' πŸ‘» Background Service Manager'));
249
+ console.log(chalk.dim(' ──────────────────────────────────────'));
250
+
251
+ const { execaCommand } = await import('execa');
252
+
253
+ if (action === 'install') {
254
+ console.log(chalk.dim(' Installing PM2 globally and starting host...'));
255
+ await execaCommand('npm install -g pm2', { stdio: 'inherit' });
256
+ await execaCommand('pm2 start ipingyou --name "ipingyou-host" -- host', { stdio: 'inherit' });
257
+ await execaCommand('pm2 save', { stdio: 'inherit' });
258
+ await execaCommand('pm2 startup', { stdio: 'inherit' });
259
+ console.log(chalk.green('\n βœ… Service installed and running in the background.'));
260
+ } else if (action === 'stop') {
261
+ await execaCommand('pm2 stop ipingyou-host', { stdio: 'inherit' });
262
+ await execaCommand('pm2 delete ipingyou-host', { stdio: 'inherit' });
263
+ await execaCommand('pm2 save', { stdio: 'inherit' });
264
+ console.log(chalk.green('\n βœ… Service stopped and removed.'));
265
+ } else if (action === 'status') {
266
+ await execaCommand('pm2 status ipingyou-host', { stdio: 'inherit' });
267
+ } else {
268
+ console.log(chalk.red(` ❌ Unknown action: ${action}. Use install, stop, or status.`));
269
+ }
270
+ } catch (err) {
271
+ fatal('service', err);
272
+ }
273
+ });
274
+
198
275
  // ─── Default: interactive mode ──────────────────────────────
199
276
  program.action(async () => {
200
277
  try {
278
+ const opts = program.opts();
279
+ if (opts.broker) process.env.BROKER_URL = opts.broker;
280
+
201
281
  installShutdownHandlers();
202
282
  await interactiveMode();
203
283
  } catch (err) {
@@ -0,0 +1,333 @@
1
+ import http from 'node:http';
2
+ import { WebSocketServer } from 'ws';
3
+ import open from 'open';
4
+ import chalk from 'chalk';
5
+
6
+ const HTML_CONTENT = `
7
+ <!DOCTYPE html>
8
+ <html lang="en">
9
+ <head>
10
+ <meta charset="UTF-8">
11
+ <title>iPingYou β€” Secure Chat Room</title>
12
+ <style>
13
+ :root {
14
+ --bg: #0f172a;
15
+ --bg-panel: #1e293b;
16
+ --text: #f8fafc;
17
+ --primary: #38bdf8;
18
+ --accent: #818cf8;
19
+ --danger: #ef4444;
20
+ --border: #334155;
21
+ }
22
+ body {
23
+ margin: 0; padding: 0; font-family: 'Inter', system-ui, sans-serif;
24
+ background: var(--bg); color: var(--text); height: 100vh; display: flex; flex-direction: column;
25
+ }
26
+ header {
27
+ background: var(--bg-panel); padding: 1rem 2rem; border-bottom: 1px solid var(--border);
28
+ display: flex; justify-content: space-between; align-items: center; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
29
+ }
30
+ h1 { margin: 0; font-size: 1.25rem; font-weight: 600; display: flex; align-items: center; gap: 0.5rem; }
31
+ .badge { background: var(--primary); color: #000; padding: 0.2rem 0.5rem; border-radius: 999px; font-size: 0.8rem; font-weight: bold; }
32
+ .leave-btn {
33
+ background: var(--danger); color: white; border: none; padding: 0.5rem 1rem; border-radius: 0.5rem;
34
+ font-weight: bold; cursor: pointer; transition: opacity 0.2s;
35
+ }
36
+ .leave-btn:hover { opacity: 0.8; }
37
+ main {
38
+ flex: 1; display: flex; overflow: hidden;
39
+ }
40
+ .sidebar {
41
+ width: 250px; background: var(--bg-panel); border-right: 1px solid var(--border);
42
+ padding: 1rem; overflow-y: auto;
43
+ }
44
+ .chat-area {
45
+ flex: 1; display: flex; flex-direction: column; background: var(--bg);
46
+ }
47
+ .messages {
48
+ flex: 1; padding: 1.5rem; overflow-y: auto; display: flex; flex-direction: column; gap: 1rem;
49
+ }
50
+ .message { max-width: 70%; padding: 0.8rem 1rem; border-radius: 1rem; line-height: 1.4; animation: popIn 0.3s ease-out; }
51
+ .message.system { max-width: 100%; align-self: center; background: transparent; color: #94a3b8; font-size: 0.9rem; font-style: italic; text-align: center; }
52
+ .message.self { align-self: flex-end; background: var(--primary); color: #000; border-bottom-right-radius: 0.25rem; }
53
+ .message.other { align-self: flex-start; background: var(--bg-panel); border-bottom-left-radius: 0.25rem; }
54
+ .message-header { font-size: 0.75rem; margin-bottom: 0.25rem; opacity: 0.8; }
55
+ .input-area {
56
+ padding: 1rem; background: var(--bg-panel); border-top: 1px solid var(--border); display: flex; gap: 1rem;
57
+ }
58
+ input[type="text"] {
59
+ flex: 1; background: var(--bg); border: 1px solid var(--border); color: var(--text);
60
+ padding: 0.75rem 1rem; border-radius: 0.5rem; outline: none; transition: border-color 0.2s;
61
+ }
62
+ input[type="text"]:focus { border-color: var(--primary); }
63
+ button.send {
64
+ background: var(--accent); color: white; border: none; padding: 0 1.5rem; border-radius: 0.5rem;
65
+ font-weight: bold; cursor: pointer; transition: transform 0.1s, opacity 0.2s;
66
+ }
67
+ button.send:active { transform: scale(0.95); }
68
+ @keyframes popIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
69
+
70
+ .user-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.5rem; }
71
+ .user-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem; border-radius: 0.5rem; background: var(--bg); }
72
+ .user-item::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: #22c55e; }
73
+ </style>
74
+ </head>
75
+ <body>
76
+ <header>
77
+ <h1>πŸ’¬ SecureLink Chat <span class="badge" id="conn-count">0 connected</span></h1>
78
+ <button class="leave-btn" id="leave-btn">Leave Room</button>
79
+ </header>
80
+ <main>
81
+ <div class="sidebar">
82
+ <h3 style="margin-top:0; font-size: 0.9rem; color: #94a3b8; text-transform: uppercase;">Participants</h3>
83
+ <ul class="user-list" id="users"></ul>
84
+ </div>
85
+ <div class="chat-area">
86
+ <div class="messages" id="msgs"></div>
87
+ <form class="input-area" id="chat-form">
88
+ <input type="text" id="msg-input" placeholder="Type a secure message..." autocomplete="off" disabled>
89
+ <button type="submit" class="send" id="send-btn" disabled>Send</button>
90
+ </form>
91
+ </div>
92
+ </main>
93
+
94
+ <script>
95
+ const isHost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
96
+ let username = isHost ? 'Host' : prompt('Enter your name for the chat:', 'Client_' + Math.floor(Math.random()*1000));
97
+ if (!username) username = 'Anonymous';
98
+
99
+ const sessionPassword = window.location.hash.substring(1);
100
+ if (!sessionPassword) {
101
+ document.body.innerHTML = '<h2 style="text-align:center; margin-top:20vh; color:red;">Fatal: Missing session password in URL hash. Cannot decrypt E2E chat.</h2>';
102
+ throw new Error("Missing password");
103
+ }
104
+
105
+ const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
106
+ const ws = new WebSocket(wsProtocol + '//' + window.location.host);
107
+
108
+ const msgs = document.getElementById('msgs');
109
+ const form = document.getElementById('chat-form');
110
+ const input = document.getElementById('msg-input');
111
+ const sendBtn = document.getElementById('send-btn');
112
+ const leaveBtn = document.getElementById('leave-btn');
113
+ const usersList = document.getElementById('users');
114
+ const connCount = document.getElementById('conn-count');
115
+
116
+ // ─── Web Crypto E2E AES-GCM ──────────────────────────────────
117
+ const enc = new TextEncoder();
118
+ const dec = new TextDecoder();
119
+
120
+ function buf2hex(buffer) { return [...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')).join(''); }
121
+ function hex2buf(hexString) { return new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); }
122
+
123
+ async function deriveKey(password, saltBuffer) {
124
+ const keyMaterial = await crypto.subtle.importKey(
125
+ "raw", enc.encode(password), {name: "PBKDF2"}, false, ["deriveBits", "deriveKey"]
126
+ );
127
+ return crypto.subtle.deriveKey(
128
+ { name: "PBKDF2", salt: saltBuffer, iterations: 100000, hash: "SHA-256" },
129
+ keyMaterial, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]
130
+ );
131
+ }
132
+
133
+ async function encryptPayload(obj) {
134
+ const salt = crypto.getRandomValues(new Uint8Array(16));
135
+ const iv = crypto.getRandomValues(new Uint8Array(12));
136
+ const key = await deriveKey(sessionPassword, salt);
137
+ const ciphertextBuffer = await crypto.subtle.encrypt(
138
+ { name: "AES-GCM", iv: iv }, key, enc.encode(JSON.stringify(obj))
139
+ );
140
+ return {
141
+ salt: buf2hex(salt),
142
+ iv: buf2hex(iv),
143
+ ciphertext: buf2hex(ciphertextBuffer)
144
+ };
145
+ }
146
+
147
+ async function decryptPayload(encObj) {
148
+ try {
149
+ const salt = hex2buf(encObj.salt);
150
+ const iv = hex2buf(encObj.iv);
151
+ const ciphertext = hex2buf(encObj.ciphertext);
152
+ const key = await deriveKey(sessionPassword, salt);
153
+ const decryptedBuffer = await crypto.subtle.decrypt(
154
+ { name: "AES-GCM", iv: iv }, key, ciphertext
155
+ );
156
+ return JSON.parse(dec.decode(decryptedBuffer));
157
+ } catch (e) {
158
+ return null;
159
+ }
160
+ }
161
+
162
+ // ─── UI Logic ────────────────────────────────────────────────
163
+ function appendMessage(msg) {
164
+ const div = document.createElement('div');
165
+ if (msg.type === 'system') {
166
+ div.className = 'message system';
167
+ div.textContent = msg.text;
168
+ } else {
169
+ div.className = 'message ' + (msg.sender === username ? 'self' : 'other');
170
+ div.innerHTML = '<div class="message-header">' + msg.sender + ' β€’ ' + msg.time + '</div><div>' + msg.text + '</div>';
171
+ }
172
+ msgs.appendChild(div);
173
+ msgs.scrollTop = msgs.scrollHeight;
174
+ }
175
+
176
+ function updateUsers(users) {
177
+ usersList.innerHTML = '';
178
+ users.forEach(u => {
179
+ const li = document.createElement('li');
180
+ li.className = 'user-item';
181
+ li.textContent = u;
182
+ usersList.appendChild(li);
183
+ });
184
+ connCount.textContent = users.length + ' connected';
185
+ }
186
+
187
+ ws.onopen = async () => {
188
+ input.disabled = false;
189
+ sendBtn.disabled = false;
190
+ input.focus();
191
+
192
+ const encPayload = await encryptPayload({ type: 'join', sender: username });
193
+ ws.send(JSON.stringify({ type: 'e2e', payload: encPayload }));
194
+ };
195
+
196
+ ws.onmessage = async (event) => {
197
+ const data = JSON.parse(event.data);
198
+ if (data.type === 'state') {
199
+ updateUsers(data.users);
200
+ } else if (data.type === 'close') {
201
+ appendMessage({ type: 'system', text: 'The Host has closed the chat room. You may leave now.' });
202
+ ws.close();
203
+ input.disabled = true;
204
+ sendBtn.disabled = true;
205
+ } else if (data.type === 'e2e') {
206
+ // Decrypt the incoming E2E message
207
+ const decrypted = await decryptPayload(data.payload);
208
+ if (decrypted) {
209
+ if (decrypted.type === 'join') {
210
+ appendMessage({ type: 'system', text: \`\${decrypted.sender} has joined the chat (E2E Encrypted)\` });
211
+ } else if (decrypted.type === 'chat') {
212
+ appendMessage(decrypted);
213
+ }
214
+ }
215
+ } else {
216
+ appendMessage(data);
217
+ }
218
+ };
219
+
220
+ ws.onclose = () => {
221
+ appendMessage({ type: 'system', text: 'Connection closed.' });
222
+ input.disabled = true;
223
+ sendBtn.disabled = true;
224
+ };
225
+
226
+ form.onsubmit = async (e) => {
227
+ e.preventDefault();
228
+ const text = input.value.trim();
229
+ if (!text) return;
230
+
231
+ const msgObj = { type: 'chat', sender: username, text: text, time: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) };
232
+ const encPayload = await encryptPayload(msgObj);
233
+
234
+ ws.send(JSON.stringify({ type: 'e2e', payload: encPayload }));
235
+ input.value = '';
236
+ };
237
+
238
+ leaveBtn.onclick = () => {
239
+ if (isHost) {
240
+ if(confirm('Are you sure you want to close the chat room for everyone?')) {
241
+ ws.send(JSON.stringify({ type: 'host_close' }));
242
+ window.close();
243
+ }
244
+ } else {
245
+ ws.close();
246
+ window.close();
247
+ document.body.innerHTML = '<h2 style="text-align:center; margin-top:20vh;">You have left the chat. You can close this tab.</h2>';
248
+ }
249
+ };
250
+ </script>
251
+ </body>
252
+ </html>
253
+ `;
254
+
255
+ export async function startChatServer(onClose) {
256
+ return new Promise((resolve) => {
257
+ const server = http.createServer((req, res) => {
258
+ if (req.url === '/') {
259
+ res.writeHead(200, { 'Content-Type': 'text/html' });
260
+ res.end(HTML_CONTENT);
261
+ } else {
262
+ res.writeHead(404);
263
+ res.end('Not found');
264
+ }
265
+ });
266
+
267
+ const wss = new WebSocketServer({ server });
268
+ const clients = new Map(); // ws -> username
269
+
270
+ function broadcastState() {
271
+ const users = Array.from(clients.values());
272
+ const payload = JSON.stringify({ type: 'state', users });
273
+ for (const client of wss.clients) {
274
+ if (client.readyState === 1) client.send(payload);
275
+ }
276
+ }
277
+
278
+ function broadcastMsg(msg) {
279
+ const payload = JSON.stringify(msg);
280
+ for (const client of wss.clients) {
281
+ if (client.readyState === 1) client.send(payload);
282
+ }
283
+ }
284
+
285
+ wss.on('connection', (ws) => {
286
+ ws.on('message', (message) => {
287
+ try {
288
+ const data = JSON.parse(message);
289
+
290
+ if (data.type === 'e2e') {
291
+ // E2E messages just get forwarded to all clients
292
+ broadcastMsg(data);
293
+ } else if (data.type === 'join_event') {
294
+ // For updating participant count dynamically if we wanted to extract sender,
295
+ // but since E2E hides sender, we rely on connection count
296
+ } else if (data.type === 'host_close') {
297
+ broadcastMsg({ type: 'close' });
298
+ server.close();
299
+ if (onClose) onClose();
300
+ }
301
+ } catch (e) {
302
+ // ignore invalid parse
303
+ }
304
+ });
305
+
306
+ ws.on('close', () => {
307
+ // Since we can't read the encrypted username, we just update state
308
+ clients.delete(ws);
309
+ broadcastState();
310
+ });
311
+ });
312
+
313
+ // When connection opens, just assign a generic ID to count them
314
+ wss.on('connection', (ws) => {
315
+ clients.set(ws, `User_${Math.floor(Math.random()*1000)}`);
316
+ broadcastState();
317
+ });
318
+
319
+ server.listen(0, '127.0.0.1', () => {
320
+ const port = server.address().port;
321
+ resolve({ port, server });
322
+ });
323
+ });
324
+ }
325
+
326
+ export async function openLocalChatUI(port, password) {
327
+ try {
328
+ const chatUrl = `http://localhost:${port}#${password}`;
329
+ await open(chatUrl);
330
+ } catch {
331
+ console.log(chalk.dim(` Unable to auto-open browser. Visit http://localhost:${port}#${password}`));
332
+ }
333
+ }