@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 +32 -9
- package/package.json +1 -1
- package/src/cli.js +4 -0
- package/src/lib/broker.js +4 -1
- package/src/modes/host.js +42 -25
- package/src/server.js +6 -3
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
|
-
|
|
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,
|
|
75
|
-
2. **Host** encrypts the
|
|
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
|
|
78
|
-
5. On `Ctrl+C`, `tree-kill` initiates a graceful shutdown, revokes the UID from the broker, and
|
|
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
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
1015
|
-
await waitForValue(() =>
|
|
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,
|
|
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
|
-
//
|
|
223
|
-
const hostToken =
|
|
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, {
|