pubblue 0.4.1 → 0.4.3
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/dist/chunk-YHFY3TW5.js +456 -0
- package/dist/index.js +53 -24
- package/dist/tunnel-daemon-QPXIGRW7.js +9 -0
- package/dist/tunnel-daemon-entry.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-77HFJKLW.js +0 -342
- package/dist/tunnel-daemon-QBJSX4JM.js +0 -7
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CHANNELS,
|
|
3
|
+
CONTROL_CHANNEL,
|
|
4
|
+
decodeMessage,
|
|
5
|
+
encodeMessage
|
|
6
|
+
} from "./chunk-56IKFMJ2.js";
|
|
7
|
+
|
|
8
|
+
// src/lib/tunnel-daemon.ts
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as net from "net";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
var OFFER_TIMEOUT_MS = 1e4;
|
|
13
|
+
var SIGNAL_POLL_WAITING_MS = 500;
|
|
14
|
+
var SIGNAL_POLL_CONNECTED_MS = 2e3;
|
|
15
|
+
var RECOVERY_DELAY_MS = 1e3;
|
|
16
|
+
var NOT_CONNECTED_WRITE_ERROR = "No browser connected. Ask the user to open the tunnel URL first, then retry.";
|
|
17
|
+
function getTunnelWriteReadinessError(isConnected) {
|
|
18
|
+
return isConnected ? null : NOT_CONNECTED_WRITE_ERROR;
|
|
19
|
+
}
|
|
20
|
+
async function startDaemon(config) {
|
|
21
|
+
const { tunnelId, apiClient, socketPath, infoPath } = config;
|
|
22
|
+
const ndc = await import("node-datachannel");
|
|
23
|
+
const buffer = { messages: [] };
|
|
24
|
+
const startTime = Date.now();
|
|
25
|
+
let stopped = false;
|
|
26
|
+
let connected = false;
|
|
27
|
+
let recovering = false;
|
|
28
|
+
let remoteDescriptionApplied = false;
|
|
29
|
+
let lastBrowserCandidateCount = 0;
|
|
30
|
+
let lastSentCandidateCount = 0;
|
|
31
|
+
const pendingRemoteCandidates = [];
|
|
32
|
+
const localCandidates = [];
|
|
33
|
+
let peer = null;
|
|
34
|
+
let channels = /* @__PURE__ */ new Map();
|
|
35
|
+
let pendingInboundBinaryMeta = /* @__PURE__ */ new Map();
|
|
36
|
+
let pollingTimer = null;
|
|
37
|
+
let localCandidateInterval = null;
|
|
38
|
+
let localCandidateStopTimer = null;
|
|
39
|
+
let recoveryTimer = null;
|
|
40
|
+
function clearPollingTimer() {
|
|
41
|
+
if (pollingTimer) {
|
|
42
|
+
clearTimeout(pollingTimer);
|
|
43
|
+
pollingTimer = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function clearLocalCandidateTimers() {
|
|
47
|
+
if (localCandidateInterval) {
|
|
48
|
+
clearInterval(localCandidateInterval);
|
|
49
|
+
localCandidateInterval = null;
|
|
50
|
+
}
|
|
51
|
+
if (localCandidateStopTimer) {
|
|
52
|
+
clearTimeout(localCandidateStopTimer);
|
|
53
|
+
localCandidateStopTimer = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function clearRecoveryTimer() {
|
|
57
|
+
if (recoveryTimer) {
|
|
58
|
+
clearTimeout(recoveryTimer);
|
|
59
|
+
recoveryTimer = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function setupChannel(name, dc) {
|
|
63
|
+
channels.set(name, dc);
|
|
64
|
+
dc.onMessage((data) => {
|
|
65
|
+
if (typeof data === "string") {
|
|
66
|
+
const msg = decodeMessage(data);
|
|
67
|
+
if (!msg) return;
|
|
68
|
+
if (msg.type === "binary" && !msg.data) {
|
|
69
|
+
pendingInboundBinaryMeta.set(name, msg);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
buffer.messages.push({ channel: name, msg, timestamp: Date.now() });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const pendingMeta = pendingInboundBinaryMeta.get(name);
|
|
76
|
+
if (pendingMeta) pendingInboundBinaryMeta.delete(name);
|
|
77
|
+
const binMsg = pendingMeta ? {
|
|
78
|
+
id: pendingMeta.id,
|
|
79
|
+
type: "binary",
|
|
80
|
+
data: data.toString("base64"),
|
|
81
|
+
meta: { ...pendingMeta.meta, size: data.length }
|
|
82
|
+
} : {
|
|
83
|
+
id: `bin-${Date.now()}`,
|
|
84
|
+
type: "binary",
|
|
85
|
+
data: data.toString("base64"),
|
|
86
|
+
meta: { size: data.length }
|
|
87
|
+
};
|
|
88
|
+
buffer.messages.push({ channel: name, msg: binMsg, timestamp: Date.now() });
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function openDataChannel(name) {
|
|
92
|
+
if (!peer) throw new Error("PeerConnection not initialized");
|
|
93
|
+
const existing = channels.get(name);
|
|
94
|
+
if (existing) return existing;
|
|
95
|
+
const dc = peer.createDataChannel(name, { ordered: true });
|
|
96
|
+
setupChannel(name, dc);
|
|
97
|
+
return dc;
|
|
98
|
+
}
|
|
99
|
+
async function waitForChannelOpen(dc, timeoutMs = 5e3) {
|
|
100
|
+
if (dc.isOpen()) return;
|
|
101
|
+
await new Promise((resolve, reject) => {
|
|
102
|
+
let settled = false;
|
|
103
|
+
const timeout = setTimeout(() => {
|
|
104
|
+
if (settled) return;
|
|
105
|
+
settled = true;
|
|
106
|
+
reject(new Error("DataChannel open timed out"));
|
|
107
|
+
}, timeoutMs);
|
|
108
|
+
dc.onOpen(() => {
|
|
109
|
+
if (settled) return;
|
|
110
|
+
settled = true;
|
|
111
|
+
clearTimeout(timeout);
|
|
112
|
+
resolve();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function resetNegotiationState() {
|
|
117
|
+
connected = false;
|
|
118
|
+
remoteDescriptionApplied = false;
|
|
119
|
+
lastBrowserCandidateCount = 0;
|
|
120
|
+
lastSentCandidateCount = 0;
|
|
121
|
+
pendingRemoteCandidates.length = 0;
|
|
122
|
+
localCandidates.length = 0;
|
|
123
|
+
clearLocalCandidateTimers();
|
|
124
|
+
}
|
|
125
|
+
function startLocalCandidateFlush() {
|
|
126
|
+
clearLocalCandidateTimers();
|
|
127
|
+
localCandidateInterval = setInterval(async () => {
|
|
128
|
+
if (localCandidates.length <= lastSentCandidateCount) return;
|
|
129
|
+
const newOnes = localCandidates.slice(lastSentCandidateCount);
|
|
130
|
+
lastSentCandidateCount = localCandidates.length;
|
|
131
|
+
await apiClient.signal(tunnelId, { candidates: newOnes }).catch(() => {
|
|
132
|
+
});
|
|
133
|
+
}, 500);
|
|
134
|
+
localCandidateStopTimer = setTimeout(() => {
|
|
135
|
+
clearLocalCandidateTimers();
|
|
136
|
+
}, 3e4);
|
|
137
|
+
}
|
|
138
|
+
function attachPeerHandlers(currentPeer) {
|
|
139
|
+
currentPeer.onLocalCandidate((candidate, mid) => {
|
|
140
|
+
if (stopped || currentPeer !== peer) return;
|
|
141
|
+
localCandidates.push(JSON.stringify({ candidate, sdpMid: mid }));
|
|
142
|
+
});
|
|
143
|
+
currentPeer.onStateChange((state) => {
|
|
144
|
+
if (stopped || currentPeer !== peer) return;
|
|
145
|
+
if (state === "connected") {
|
|
146
|
+
connected = true;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (state === "disconnected" || state === "failed") {
|
|
150
|
+
connected = false;
|
|
151
|
+
scheduleRecovery();
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
currentPeer.onDataChannel((dc) => {
|
|
155
|
+
if (stopped || currentPeer !== peer) return;
|
|
156
|
+
setupChannel(dc.getLabel(), dc);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
function createPeer() {
|
|
160
|
+
const nextPeer = new ndc.PeerConnection("agent", {
|
|
161
|
+
iceServers: ["stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302"]
|
|
162
|
+
});
|
|
163
|
+
peer = nextPeer;
|
|
164
|
+
channels = /* @__PURE__ */ new Map();
|
|
165
|
+
pendingInboundBinaryMeta = /* @__PURE__ */ new Map();
|
|
166
|
+
attachPeerHandlers(nextPeer);
|
|
167
|
+
openDataChannel(CONTROL_CHANNEL);
|
|
168
|
+
openDataChannel(CHANNELS.CHAT);
|
|
169
|
+
openDataChannel(CHANNELS.CANVAS);
|
|
170
|
+
}
|
|
171
|
+
function closeCurrentPeer() {
|
|
172
|
+
for (const dc of channels.values()) {
|
|
173
|
+
try {
|
|
174
|
+
dc.close();
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
channels.clear();
|
|
179
|
+
pendingInboundBinaryMeta.clear();
|
|
180
|
+
if (peer) {
|
|
181
|
+
try {
|
|
182
|
+
peer.close();
|
|
183
|
+
} catch {
|
|
184
|
+
}
|
|
185
|
+
peer = null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function scheduleNextPoll(delayMs) {
|
|
189
|
+
if (stopped) return;
|
|
190
|
+
clearPollingTimer();
|
|
191
|
+
pollingTimer = setTimeout(() => {
|
|
192
|
+
void runPollingLoop();
|
|
193
|
+
}, delayMs);
|
|
194
|
+
}
|
|
195
|
+
async function pollSignalingOnce() {
|
|
196
|
+
const tunnel = await apiClient.get(tunnelId);
|
|
197
|
+
if (tunnel.browserAnswer && !remoteDescriptionApplied) {
|
|
198
|
+
if (!peer) return;
|
|
199
|
+
try {
|
|
200
|
+
const answer = JSON.parse(tunnel.browserAnswer);
|
|
201
|
+
peer.setRemoteDescription(answer.sdp, answer.type);
|
|
202
|
+
remoteDescriptionApplied = true;
|
|
203
|
+
while (pendingRemoteCandidates.length > 0) {
|
|
204
|
+
const next = pendingRemoteCandidates.shift();
|
|
205
|
+
if (!next) break;
|
|
206
|
+
try {
|
|
207
|
+
peer.addRemoteCandidate(next.candidate, next.sdpMid);
|
|
208
|
+
} catch {
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (tunnel.browserCandidates.length > lastBrowserCandidateCount) {
|
|
215
|
+
const newCandidates = tunnel.browserCandidates.slice(lastBrowserCandidateCount);
|
|
216
|
+
lastBrowserCandidateCount = tunnel.browserCandidates.length;
|
|
217
|
+
for (const c of newCandidates) {
|
|
218
|
+
try {
|
|
219
|
+
const parsed = JSON.parse(c);
|
|
220
|
+
if (typeof parsed.candidate !== "string") continue;
|
|
221
|
+
const sdpMid = typeof parsed.sdpMid === "string" ? parsed.sdpMid : "0";
|
|
222
|
+
if (!remoteDescriptionApplied) {
|
|
223
|
+
pendingRemoteCandidates.push({ candidate: parsed.candidate, sdpMid });
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (!peer) continue;
|
|
227
|
+
peer.addRemoteCandidate(parsed.candidate, sdpMid);
|
|
228
|
+
} catch {
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async function runPollingLoop() {
|
|
234
|
+
if (stopped) return;
|
|
235
|
+
try {
|
|
236
|
+
await pollSignalingOnce();
|
|
237
|
+
} catch {
|
|
238
|
+
}
|
|
239
|
+
scheduleNextPoll(remoteDescriptionApplied ? SIGNAL_POLL_CONNECTED_MS : SIGNAL_POLL_WAITING_MS);
|
|
240
|
+
}
|
|
241
|
+
async function runNegotiationCycle() {
|
|
242
|
+
if (!peer) throw new Error("PeerConnection not initialized");
|
|
243
|
+
resetNegotiationState();
|
|
244
|
+
const offer = await generateOffer(peer, OFFER_TIMEOUT_MS);
|
|
245
|
+
await apiClient.signal(tunnelId, { offer });
|
|
246
|
+
startLocalCandidateFlush();
|
|
247
|
+
}
|
|
248
|
+
async function recoverPeer() {
|
|
249
|
+
if (stopped || recovering) return;
|
|
250
|
+
recovering = true;
|
|
251
|
+
try {
|
|
252
|
+
closeCurrentPeer();
|
|
253
|
+
createPeer();
|
|
254
|
+
await runNegotiationCycle();
|
|
255
|
+
} finally {
|
|
256
|
+
recovering = false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function scheduleRecovery(delayMs = RECOVERY_DELAY_MS) {
|
|
260
|
+
if (stopped || recovering || recoveryTimer) return;
|
|
261
|
+
recoveryTimer = setTimeout(() => {
|
|
262
|
+
recoveryTimer = null;
|
|
263
|
+
if (stopped || connected) return;
|
|
264
|
+
void recoverPeer().catch(() => {
|
|
265
|
+
if (!stopped) scheduleRecovery(delayMs);
|
|
266
|
+
});
|
|
267
|
+
}, delayMs);
|
|
268
|
+
}
|
|
269
|
+
if (fs.existsSync(socketPath)) {
|
|
270
|
+
let stale = true;
|
|
271
|
+
try {
|
|
272
|
+
const raw = fs.readFileSync(infoPath, "utf-8");
|
|
273
|
+
const info = JSON.parse(raw);
|
|
274
|
+
process.kill(info.pid, 0);
|
|
275
|
+
stale = false;
|
|
276
|
+
} catch {
|
|
277
|
+
stale = true;
|
|
278
|
+
}
|
|
279
|
+
if (stale) {
|
|
280
|
+
try {
|
|
281
|
+
fs.unlinkSync(socketPath);
|
|
282
|
+
} catch {
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
throw new Error(`Daemon already running (socket: ${socketPath})`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
createPeer();
|
|
289
|
+
const ipcServer = net.createServer((conn) => {
|
|
290
|
+
let data = "";
|
|
291
|
+
conn.on("data", (chunk) => {
|
|
292
|
+
data += chunk.toString();
|
|
293
|
+
const newlineIdx = data.indexOf("\n");
|
|
294
|
+
if (newlineIdx === -1) return;
|
|
295
|
+
const line = data.slice(0, newlineIdx);
|
|
296
|
+
data = data.slice(newlineIdx + 1);
|
|
297
|
+
let request;
|
|
298
|
+
try {
|
|
299
|
+
request = JSON.parse(line);
|
|
300
|
+
} catch {
|
|
301
|
+
conn.write(`${JSON.stringify({ ok: false, error: "Invalid JSON" })}
|
|
302
|
+
`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
handleIpcRequest(request).then((response) => conn.write(`${JSON.stringify(response)}
|
|
306
|
+
`)).catch((err) => conn.write(`${JSON.stringify({ ok: false, error: String(err) })}
|
|
307
|
+
`));
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
ipcServer.listen(socketPath);
|
|
311
|
+
const infoDir = path.dirname(infoPath);
|
|
312
|
+
if (!fs.existsSync(infoDir)) fs.mkdirSync(infoDir, { recursive: true });
|
|
313
|
+
fs.writeFileSync(
|
|
314
|
+
infoPath,
|
|
315
|
+
JSON.stringify({ pid: process.pid, tunnelId, socketPath, startedAt: startTime })
|
|
316
|
+
);
|
|
317
|
+
scheduleNextPoll(0);
|
|
318
|
+
try {
|
|
319
|
+
await runNegotiationCycle();
|
|
320
|
+
} catch (error) {
|
|
321
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
322
|
+
await cleanup();
|
|
323
|
+
throw new Error(`Failed to generate WebRTC offer: ${message}`);
|
|
324
|
+
}
|
|
325
|
+
async function cleanup() {
|
|
326
|
+
if (stopped) return;
|
|
327
|
+
stopped = true;
|
|
328
|
+
clearPollingTimer();
|
|
329
|
+
clearLocalCandidateTimers();
|
|
330
|
+
clearRecoveryTimer();
|
|
331
|
+
closeCurrentPeer();
|
|
332
|
+
ipcServer.close();
|
|
333
|
+
try {
|
|
334
|
+
fs.unlinkSync(socketPath);
|
|
335
|
+
} catch {
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
fs.unlinkSync(infoPath);
|
|
339
|
+
} catch {
|
|
340
|
+
}
|
|
341
|
+
await apiClient.close(tunnelId).catch(() => {
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
async function shutdown() {
|
|
345
|
+
await cleanup();
|
|
346
|
+
process.exit(0);
|
|
347
|
+
}
|
|
348
|
+
process.on("SIGTERM", () => {
|
|
349
|
+
void shutdown();
|
|
350
|
+
});
|
|
351
|
+
process.on("SIGINT", () => {
|
|
352
|
+
void shutdown();
|
|
353
|
+
});
|
|
354
|
+
async function handleIpcRequest(req) {
|
|
355
|
+
switch (req.method) {
|
|
356
|
+
case "write": {
|
|
357
|
+
const channel = req.params.channel || CHANNELS.CHAT;
|
|
358
|
+
const readinessError = getTunnelWriteReadinessError(connected);
|
|
359
|
+
if (readinessError) return { ok: false, error: readinessError };
|
|
360
|
+
const msg = req.params.msg;
|
|
361
|
+
const binaryBase64 = typeof req.params.binaryBase64 === "string" ? req.params.binaryBase64 : void 0;
|
|
362
|
+
let targetDc = channels.get(channel);
|
|
363
|
+
if (!targetDc) targetDc = openDataChannel(channel);
|
|
364
|
+
try {
|
|
365
|
+
await waitForChannelOpen(targetDc);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
368
|
+
return { ok: false, error: `Channel "${channel}" not open: ${message}` };
|
|
369
|
+
}
|
|
370
|
+
if (msg.type === "binary" && binaryBase64) {
|
|
371
|
+
const payload = Buffer.from(binaryBase64, "base64");
|
|
372
|
+
targetDc.sendMessage(
|
|
373
|
+
encodeMessage({
|
|
374
|
+
...msg,
|
|
375
|
+
meta: {
|
|
376
|
+
...msg.meta || {},
|
|
377
|
+
size: payload.length
|
|
378
|
+
}
|
|
379
|
+
})
|
|
380
|
+
);
|
|
381
|
+
targetDc.sendMessageBinary(payload);
|
|
382
|
+
} else {
|
|
383
|
+
targetDc.sendMessage(encodeMessage(msg));
|
|
384
|
+
}
|
|
385
|
+
return { ok: true };
|
|
386
|
+
}
|
|
387
|
+
case "read": {
|
|
388
|
+
const channel = req.params.channel;
|
|
389
|
+
let msgs;
|
|
390
|
+
if (channel) {
|
|
391
|
+
msgs = buffer.messages.filter((m) => m.channel === channel);
|
|
392
|
+
buffer.messages = buffer.messages.filter((m) => m.channel !== channel);
|
|
393
|
+
} else {
|
|
394
|
+
msgs = [...buffer.messages];
|
|
395
|
+
buffer.messages = [];
|
|
396
|
+
}
|
|
397
|
+
return { ok: true, messages: msgs };
|
|
398
|
+
}
|
|
399
|
+
case "channels": {
|
|
400
|
+
const chList = [...channels.keys()].map((name) => ({ name, direction: "bidi" }));
|
|
401
|
+
return { ok: true, channels: chList };
|
|
402
|
+
}
|
|
403
|
+
case "status": {
|
|
404
|
+
return {
|
|
405
|
+
ok: true,
|
|
406
|
+
connected,
|
|
407
|
+
uptime: Math.floor((Date.now() - startTime) / 1e3),
|
|
408
|
+
channels: [...channels.keys()],
|
|
409
|
+
bufferedMessages: buffer.messages.length
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
case "close": {
|
|
413
|
+
void shutdown();
|
|
414
|
+
return { ok: true };
|
|
415
|
+
}
|
|
416
|
+
default:
|
|
417
|
+
return { ok: false, error: `Unknown method: ${req.method}` };
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function generateOffer(peer, timeoutMs) {
|
|
422
|
+
return new Promise((resolve, reject) => {
|
|
423
|
+
let resolved = false;
|
|
424
|
+
const done = (sdp, type) => {
|
|
425
|
+
if (resolved) return;
|
|
426
|
+
resolved = true;
|
|
427
|
+
clearTimeout(timeout);
|
|
428
|
+
resolve(JSON.stringify({ sdp, type }));
|
|
429
|
+
};
|
|
430
|
+
peer.onLocalDescription((sdp, type) => {
|
|
431
|
+
done(sdp, type);
|
|
432
|
+
});
|
|
433
|
+
peer.onGatheringStateChange((state) => {
|
|
434
|
+
if (state === "complete" && !resolved) {
|
|
435
|
+
const desc = peer.localDescription();
|
|
436
|
+
if (desc) done(desc.sdp, desc.type);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
const timeout = setTimeout(() => {
|
|
440
|
+
if (resolved) return;
|
|
441
|
+
const desc = peer.localDescription();
|
|
442
|
+
if (desc) {
|
|
443
|
+
done(desc.sdp, desc.type);
|
|
444
|
+
} else {
|
|
445
|
+
resolved = true;
|
|
446
|
+
reject(new Error(`Timed out after ${timeoutMs}ms`));
|
|
447
|
+
}
|
|
448
|
+
}, timeoutMs);
|
|
449
|
+
peer.setLocalDescription();
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export {
|
|
454
|
+
getTunnelWriteReadinessError,
|
|
455
|
+
startDaemon
|
|
456
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -215,19 +215,22 @@ function isDaemonRunning(tunnelId) {
|
|
|
215
215
|
return false;
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
|
+
function getFollowReadDelayMs(disconnected, consecutiveFailures) {
|
|
219
|
+
if (!disconnected) return 1e3;
|
|
220
|
+
return Math.min(5e3, 1e3 * 2 ** Math.min(consecutiveFailures, 3));
|
|
221
|
+
}
|
|
218
222
|
function registerTunnelCommands(program2) {
|
|
219
223
|
const tunnel = program2.command("tunnel").description("P2P encrypted tunnel to browser");
|
|
220
|
-
tunnel.command("start").description("Start a new tunnel (spawns background daemon)").option("--
|
|
224
|
+
tunnel.command("start").description("Start a new tunnel (spawns background daemon)").option("--expires <duration>", "Auto-close after duration (e.g. 4h, 1d)", "24h").option("--foreground", "Run in foreground (don't fork)").action(async (opts) => {
|
|
221
225
|
await ensureNodeDatachannelAvailable();
|
|
222
226
|
const apiClient = createApiClient();
|
|
223
227
|
const result = await apiClient.create({
|
|
224
|
-
title: opts.title,
|
|
225
228
|
expiresIn: opts.expires
|
|
226
229
|
});
|
|
227
230
|
const socketPath = getSocketPath(result.tunnelId);
|
|
228
231
|
const infoPath = tunnelInfoPath(result.tunnelId);
|
|
229
232
|
if (opts.foreground) {
|
|
230
|
-
const { startDaemon } = await import("./tunnel-daemon-
|
|
233
|
+
const { startDaemon } = await import("./tunnel-daemon-QPXIGRW7.js");
|
|
231
234
|
console.log(`Tunnel started: ${result.url}`);
|
|
232
235
|
console.log(`Tunnel ID: ${result.tunnelId}`);
|
|
233
236
|
console.log(`Expires: ${new Date(result.expiresAt).toISOString()}`);
|
|
@@ -335,21 +338,33 @@ function registerTunnelCommands(program2) {
|
|
|
335
338
|
const tunnelId = tunnelIdArg || await resolveActiveTunnel();
|
|
336
339
|
const socketPath = getSocketPath(tunnelId);
|
|
337
340
|
if (opts.follow) {
|
|
341
|
+
let consecutiveFailures = 0;
|
|
342
|
+
let warnedDisconnected = false;
|
|
338
343
|
while (true) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
344
|
+
try {
|
|
345
|
+
const response = await ipcCall(socketPath, {
|
|
346
|
+
method: "read",
|
|
347
|
+
params: { channel: opts.channel }
|
|
348
|
+
});
|
|
349
|
+
if (warnedDisconnected) {
|
|
350
|
+
console.error("Daemon reconnected.");
|
|
351
|
+
warnedDisconnected = false;
|
|
352
|
+
}
|
|
353
|
+
consecutiveFailures = 0;
|
|
354
|
+
if (response.messages && response.messages.length > 0) {
|
|
355
|
+
for (const m of response.messages) {
|
|
356
|
+
console.log(JSON.stringify(m));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
} catch {
|
|
360
|
+
consecutiveFailures += 1;
|
|
361
|
+
if (!warnedDisconnected) {
|
|
362
|
+
console.error("Daemon disconnected. Waiting for recovery...");
|
|
363
|
+
warnedDisconnected = true;
|
|
350
364
|
}
|
|
351
365
|
}
|
|
352
|
-
|
|
366
|
+
const delayMs = getFollowReadDelayMs(warnedDisconnected, consecutiveFailures);
|
|
367
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
353
368
|
}
|
|
354
369
|
} else {
|
|
355
370
|
const response = await ipcCall(socketPath, {
|
|
@@ -395,9 +410,7 @@ function registerTunnelCommands(program2) {
|
|
|
395
410
|
const age = Math.floor((Date.now() - t.createdAt) / 6e4);
|
|
396
411
|
const running = isDaemonRunning(t.tunnelId) ? "running" : "no daemon";
|
|
397
412
|
const conn = t.hasConnection ? "connected" : "waiting";
|
|
398
|
-
console.log(
|
|
399
|
-
` ${t.tunnelId} ${t.title || "(untitled)"} ${conn} ${running} ${age}m ago`
|
|
400
|
-
);
|
|
413
|
+
console.log(` ${t.tunnelId} ${conn} ${running} ${age}m ago`);
|
|
401
414
|
}
|
|
402
415
|
});
|
|
403
416
|
tunnel.command("close").description("Close a tunnel and stop its daemon").argument("<tunnelId>", "Tunnel ID").action(async (tunnelId) => {
|
|
@@ -568,6 +581,14 @@ async function resolveConfigureApiKey(opts) {
|
|
|
568
581
|
}
|
|
569
582
|
return readApiKeyFromPrompt();
|
|
570
583
|
}
|
|
584
|
+
function resolveVisibilityFlags(opts) {
|
|
585
|
+
if (opts.public && opts.private) {
|
|
586
|
+
throw new Error(`Use only one of --public or --private for ${opts.commandName}.`);
|
|
587
|
+
}
|
|
588
|
+
if (opts.public) return true;
|
|
589
|
+
if (opts.private) return false;
|
|
590
|
+
return void 0;
|
|
591
|
+
}
|
|
571
592
|
function readFile(filePath) {
|
|
572
593
|
const resolved = path3.resolve(filePath);
|
|
573
594
|
if (!fs3.existsSync(resolved)) {
|
|
@@ -579,7 +600,7 @@ function readFile(filePath) {
|
|
|
579
600
|
basename: path3.basename(resolved)
|
|
580
601
|
};
|
|
581
602
|
}
|
|
582
|
-
program.name("pubblue").description("Publish static content and get shareable URLs").version("0.4.
|
|
603
|
+
program.name("pubblue").description("Publish static content and get shareable URLs").version("0.4.3");
|
|
583
604
|
program.command("configure").description("Configure the CLI with your API key").option("--api-key <key>", "Your API key (less secure: appears in shell history)").option("--api-key-stdin", "Read API key from stdin").action(async (opts) => {
|
|
584
605
|
try {
|
|
585
606
|
const apiKey = await resolveConfigureApiKey(opts);
|
|
@@ -591,7 +612,7 @@ program.command("configure").description("Configure the CLI with your API key").
|
|
|
591
612
|
process.exit(1);
|
|
592
613
|
}
|
|
593
614
|
});
|
|
594
|
-
program.command("create").description("Create a new publication").argument("[file]", "Path to the file (reads stdin if omitted)").option("--slug <slug>", "Custom slug for the URL").option("--title <title>", "Title for the publication").option("--private", "Make the publication private (default)").option("--expires <duration>", "Auto-delete after duration (e.g. 1h, 24h, 7d)").action(
|
|
615
|
+
program.command("create").description("Create a new publication").argument("[file]", "Path to the file (reads stdin if omitted)").option("--slug <slug>", "Custom slug for the URL").option("--title <title>", "Title for the publication").option("--public", "Make the publication public").option("--private", "Make the publication private (default)").option("--expires <duration>", "Auto-delete after duration (e.g. 1h, 24h, 7d)").action(
|
|
595
616
|
async (fileArg, opts) => {
|
|
596
617
|
const client = createClient();
|
|
597
618
|
let content;
|
|
@@ -603,12 +624,17 @@ program.command("create").description("Create a new publication").argument("[fil
|
|
|
603
624
|
} else {
|
|
604
625
|
content = await readFromStdin();
|
|
605
626
|
}
|
|
627
|
+
const resolvedVisibility = resolveVisibilityFlags({
|
|
628
|
+
public: opts.public,
|
|
629
|
+
private: opts.private,
|
|
630
|
+
commandName: "create"
|
|
631
|
+
});
|
|
606
632
|
const result = await client.create({
|
|
607
633
|
content,
|
|
608
634
|
filename,
|
|
609
635
|
title: opts.title,
|
|
610
636
|
slug: opts.slug,
|
|
611
|
-
isPublic: false,
|
|
637
|
+
isPublic: resolvedVisibility ?? false,
|
|
612
638
|
expiresIn: opts.expires
|
|
613
639
|
});
|
|
614
640
|
console.log(`Created: ${result.url}`);
|
|
@@ -633,7 +659,7 @@ program.command("get").description("Get details of a publication").argument("<sl
|
|
|
633
659
|
console.log(` Updated: ${new Date(pub.updatedAt).toLocaleDateString()}`);
|
|
634
660
|
console.log(` Size: ${pub.content.length} bytes`);
|
|
635
661
|
});
|
|
636
|
-
program.command("update").description("Update a publication's content and/or metadata").argument("<slug>", "Slug of the publication to update").option("--file <file>", "New content from file").option("--title <title>", "New title").option("--private", "Make the publication private").option("--slug <newSlug>", "Rename the slug").action(
|
|
662
|
+
program.command("update").description("Update a publication's content and/or metadata").argument("<slug>", "Slug of the publication to update").option("--file <file>", "New content from file").option("--title <title>", "New title").option("--public", "Make the publication public").option("--private", "Make the publication private").option("--slug <newSlug>", "Rename the slug").action(
|
|
637
663
|
async (slug, opts) => {
|
|
638
664
|
const client = createClient();
|
|
639
665
|
let content;
|
|
@@ -643,8 +669,11 @@ program.command("update").description("Update a publication's content and/or met
|
|
|
643
669
|
content = file.content;
|
|
644
670
|
filename = file.basename;
|
|
645
671
|
}
|
|
646
|
-
|
|
647
|
-
|
|
672
|
+
const isPublic = resolveVisibilityFlags({
|
|
673
|
+
public: opts.public,
|
|
674
|
+
private: opts.private,
|
|
675
|
+
commandName: "update"
|
|
676
|
+
});
|
|
648
677
|
const result = await client.update({
|
|
649
678
|
slug,
|
|
650
679
|
content,
|
package/package.json
CHANGED
package/dist/chunk-77HFJKLW.js
DELETED
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CHANNELS,
|
|
3
|
-
CONTROL_CHANNEL,
|
|
4
|
-
decodeMessage,
|
|
5
|
-
encodeMessage
|
|
6
|
-
} from "./chunk-56IKFMJ2.js";
|
|
7
|
-
|
|
8
|
-
// src/lib/tunnel-daemon.ts
|
|
9
|
-
import * as fs from "fs";
|
|
10
|
-
import * as net from "net";
|
|
11
|
-
import * as path from "path";
|
|
12
|
-
var OFFER_TIMEOUT_MS = 1e4;
|
|
13
|
-
async function startDaemon(config) {
|
|
14
|
-
const { tunnelId, apiClient, socketPath, infoPath } = config;
|
|
15
|
-
const ndc = await import("node-datachannel");
|
|
16
|
-
const buffer = { messages: [] };
|
|
17
|
-
const startTime = Date.now();
|
|
18
|
-
let connected = false;
|
|
19
|
-
let pollingInterval = null;
|
|
20
|
-
let lastBrowserCandidateCount = 0;
|
|
21
|
-
let remoteDescriptionApplied = false;
|
|
22
|
-
const pendingRemoteCandidates = [];
|
|
23
|
-
const peer = new ndc.PeerConnection("agent", {
|
|
24
|
-
iceServers: ["stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302"]
|
|
25
|
-
});
|
|
26
|
-
const channels = /* @__PURE__ */ new Map();
|
|
27
|
-
const pendingInboundBinaryMeta = /* @__PURE__ */ new Map();
|
|
28
|
-
function openDataChannel(name) {
|
|
29
|
-
const existing = channels.get(name);
|
|
30
|
-
if (existing) return existing;
|
|
31
|
-
const dc = peer.createDataChannel(name, { ordered: true });
|
|
32
|
-
setupChannel(name, dc);
|
|
33
|
-
return dc;
|
|
34
|
-
}
|
|
35
|
-
async function waitForChannelOpen(dc, timeoutMs = 5e3) {
|
|
36
|
-
if (dc.isOpen()) return;
|
|
37
|
-
await new Promise((resolve, reject) => {
|
|
38
|
-
let settled = false;
|
|
39
|
-
const timeout = setTimeout(() => {
|
|
40
|
-
if (settled) return;
|
|
41
|
-
settled = true;
|
|
42
|
-
reject(new Error("DataChannel open timed out"));
|
|
43
|
-
}, timeoutMs);
|
|
44
|
-
dc.onOpen(() => {
|
|
45
|
-
if (settled) return;
|
|
46
|
-
settled = true;
|
|
47
|
-
clearTimeout(timeout);
|
|
48
|
-
resolve();
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
function setupChannel(name, dc) {
|
|
53
|
-
channels.set(name, dc);
|
|
54
|
-
dc.onMessage((data) => {
|
|
55
|
-
if (typeof data === "string") {
|
|
56
|
-
const msg = decodeMessage(data);
|
|
57
|
-
if (msg) {
|
|
58
|
-
if (msg.type === "binary" && !msg.data) {
|
|
59
|
-
pendingInboundBinaryMeta.set(name, msg);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
buffer.messages.push({ channel: name, msg, timestamp: Date.now() });
|
|
63
|
-
}
|
|
64
|
-
} else {
|
|
65
|
-
const pendingMeta = pendingInboundBinaryMeta.get(name);
|
|
66
|
-
if (pendingMeta) pendingInboundBinaryMeta.delete(name);
|
|
67
|
-
const binMsg = pendingMeta ? {
|
|
68
|
-
id: pendingMeta.id,
|
|
69
|
-
type: "binary",
|
|
70
|
-
data: data.toString("base64"),
|
|
71
|
-
meta: { ...pendingMeta.meta, size: data.length }
|
|
72
|
-
} : {
|
|
73
|
-
id: `bin-${Date.now()}`,
|
|
74
|
-
type: "binary",
|
|
75
|
-
data: data.toString("base64"),
|
|
76
|
-
meta: { size: data.length }
|
|
77
|
-
};
|
|
78
|
-
buffer.messages.push({ channel: name, msg: binMsg, timestamp: Date.now() });
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
openDataChannel(CONTROL_CHANNEL);
|
|
83
|
-
openDataChannel(CHANNELS.CHAT);
|
|
84
|
-
openDataChannel(CHANNELS.CANVAS);
|
|
85
|
-
const localCandidates = [];
|
|
86
|
-
peer.onLocalCandidate((candidate, mid) => {
|
|
87
|
-
localCandidates.push(JSON.stringify({ candidate, sdpMid: mid }));
|
|
88
|
-
});
|
|
89
|
-
peer.onStateChange((state) => {
|
|
90
|
-
if (state === "connected") {
|
|
91
|
-
connected = true;
|
|
92
|
-
if (pollingInterval) {
|
|
93
|
-
clearInterval(pollingInterval);
|
|
94
|
-
pollingInterval = null;
|
|
95
|
-
}
|
|
96
|
-
} else if (state === "disconnected" || state === "failed") {
|
|
97
|
-
connected = false;
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
peer.onDataChannel((dc) => {
|
|
101
|
-
setupChannel(dc.getLabel(), dc);
|
|
102
|
-
});
|
|
103
|
-
if (fs.existsSync(socketPath)) {
|
|
104
|
-
let stale = true;
|
|
105
|
-
try {
|
|
106
|
-
const raw = fs.readFileSync(infoPath, "utf-8");
|
|
107
|
-
const info = JSON.parse(raw);
|
|
108
|
-
process.kill(info.pid, 0);
|
|
109
|
-
stale = false;
|
|
110
|
-
} catch {
|
|
111
|
-
stale = true;
|
|
112
|
-
}
|
|
113
|
-
if (stale) {
|
|
114
|
-
try {
|
|
115
|
-
fs.unlinkSync(socketPath);
|
|
116
|
-
} catch {
|
|
117
|
-
}
|
|
118
|
-
} else {
|
|
119
|
-
throw new Error(`Daemon already running (socket: ${socketPath})`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
const ipcServer = net.createServer((conn) => {
|
|
123
|
-
let data = "";
|
|
124
|
-
conn.on("data", (chunk) => {
|
|
125
|
-
data += chunk.toString();
|
|
126
|
-
const newlineIdx = data.indexOf("\n");
|
|
127
|
-
if (newlineIdx === -1) return;
|
|
128
|
-
const line = data.slice(0, newlineIdx);
|
|
129
|
-
data = data.slice(newlineIdx + 1);
|
|
130
|
-
let request;
|
|
131
|
-
try {
|
|
132
|
-
request = JSON.parse(line);
|
|
133
|
-
} catch {
|
|
134
|
-
conn.write(`${JSON.stringify({ ok: false, error: "Invalid JSON" })}
|
|
135
|
-
`);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
handleIpcRequest(request).then((response) => conn.write(`${JSON.stringify(response)}
|
|
139
|
-
`)).catch((err) => conn.write(`${JSON.stringify({ ok: false, error: String(err) })}
|
|
140
|
-
`));
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
ipcServer.listen(socketPath);
|
|
144
|
-
const infoDir = path.dirname(infoPath);
|
|
145
|
-
if (!fs.existsSync(infoDir)) fs.mkdirSync(infoDir, { recursive: true });
|
|
146
|
-
fs.writeFileSync(
|
|
147
|
-
infoPath,
|
|
148
|
-
JSON.stringify({ pid: process.pid, tunnelId, socketPath, startedAt: startTime })
|
|
149
|
-
);
|
|
150
|
-
async function cleanup() {
|
|
151
|
-
if (pollingInterval) clearInterval(pollingInterval);
|
|
152
|
-
for (const dc of channels.values()) dc.close();
|
|
153
|
-
peer.close();
|
|
154
|
-
ipcServer.close();
|
|
155
|
-
try {
|
|
156
|
-
fs.unlinkSync(socketPath);
|
|
157
|
-
} catch {
|
|
158
|
-
}
|
|
159
|
-
try {
|
|
160
|
-
fs.unlinkSync(infoPath);
|
|
161
|
-
} catch {
|
|
162
|
-
}
|
|
163
|
-
await apiClient.close(tunnelId).catch(() => {
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
async function shutdown() {
|
|
167
|
-
await cleanup();
|
|
168
|
-
process.exit(0);
|
|
169
|
-
}
|
|
170
|
-
process.on("SIGTERM", () => void shutdown());
|
|
171
|
-
process.on("SIGINT", () => void shutdown());
|
|
172
|
-
let offer;
|
|
173
|
-
try {
|
|
174
|
-
offer = await generateOffer(peer, OFFER_TIMEOUT_MS);
|
|
175
|
-
} catch (error) {
|
|
176
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
177
|
-
await cleanup();
|
|
178
|
-
throw new Error(`Failed to generate WebRTC offer: ${message}`);
|
|
179
|
-
}
|
|
180
|
-
await apiClient.signal(tunnelId, { offer });
|
|
181
|
-
setTimeout(async () => {
|
|
182
|
-
if (localCandidates.length > 0) {
|
|
183
|
-
await apiClient.signal(tunnelId, { candidates: localCandidates }).catch(() => {
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
}, 1e3);
|
|
187
|
-
let lastSentCandidateCount = 0;
|
|
188
|
-
const candidateInterval = setInterval(async () => {
|
|
189
|
-
if (localCandidates.length > lastSentCandidateCount) {
|
|
190
|
-
const newOnes = localCandidates.slice(lastSentCandidateCount);
|
|
191
|
-
lastSentCandidateCount = localCandidates.length;
|
|
192
|
-
await apiClient.signal(tunnelId, { candidates: newOnes }).catch(() => {
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}, 500);
|
|
196
|
-
setTimeout(() => clearInterval(candidateInterval), 3e4);
|
|
197
|
-
pollingInterval = setInterval(async () => {
|
|
198
|
-
try {
|
|
199
|
-
const tunnel = await apiClient.get(tunnelId);
|
|
200
|
-
if (tunnel.browserAnswer && !remoteDescriptionApplied) {
|
|
201
|
-
try {
|
|
202
|
-
const answer = JSON.parse(tunnel.browserAnswer);
|
|
203
|
-
peer.setRemoteDescription(answer.sdp, answer.type);
|
|
204
|
-
remoteDescriptionApplied = true;
|
|
205
|
-
while (pendingRemoteCandidates.length > 0) {
|
|
206
|
-
const next = pendingRemoteCandidates.shift();
|
|
207
|
-
if (!next) break;
|
|
208
|
-
try {
|
|
209
|
-
peer.addRemoteCandidate(next.candidate, next.sdpMid);
|
|
210
|
-
} catch {
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
} catch {
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
if (tunnel.browserCandidates.length > lastBrowserCandidateCount) {
|
|
217
|
-
const newCandidates = tunnel.browserCandidates.slice(lastBrowserCandidateCount);
|
|
218
|
-
lastBrowserCandidateCount = tunnel.browserCandidates.length;
|
|
219
|
-
for (const c of newCandidates) {
|
|
220
|
-
try {
|
|
221
|
-
const parsed = JSON.parse(c);
|
|
222
|
-
if (typeof parsed.candidate !== "string") continue;
|
|
223
|
-
const sdpMid = typeof parsed.sdpMid === "string" ? parsed.sdpMid : "0";
|
|
224
|
-
if (!remoteDescriptionApplied) {
|
|
225
|
-
pendingRemoteCandidates.push({ candidate: parsed.candidate, sdpMid });
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
peer.addRemoteCandidate(parsed.candidate, sdpMid);
|
|
229
|
-
} catch {
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
} catch {
|
|
234
|
-
}
|
|
235
|
-
}, 500);
|
|
236
|
-
async function handleIpcRequest(req) {
|
|
237
|
-
switch (req.method) {
|
|
238
|
-
case "write": {
|
|
239
|
-
const channel = req.params.channel || CHANNELS.CHAT;
|
|
240
|
-
const msg = req.params.msg;
|
|
241
|
-
const binaryBase64 = typeof req.params.binaryBase64 === "string" ? req.params.binaryBase64 : void 0;
|
|
242
|
-
const dc = channels.get(channel);
|
|
243
|
-
let targetDc = dc;
|
|
244
|
-
if (!targetDc) {
|
|
245
|
-
const newDc = openDataChannel(channel);
|
|
246
|
-
targetDc = newDc;
|
|
247
|
-
}
|
|
248
|
-
try {
|
|
249
|
-
await waitForChannelOpen(targetDc);
|
|
250
|
-
} catch (error) {
|
|
251
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
252
|
-
return { ok: false, error: `Channel "${channel}" not open: ${message}` };
|
|
253
|
-
}
|
|
254
|
-
if (msg.type === "binary" && binaryBase64) {
|
|
255
|
-
const payload = Buffer.from(binaryBase64, "base64");
|
|
256
|
-
targetDc.sendMessage(
|
|
257
|
-
encodeMessage({
|
|
258
|
-
...msg,
|
|
259
|
-
meta: {
|
|
260
|
-
...msg.meta || {},
|
|
261
|
-
size: payload.length
|
|
262
|
-
}
|
|
263
|
-
})
|
|
264
|
-
);
|
|
265
|
-
targetDc.sendMessageBinary(payload);
|
|
266
|
-
} else {
|
|
267
|
-
targetDc.sendMessage(encodeMessage(msg));
|
|
268
|
-
}
|
|
269
|
-
return { ok: true };
|
|
270
|
-
}
|
|
271
|
-
case "read": {
|
|
272
|
-
const channel = req.params.channel;
|
|
273
|
-
let msgs;
|
|
274
|
-
if (channel) {
|
|
275
|
-
msgs = buffer.messages.filter((m) => m.channel === channel);
|
|
276
|
-
buffer.messages = buffer.messages.filter((m) => m.channel !== channel);
|
|
277
|
-
} else {
|
|
278
|
-
msgs = [...buffer.messages];
|
|
279
|
-
buffer.messages = [];
|
|
280
|
-
}
|
|
281
|
-
return { ok: true, messages: msgs };
|
|
282
|
-
}
|
|
283
|
-
case "channels": {
|
|
284
|
-
const chList = [...channels.keys()].map((name) => ({
|
|
285
|
-
name,
|
|
286
|
-
direction: "bidi"
|
|
287
|
-
}));
|
|
288
|
-
return { ok: true, channels: chList };
|
|
289
|
-
}
|
|
290
|
-
case "status": {
|
|
291
|
-
return {
|
|
292
|
-
ok: true,
|
|
293
|
-
connected,
|
|
294
|
-
uptime: Math.floor((Date.now() - startTime) / 1e3),
|
|
295
|
-
channels: [...channels.keys()],
|
|
296
|
-
bufferedMessages: buffer.messages.length
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
case "close": {
|
|
300
|
-
void shutdown();
|
|
301
|
-
return { ok: true };
|
|
302
|
-
}
|
|
303
|
-
default:
|
|
304
|
-
return { ok: false, error: `Unknown method: ${req.method}` };
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
function generateOffer(peer, timeoutMs) {
|
|
309
|
-
return new Promise((resolve, reject) => {
|
|
310
|
-
let resolved = false;
|
|
311
|
-
const done = (sdp, type) => {
|
|
312
|
-
if (resolved) return;
|
|
313
|
-
resolved = true;
|
|
314
|
-
clearTimeout(timeout);
|
|
315
|
-
resolve(JSON.stringify({ sdp, type }));
|
|
316
|
-
};
|
|
317
|
-
peer.onLocalDescription((sdp, type) => {
|
|
318
|
-
done(sdp, type);
|
|
319
|
-
});
|
|
320
|
-
peer.onGatheringStateChange((state) => {
|
|
321
|
-
if (state === "complete" && !resolved) {
|
|
322
|
-
const desc = peer.localDescription();
|
|
323
|
-
if (desc) done(desc.sdp, desc.type);
|
|
324
|
-
}
|
|
325
|
-
});
|
|
326
|
-
const timeout = setTimeout(() => {
|
|
327
|
-
if (resolved) return;
|
|
328
|
-
const desc = peer.localDescription();
|
|
329
|
-
if (desc) {
|
|
330
|
-
done(desc.sdp, desc.type);
|
|
331
|
-
} else {
|
|
332
|
-
resolved = true;
|
|
333
|
-
reject(new Error(`Timed out after ${timeoutMs}ms`));
|
|
334
|
-
}
|
|
335
|
-
}, timeoutMs);
|
|
336
|
-
peer.setLocalDescription();
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
export {
|
|
341
|
-
startDaemon
|
|
342
|
-
};
|