@miraj181/ipingyou 1.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/LICENSE +21 -0
- package/README.md +133 -0
- package/package.json +62 -0
- package/src/cli.js +241 -0
- package/src/lib/animations.js +226 -0
- package/src/lib/cleanup.js +131 -0
- package/src/lib/crypto.js +53 -0
- package/src/lib/platform.js +179 -0
- package/src/lib/uid.js +23 -0
- package/src/modes/client.js +322 -0
- package/src/modes/host.js +317 -0
- package/src/server.js +144 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================
|
|
3
|
+
* Host Mode — "Allow Remote Access"
|
|
4
|
+
* ============================================================
|
|
5
|
+
* 1. Generate a session UID
|
|
6
|
+
* 2. Ensure local SSH service is running
|
|
7
|
+
* 3. Spawn cloudflared tunnel → localhost:22
|
|
8
|
+
* 4. ENCRYPT tunnel URL locally, send ciphertext to Broker
|
|
9
|
+
* 5. Monitor connections & provide termination controls
|
|
10
|
+
*
|
|
11
|
+
* Security: The broker NEVER sees the plaintext tunnel URL.
|
|
12
|
+
* Only someone with the shared SECRET_KEY can decrypt.
|
|
13
|
+
* ============================================================
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { execa, execaCommand } from 'execa';
|
|
17
|
+
import chalk from 'chalk';
|
|
18
|
+
import inquirer from 'inquirer';
|
|
19
|
+
import { generateUID } from '../lib/uid.js';
|
|
20
|
+
import { encrypt } from '../lib/crypto.js';
|
|
21
|
+
import { trackPID, untrackPID, setRevokeOnExit } from '../lib/cleanup.js';
|
|
22
|
+
import { detectOS } from '../lib/platform.js';
|
|
23
|
+
import { createSpinner, cryptoSpinner, tunnelSpinner, networkSpinner, typeText } from '../lib/animations.js';
|
|
24
|
+
|
|
25
|
+
const BROKER_URL = process.env.BROKER_URL || 'http://localhost:4000';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Ensure the local SSH server is running.
|
|
29
|
+
*/
|
|
30
|
+
async function ensureSSHRunning() {
|
|
31
|
+
const spinner = createSpinner('Checking SSH service...', networkSpinner).start();
|
|
32
|
+
const osInfo = detectOS();
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
if (osInfo.isLinux) {
|
|
36
|
+
try {
|
|
37
|
+
await execaCommand('systemctl is-active ssh', { reject: true });
|
|
38
|
+
spinner.succeed('SSH service is active');
|
|
39
|
+
} catch {
|
|
40
|
+
spinner.text = 'Starting SSH service...';
|
|
41
|
+
try {
|
|
42
|
+
await execaCommand('sudo systemctl start ssh', { stdio: 'inherit' });
|
|
43
|
+
spinner.succeed('SSH service started');
|
|
44
|
+
} catch {
|
|
45
|
+
await execaCommand('sudo systemctl start sshd', { stdio: 'inherit' });
|
|
46
|
+
spinner.succeed('SSH service started (sshd)');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} else if (osInfo.isMac) {
|
|
50
|
+
try {
|
|
51
|
+
const { stdout } = await execaCommand('sudo systemsetup -getremotelogin', { reject: false });
|
|
52
|
+
if (stdout.toLowerCase().includes('off')) {
|
|
53
|
+
spinner.text = 'Enabling Remote Login...';
|
|
54
|
+
await execaCommand('sudo systemsetup -setremotelogin on', { stdio: 'inherit' });
|
|
55
|
+
spinner.succeed('Remote Login enabled');
|
|
56
|
+
} else {
|
|
57
|
+
spinner.succeed('SSH (Remote Login) is active');
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
spinner.warn('Could not verify SSH status — ensure Remote Login is enabled in System Preferences');
|
|
61
|
+
}
|
|
62
|
+
} else if (osInfo.isWindows) {
|
|
63
|
+
try {
|
|
64
|
+
const { stdout } = await execaCommand('sc query sshd', { reject: false });
|
|
65
|
+
if (stdout.includes('STOPPED')) {
|
|
66
|
+
spinner.text = 'Starting OpenSSH Server...';
|
|
67
|
+
await execaCommand('net start sshd', { stdio: 'inherit' });
|
|
68
|
+
spinner.succeed('OpenSSH Server started');
|
|
69
|
+
} else if (stdout.includes('RUNNING')) {
|
|
70
|
+
spinner.succeed('OpenSSH Server is running');
|
|
71
|
+
} else {
|
|
72
|
+
spinner.warn('OpenSSH Server status unknown — ensure it is installed');
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
spinner.warn('Could not check SSH service — ensure OpenSSH Server is installed');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
spinner.fail(`SSH check failed: ${err.message}`);
|
|
80
|
+
console.log(chalk.dim(' Continue anyway? The tunnel will still start, but SSH connections may fail.'));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Spawn cloudflared tunnel and extract the generated URL.
|
|
86
|
+
*/
|
|
87
|
+
async function spawnTunnel() {
|
|
88
|
+
const spinner = createSpinner('Starting Cloudflare tunnel...', tunnelSpinner).start();
|
|
89
|
+
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
const child = execa('cloudflared', ['tunnel', '--url', 'ssh://localhost:22'], {
|
|
92
|
+
reject: false,
|
|
93
|
+
all: true,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
trackPID(child.pid);
|
|
97
|
+
let tunnelUrl = null;
|
|
98
|
+
let resolved = false;
|
|
99
|
+
|
|
100
|
+
child.all.on('data', (chunk) => {
|
|
101
|
+
const text = chunk.toString();
|
|
102
|
+
const match = text.match(/https:\/\/[-0-9a-z]+\.trycloudflare\.com/);
|
|
103
|
+
if (match && !resolved) {
|
|
104
|
+
tunnelUrl = match[0];
|
|
105
|
+
resolved = true;
|
|
106
|
+
spinner.succeed(`Tunnel active: ${chalk.cyan(tunnelUrl)}`);
|
|
107
|
+
resolve({ process: child, url: tunnelUrl });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
child.on('exit', (code) => {
|
|
112
|
+
untrackPID(child.pid);
|
|
113
|
+
if (!resolved) {
|
|
114
|
+
spinner.fail('Cloudflare tunnel exited before generating URL');
|
|
115
|
+
reject(new Error(`cloudflared exited with code ${code}`));
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
child.on('error', (err) => {
|
|
120
|
+
untrackPID(child.pid);
|
|
121
|
+
if (!resolved) {
|
|
122
|
+
spinner.fail(`Tunnel error: ${err.message}`);
|
|
123
|
+
reject(err);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
setTimeout(() => {
|
|
128
|
+
if (!resolved) {
|
|
129
|
+
spinner.fail('Timeout: No tunnel URL received after 30 seconds');
|
|
130
|
+
reject(new Error('Tunnel timeout'));
|
|
131
|
+
}
|
|
132
|
+
}, 30000);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Encrypt tunnel URL and register with the Central Broker.
|
|
138
|
+
*/
|
|
139
|
+
async function registerWithBroker(uid, tunnelUrl) {
|
|
140
|
+
const spinner = createSpinner('Encrypting tunnel URL...', cryptoSpinner).start();
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Encrypt the tunnel URL LOCALLY before sending
|
|
144
|
+
await new Promise(r => setTimeout(r, 600)); // animation effect
|
|
145
|
+
const encrypted = encrypt(tunnelUrl);
|
|
146
|
+
|
|
147
|
+
spinner.text = 'Registering with broker...';
|
|
148
|
+
|
|
149
|
+
const res = await fetch(`${BROKER_URL}/register`, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: { 'Content-Type': 'application/json' },
|
|
152
|
+
body: JSON.stringify({
|
|
153
|
+
uid,
|
|
154
|
+
iv: encrypted.iv,
|
|
155
|
+
ciphertext: encrypted.ciphertext,
|
|
156
|
+
}),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (!res.ok) {
|
|
160
|
+
const data = await res.json().catch(() => ({}));
|
|
161
|
+
throw new Error(data.error || `HTTP ${res.status}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
spinner.succeed(`Registered with broker ${chalk.dim(`(${BROKER_URL})`)} ${chalk.green('[E2E encrypted]')}`);
|
|
165
|
+
return true;
|
|
166
|
+
} catch (err) {
|
|
167
|
+
spinner.fail(`Broker registration failed: ${err.message}`);
|
|
168
|
+
console.error(chalk.red(` ❌ Error: ${err.message}`));
|
|
169
|
+
console.log(chalk.yellow(' ⚠️ Remote clients won\'t be able to find you without the broker.'));
|
|
170
|
+
console.log(chalk.dim(' Share the tunnel URL directly if needed.'));
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Monitor active connections to port 22.
|
|
177
|
+
*/
|
|
178
|
+
async function getConnectedIPs() {
|
|
179
|
+
const osInfo = detectOS();
|
|
180
|
+
try {
|
|
181
|
+
let cmd;
|
|
182
|
+
if (osInfo.isWindows) {
|
|
183
|
+
cmd = 'netstat -an | findstr :22 | findstr ESTABLISHED';
|
|
184
|
+
} else {
|
|
185
|
+
cmd = "ss -tn sport = :22 | grep ESTAB | awk '{print $5}' | cut -d: -f1";
|
|
186
|
+
}
|
|
187
|
+
const { stdout } = await execaCommand(cmd, { shell: true, reject: false });
|
|
188
|
+
return stdout.split('\n').filter(Boolean).map(ip => ip.trim());
|
|
189
|
+
} catch {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Display the host dashboard and handle user input.
|
|
196
|
+
*/
|
|
197
|
+
async function hostDashboard(uid, tunnelUrl) {
|
|
198
|
+
console.log('');
|
|
199
|
+
console.log(chalk.bold(' ╔════════════════════════════════════════════════════╗'));
|
|
200
|
+
console.log(chalk.bold(' ║ 🛡️ SecureLink — HOST MODE ACTIVE ║'));
|
|
201
|
+
console.log(chalk.bold(' ╠════════════════════════════════════════════════════╣'));
|
|
202
|
+
console.log(` ║ ${chalk.cyan('UID:')} ${chalk.bold.white(uid)} ║`);
|
|
203
|
+
console.log(` ║ ${chalk.cyan('Tunnel:')} ${chalk.dim(tunnelUrl.substring(0, 40))} ║`);
|
|
204
|
+
console.log(` ║ ${chalk.cyan('Broker:')} ${chalk.dim(BROKER_URL.padEnd(25))} ║`);
|
|
205
|
+
console.log(` ║ ${chalk.cyan('Crypto:')} ${chalk.green('AES-256-CBC E2E')} ║`);
|
|
206
|
+
console.log(chalk.bold(' ╠════════════════════════════════════════════════════╣'));
|
|
207
|
+
console.log(` ║ ${chalk.yellow('Share this UID with the person who needs access')} ║`);
|
|
208
|
+
console.log(` ║ ${chalk.dim('Press Ctrl+C to terminate the session')} ║`);
|
|
209
|
+
console.log(chalk.bold(' ╚════════════════════════════════════════════════════╝'));
|
|
210
|
+
console.log('');
|
|
211
|
+
|
|
212
|
+
await typeText(chalk.dim(' Listening for incoming connections...'), 30);
|
|
213
|
+
|
|
214
|
+
const monitorInterval = setInterval(async () => {
|
|
215
|
+
const ips = await getConnectedIPs();
|
|
216
|
+
if (ips.length > 0) {
|
|
217
|
+
console.log(chalk.cyan(` 📡 Active connections (${ips.length}):`));
|
|
218
|
+
ips.forEach(ip => console.log(chalk.dim(` → ${ip}`)));
|
|
219
|
+
}
|
|
220
|
+
}, 15000);
|
|
221
|
+
|
|
222
|
+
const waitForAction = async () => {
|
|
223
|
+
try {
|
|
224
|
+
const { action } = await inquirer.prompt([
|
|
225
|
+
{
|
|
226
|
+
type: 'list',
|
|
227
|
+
name: 'action',
|
|
228
|
+
message: 'Host Controls:',
|
|
229
|
+
choices: [
|
|
230
|
+
{ name: '📡 Show connected clients', value: 'show' },
|
|
231
|
+
{ name: '🔄 Re-register with broker', value: 'reregister' },
|
|
232
|
+
{ name: '🚫 Terminate all connections', value: 'terminate' },
|
|
233
|
+
{ name: '❌ Shut down session', value: 'exit' },
|
|
234
|
+
],
|
|
235
|
+
},
|
|
236
|
+
]);
|
|
237
|
+
|
|
238
|
+
switch (action) {
|
|
239
|
+
case 'show': {
|
|
240
|
+
const ips = await getConnectedIPs();
|
|
241
|
+
if (ips.length === 0) {
|
|
242
|
+
console.log(chalk.dim(' No active connections.'));
|
|
243
|
+
} else {
|
|
244
|
+
console.log(chalk.cyan(` 📡 ${ips.length} connected client(s):`));
|
|
245
|
+
ips.forEach(ip => console.log(` ${chalk.white('→')} ${ip}`));
|
|
246
|
+
}
|
|
247
|
+
return waitForAction();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
case 'reregister':
|
|
251
|
+
await registerWithBroker(uid, tunnelUrl);
|
|
252
|
+
return waitForAction();
|
|
253
|
+
|
|
254
|
+
case 'terminate': {
|
|
255
|
+
const spinner = createSpinner('Terminating active SSH sessions...', networkSpinner).start();
|
|
256
|
+
try {
|
|
257
|
+
if (process.platform === 'win32') {
|
|
258
|
+
await execaCommand('taskkill /F /IM sshd.exe', { reject: false });
|
|
259
|
+
} else {
|
|
260
|
+
await execaCommand("pkill -f 'sshd:.*@'", { shell: true, reject: false });
|
|
261
|
+
}
|
|
262
|
+
spinner.succeed('All client SSH sessions terminated');
|
|
263
|
+
} catch {
|
|
264
|
+
spinner.warn('Could not terminate sessions (none active?)');
|
|
265
|
+
}
|
|
266
|
+
return waitForAction();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
case 'exit':
|
|
270
|
+
clearInterval(monitorInterval);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
} catch (err) {
|
|
274
|
+
clearInterval(monitorInterval);
|
|
275
|
+
throw err;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
await waitForAction();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Main Host Mode entry point.
|
|
284
|
+
*/
|
|
285
|
+
export async function startHostMode() {
|
|
286
|
+
console.log('');
|
|
287
|
+
console.log(chalk.bold.cyan(' 🔒 HOST MODE — Allow Remote Access'));
|
|
288
|
+
console.log(chalk.dim(' ─────────────────────────────────────'));
|
|
289
|
+
console.log('');
|
|
290
|
+
|
|
291
|
+
const uid = generateUID();
|
|
292
|
+
console.log(` ${chalk.green('✓')} Session UID: ${chalk.bold.white(uid)}`);
|
|
293
|
+
console.log('');
|
|
294
|
+
|
|
295
|
+
await ensureSSHRunning();
|
|
296
|
+
|
|
297
|
+
let tunnel;
|
|
298
|
+
try {
|
|
299
|
+
tunnel = await spawnTunnel();
|
|
300
|
+
} catch (err) {
|
|
301
|
+
console.error(chalk.red(`\n ❌ FATAL: Failed to start tunnel`));
|
|
302
|
+
console.error(chalk.red(` Error: ${err.message}`));
|
|
303
|
+
console.log(chalk.dim(' Make sure cloudflared is installed and in your PATH.'));
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const registered = await registerWithBroker(uid, tunnel.url);
|
|
308
|
+
if (!registered) {
|
|
309
|
+
console.error(chalk.red(`\n ❌ FATAL: Could not register with broker at ${BROKER_URL}`));
|
|
310
|
+
console.log(chalk.dim(' Is the broker running? Try: npx ipingyou broker'));
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
setRevokeOnExit(uid, BROKER_URL);
|
|
315
|
+
|
|
316
|
+
await hostDashboard(uid, tunnel.url);
|
|
317
|
+
}
|
package/src/server.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================
|
|
3
|
+
* SecureLink-CLI — Central Broker Server
|
|
4
|
+
* ============================================================
|
|
5
|
+
* A lightweight REST relay that pairs hosts and clients via
|
|
6
|
+
* temporary UIDs. The broker is a DUMB PIPE — it stores and
|
|
7
|
+
* returns encrypted blobs without ever seeing plaintext.
|
|
8
|
+
* Encryption/decryption happens ONLY on the CLI side.
|
|
9
|
+
* ============================================================
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import 'dotenv/config';
|
|
13
|
+
import express from 'express';
|
|
14
|
+
|
|
15
|
+
const app = express();
|
|
16
|
+
app.use(express.json());
|
|
17
|
+
|
|
18
|
+
// ─── In-memory store — auto-expire after TTL ─────────────────
|
|
19
|
+
const TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
20
|
+
const store = new Map(); // uid → { iv, ciphertext, createdAt }
|
|
21
|
+
|
|
22
|
+
function pruneExpired() {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
for (const [uid, entry] of store) {
|
|
25
|
+
if (now - entry.createdAt > TTL_MS) {
|
|
26
|
+
store.delete(uid);
|
|
27
|
+
console.log(`🗑️ Expired UID: ${uid}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Run pruning every 5 minutes
|
|
33
|
+
setInterval(pruneExpired, 5 * 60 * 1000);
|
|
34
|
+
|
|
35
|
+
// ─── Routes ──────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
// Health
|
|
38
|
+
app.get('/health', (_req, res) => {
|
|
39
|
+
res.json({ status: 'ok', uptime: process.uptime(), activeUIDs: store.size });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* POST /register
|
|
44
|
+
* Body: { uid: string, iv: string, ciphertext: string }
|
|
45
|
+
*
|
|
46
|
+
* The broker receives an ALREADY-ENCRYPTED blob from the host CLI.
|
|
47
|
+
* It never decrypts — just stores the { iv, ciphertext } pair.
|
|
48
|
+
*/
|
|
49
|
+
app.post('/register', (req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
const { uid, iv, ciphertext } = req.body;
|
|
52
|
+
|
|
53
|
+
if (!uid || !iv || !ciphertext) {
|
|
54
|
+
return res.status(400).json({ error: 'Missing uid, iv, or ciphertext' });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (typeof uid !== 'string' || uid.length < 6 || uid.length > 16) {
|
|
58
|
+
return res.status(400).json({ error: 'Invalid UID format (6-16 chars)' });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Validate IV format — must be 32 hex chars (16 bytes)
|
|
62
|
+
if (!/^[a-f0-9]{32}$/i.test(iv)) {
|
|
63
|
+
return res.status(400).json({ error: 'Invalid IV format (expected 32 hex chars)' });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Validate ciphertext is non-empty base64
|
|
67
|
+
if (typeof ciphertext !== 'string' || ciphertext.length === 0) {
|
|
68
|
+
return res.status(400).json({ error: 'Invalid ciphertext' });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Store the encrypted blob as-is — broker NEVER decrypts
|
|
72
|
+
store.set(uid, {
|
|
73
|
+
iv,
|
|
74
|
+
ciphertext,
|
|
75
|
+
createdAt: Date.now(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
console.log(`✅ [${new Date().toLocaleTimeString()}] Registered UID: ${uid} (encrypted, ${ciphertext.length} bytes)`);
|
|
79
|
+
res.json({ status: 'registered', uid, expiresIn: '1 hour' });
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.error('❌ Register error:', err.message);
|
|
82
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* GET /resolve/:uid
|
|
88
|
+
* Returns the ENCRYPTED blob { iv, ciphertext } for a given UID.
|
|
89
|
+
* The client CLI decrypts it locally — the broker never sees plaintext.
|
|
90
|
+
*/
|
|
91
|
+
app.get('/resolve/:uid', (req, res) => {
|
|
92
|
+
try {
|
|
93
|
+
const { uid } = req.params;
|
|
94
|
+
const entry = store.get(uid);
|
|
95
|
+
|
|
96
|
+
if (!entry) {
|
|
97
|
+
return res.status(404).json({ error: 'UID not found or expired' });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check TTL
|
|
101
|
+
if (Date.now() - entry.createdAt > TTL_MS) {
|
|
102
|
+
store.delete(uid);
|
|
103
|
+
return res.status(410).json({ error: 'UID expired' });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log(`🔍 [${new Date().toLocaleTimeString()}] Resolved UID: ${uid} (returning encrypted blob)`);
|
|
107
|
+
|
|
108
|
+
// Return encrypted blob — client decrypts
|
|
109
|
+
res.json({ uid, iv: entry.iv, ciphertext: entry.ciphertext });
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error('❌ Resolve error:', err.message);
|
|
112
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* DELETE /revoke/:uid
|
|
118
|
+
* Allows host to explicitly remove their UID before expiry.
|
|
119
|
+
*/
|
|
120
|
+
app.delete('/revoke/:uid', (req, res) => {
|
|
121
|
+
const { uid } = req.params;
|
|
122
|
+
const existed = store.delete(uid);
|
|
123
|
+
console.log(`🚫 [${new Date().toLocaleTimeString()}] Revoked UID: ${uid} (existed: ${existed})`);
|
|
124
|
+
res.json({ status: existed ? 'revoked' : 'not_found' });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ─── Launch ──────────────────────────────────────────────────
|
|
128
|
+
// Render injects PORT; locally we use BROKER_PORT; fallback 4000
|
|
129
|
+
const PORT = process.env.PORT || process.env.BROKER_PORT || 4000;
|
|
130
|
+
const HOST = process.env.HOST || '0.0.0.0';
|
|
131
|
+
|
|
132
|
+
app.listen(PORT, HOST, () => {
|
|
133
|
+
console.log('');
|
|
134
|
+
console.log(' ╔══════════════════════════════════════════╗');
|
|
135
|
+
console.log(' ║ 🔗 SecureLink Broker — Active ║');
|
|
136
|
+
console.log(` ║ 📡 Port: ${String(PORT).padEnd(29)}║`);
|
|
137
|
+
console.log(' ║ 🔒 Zero-Knowledge Encrypted Store ║');
|
|
138
|
+
console.log(' ║ ⏱️ TTL: 1 hour per UID ║');
|
|
139
|
+
console.log(' ║ 🚫 Broker NEVER sees plaintext ║');
|
|
140
|
+
console.log(' ╚══════════════════════════════════════════╝');
|
|
141
|
+
console.log('');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
export default app;
|