pubblue 0.4.3 → 0.4.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/dist/{chunk-YHFY3TW5.js → chunk-AIEPM67G.js} +237 -34
- package/dist/chunk-MW35LBNH.js +60 -0
- package/dist/index.js +40 -14
- package/dist/tunnel-daemon-DR4A65ME.js +11 -0
- package/dist/tunnel-daemon-entry.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-56IKFMJ2.js +0 -40
- package/dist/tunnel-daemon-QPXIGRW7.js +0 -9
|
@@ -2,21 +2,40 @@ import {
|
|
|
2
2
|
CHANNELS,
|
|
3
3
|
CONTROL_CHANNEL,
|
|
4
4
|
decodeMessage,
|
|
5
|
-
encodeMessage
|
|
6
|
-
|
|
5
|
+
encodeMessage,
|
|
6
|
+
makeAckMessage,
|
|
7
|
+
parseAckMessage,
|
|
8
|
+
shouldAcknowledgeMessage
|
|
9
|
+
} from "./chunk-MW35LBNH.js";
|
|
7
10
|
|
|
8
11
|
// src/lib/tunnel-daemon.ts
|
|
9
12
|
import * as fs from "fs";
|
|
10
13
|
import * as net from "net";
|
|
11
14
|
import * as path from "path";
|
|
15
|
+
|
|
16
|
+
// src/lib/ack-routing.ts
|
|
17
|
+
function resolveAckChannel(input) {
|
|
18
|
+
if (input.messageChannelOpen) return input.messageChannel;
|
|
19
|
+
if (input.controlChannelOpen) return CONTROL_CHANNEL;
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/lib/tunnel-daemon.ts
|
|
12
24
|
var OFFER_TIMEOUT_MS = 1e4;
|
|
13
25
|
var SIGNAL_POLL_WAITING_MS = 500;
|
|
14
26
|
var SIGNAL_POLL_CONNECTED_MS = 2e3;
|
|
15
27
|
var RECOVERY_DELAY_MS = 1e3;
|
|
28
|
+
var WRITE_ACK_TIMEOUT_MS = 5e3;
|
|
16
29
|
var NOT_CONNECTED_WRITE_ERROR = "No browser connected. Ask the user to open the tunnel URL first, then retry.";
|
|
17
30
|
function getTunnelWriteReadinessError(isConnected) {
|
|
18
31
|
return isConnected ? null : NOT_CONNECTED_WRITE_ERROR;
|
|
19
32
|
}
|
|
33
|
+
function shouldRecoverForBrowserAnswerChange(params) {
|
|
34
|
+
const { incomingBrowserAnswer, lastAppliedBrowserAnswer, remoteDescriptionApplied } = params;
|
|
35
|
+
if (!remoteDescriptionApplied) return false;
|
|
36
|
+
if (!incomingBrowserAnswer) return false;
|
|
37
|
+
return incomingBrowserAnswer !== lastAppliedBrowserAnswer;
|
|
38
|
+
}
|
|
20
39
|
async function startDaemon(config) {
|
|
21
40
|
const { tunnelId, apiClient, socketPath, infoPath } = config;
|
|
22
41
|
const ndc = await import("node-datachannel");
|
|
@@ -26,10 +45,14 @@ async function startDaemon(config) {
|
|
|
26
45
|
let connected = false;
|
|
27
46
|
let recovering = false;
|
|
28
47
|
let remoteDescriptionApplied = false;
|
|
48
|
+
let lastAppliedBrowserAnswer = null;
|
|
29
49
|
let lastBrowserCandidateCount = 0;
|
|
30
50
|
let lastSentCandidateCount = 0;
|
|
31
51
|
const pendingRemoteCandidates = [];
|
|
32
52
|
const localCandidates = [];
|
|
53
|
+
const stickyOutboundByChannel = /* @__PURE__ */ new Map();
|
|
54
|
+
const pendingOutboundAcks = /* @__PURE__ */ new Map();
|
|
55
|
+
const pendingDeliveryAcks = /* @__PURE__ */ new Map();
|
|
33
56
|
let peer = null;
|
|
34
57
|
let channels = /* @__PURE__ */ new Map();
|
|
35
58
|
let pendingInboundBinaryMeta = /* @__PURE__ */ new Map();
|
|
@@ -37,6 +60,18 @@ async function startDaemon(config) {
|
|
|
37
60
|
let localCandidateInterval = null;
|
|
38
61
|
let localCandidateStopTimer = null;
|
|
39
62
|
let recoveryTimer = null;
|
|
63
|
+
let lastError = null;
|
|
64
|
+
const debugEnabled = process.env.PUBBLUE_TUNNEL_DEBUG === "1";
|
|
65
|
+
function debugLog(message, error) {
|
|
66
|
+
if (!debugEnabled) return;
|
|
67
|
+
const detail = error === void 0 ? "" : ` | ${error instanceof Error ? `${error.name}: ${error.message}` : typeof error === "string" ? error : JSON.stringify(error)}`;
|
|
68
|
+
console.error(`[pubblue-daemon:${tunnelId}] ${message}${detail}`);
|
|
69
|
+
}
|
|
70
|
+
function markError(message, error) {
|
|
71
|
+
const detail = error === void 0 ? message : `${message}: ${error instanceof Error ? error.message : typeof error === "string" ? error : JSON.stringify(error)}`;
|
|
72
|
+
lastError = detail;
|
|
73
|
+
debugLog(message, error);
|
|
74
|
+
}
|
|
40
75
|
function clearPollingTimer() {
|
|
41
76
|
if (pollingTimer) {
|
|
42
77
|
clearTimeout(pollingTimer);
|
|
@@ -61,14 +96,25 @@ async function startDaemon(config) {
|
|
|
61
96
|
}
|
|
62
97
|
function setupChannel(name, dc) {
|
|
63
98
|
channels.set(name, dc);
|
|
99
|
+
dc.onOpen(() => {
|
|
100
|
+
if (name === CONTROL_CHANNEL) flushQueuedAcks();
|
|
101
|
+
});
|
|
64
102
|
dc.onMessage((data) => {
|
|
65
103
|
if (typeof data === "string") {
|
|
66
104
|
const msg = decodeMessage(data);
|
|
67
105
|
if (!msg) return;
|
|
106
|
+
const ack = parseAckMessage(msg);
|
|
107
|
+
if (ack) {
|
|
108
|
+
settlePendingAck(ack.messageId, true);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
68
111
|
if (msg.type === "binary" && !msg.data) {
|
|
69
112
|
pendingInboundBinaryMeta.set(name, msg);
|
|
70
113
|
return;
|
|
71
114
|
}
|
|
115
|
+
if (shouldAcknowledgeMessage(name, msg)) {
|
|
116
|
+
queueAck(msg.id, name);
|
|
117
|
+
}
|
|
72
118
|
buffer.messages.push({ channel: name, msg, timestamp: Date.now() });
|
|
73
119
|
return;
|
|
74
120
|
}
|
|
@@ -85,9 +131,75 @@ async function startDaemon(config) {
|
|
|
85
131
|
data: data.toString("base64"),
|
|
86
132
|
meta: { size: data.length }
|
|
87
133
|
};
|
|
134
|
+
if (shouldAcknowledgeMessage(name, binMsg)) {
|
|
135
|
+
queueAck(binMsg.id, name);
|
|
136
|
+
}
|
|
88
137
|
buffer.messages.push({ channel: name, msg: binMsg, timestamp: Date.now() });
|
|
89
138
|
});
|
|
90
139
|
}
|
|
140
|
+
function getAckKey(messageId, channel) {
|
|
141
|
+
return `${channel}:${messageId}`;
|
|
142
|
+
}
|
|
143
|
+
function queueAck(messageId, channel) {
|
|
144
|
+
pendingOutboundAcks.set(getAckKey(messageId, channel), { messageId, channel });
|
|
145
|
+
flushQueuedAcks();
|
|
146
|
+
}
|
|
147
|
+
function flushQueuedAcks() {
|
|
148
|
+
const controlDc = channels.get(CONTROL_CHANNEL);
|
|
149
|
+
for (const [ackKey, ack] of pendingOutboundAcks) {
|
|
150
|
+
const messageDc = channels.get(ack.channel);
|
|
151
|
+
const targetChannel = resolveAckChannel({
|
|
152
|
+
controlChannelOpen: Boolean(controlDc?.isOpen()),
|
|
153
|
+
messageChannelOpen: Boolean(messageDc?.isOpen()),
|
|
154
|
+
messageChannel: ack.channel
|
|
155
|
+
});
|
|
156
|
+
if (!targetChannel) continue;
|
|
157
|
+
const encodedAck = encodeMessage(makeAckMessage(ack.messageId, ack.channel));
|
|
158
|
+
const primaryDc = targetChannel === CONTROL_CHANNEL ? controlDc : messageDc;
|
|
159
|
+
try {
|
|
160
|
+
if (primaryDc?.isOpen()) {
|
|
161
|
+
primaryDc.sendMessage(encodedAck);
|
|
162
|
+
pendingOutboundAcks.delete(ackKey);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
markError("failed to flush queued ack on primary channel", error);
|
|
167
|
+
}
|
|
168
|
+
const fallbackChannel = targetChannel === ack.channel ? CONTROL_CHANNEL : ack.channel;
|
|
169
|
+
const fallbackDc = fallbackChannel === CONTROL_CHANNEL ? controlDc : messageDc;
|
|
170
|
+
try {
|
|
171
|
+
if (fallbackDc?.isOpen()) {
|
|
172
|
+
fallbackDc.sendMessage(encodedAck);
|
|
173
|
+
pendingOutboundAcks.delete(ackKey);
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
markError("failed to flush queued ack on fallback channel", error);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function waitForDeliveryAck(messageId, timeoutMs) {
|
|
181
|
+
return new Promise((resolve) => {
|
|
182
|
+
const timeout = setTimeout(() => {
|
|
183
|
+
pendingDeliveryAcks.delete(messageId);
|
|
184
|
+
resolve(false);
|
|
185
|
+
}, timeoutMs);
|
|
186
|
+
pendingDeliveryAcks.set(messageId, { resolve, timeout });
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function settlePendingAck(messageId, received) {
|
|
190
|
+
const pending = pendingDeliveryAcks.get(messageId);
|
|
191
|
+
if (!pending) return;
|
|
192
|
+
clearTimeout(pending.timeout);
|
|
193
|
+
pendingDeliveryAcks.delete(messageId);
|
|
194
|
+
pending.resolve(received);
|
|
195
|
+
}
|
|
196
|
+
function failPendingAcks() {
|
|
197
|
+
for (const [messageId, pending] of pendingDeliveryAcks) {
|
|
198
|
+
clearTimeout(pending.timeout);
|
|
199
|
+
pending.resolve(false);
|
|
200
|
+
pendingDeliveryAcks.delete(messageId);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
91
203
|
function openDataChannel(name) {
|
|
92
204
|
if (!peer) throw new Error("PeerConnection not initialized");
|
|
93
205
|
const existing = channels.get(name);
|
|
@@ -113,9 +225,52 @@ async function startDaemon(config) {
|
|
|
113
225
|
});
|
|
114
226
|
});
|
|
115
227
|
}
|
|
228
|
+
function maybePersistStickyOutbound(channel, msg, binaryPayload) {
|
|
229
|
+
if (channel !== CHANNELS.CANVAS) return;
|
|
230
|
+
if (msg.type === "event" && msg.data === "hide") {
|
|
231
|
+
stickyOutboundByChannel.delete(channel);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (msg.type !== "html") return;
|
|
235
|
+
stickyOutboundByChannel.set(channel, {
|
|
236
|
+
msg: {
|
|
237
|
+
...msg,
|
|
238
|
+
meta: msg.meta ? { ...msg.meta } : void 0
|
|
239
|
+
},
|
|
240
|
+
binaryPayload
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
async function replayStickyOutboundMessages() {
|
|
244
|
+
if (!connected || recovering || stopped) return;
|
|
245
|
+
for (const [channel, sticky] of stickyOutboundByChannel) {
|
|
246
|
+
try {
|
|
247
|
+
let targetDc = channels.get(channel);
|
|
248
|
+
if (!targetDc) targetDc = openDataChannel(channel);
|
|
249
|
+
await waitForChannelOpen(targetDc, 3e3);
|
|
250
|
+
if (sticky.msg.type === "binary" && sticky.binaryPayload) {
|
|
251
|
+
targetDc.sendMessage(
|
|
252
|
+
encodeMessage({
|
|
253
|
+
...sticky.msg,
|
|
254
|
+
meta: {
|
|
255
|
+
...sticky.msg.meta || {},
|
|
256
|
+
size: sticky.binaryPayload.length
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
);
|
|
260
|
+
targetDc.sendMessageBinary(sticky.binaryPayload);
|
|
261
|
+
} else {
|
|
262
|
+
targetDc.sendMessage(encodeMessage(sticky.msg));
|
|
263
|
+
}
|
|
264
|
+
} catch (error) {
|
|
265
|
+
debugLog(`sticky outbound replay failed for channel ${channel}`, error);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
116
269
|
function resetNegotiationState() {
|
|
117
270
|
connected = false;
|
|
271
|
+
failPendingAcks();
|
|
118
272
|
remoteDescriptionApplied = false;
|
|
273
|
+
lastAppliedBrowserAnswer = null;
|
|
119
274
|
lastBrowserCandidateCount = 0;
|
|
120
275
|
lastSentCandidateCount = 0;
|
|
121
276
|
pendingRemoteCandidates.length = 0;
|
|
@@ -128,7 +283,8 @@ async function startDaemon(config) {
|
|
|
128
283
|
if (localCandidates.length <= lastSentCandidateCount) return;
|
|
129
284
|
const newOnes = localCandidates.slice(lastSentCandidateCount);
|
|
130
285
|
lastSentCandidateCount = localCandidates.length;
|
|
131
|
-
await apiClient.signal(tunnelId, { candidates: newOnes }).catch(() => {
|
|
286
|
+
await apiClient.signal(tunnelId, { candidates: newOnes }).catch((error) => {
|
|
287
|
+
debugLog("failed to publish local ICE candidates", error);
|
|
132
288
|
});
|
|
133
289
|
}, 500);
|
|
134
290
|
localCandidateStopTimer = setTimeout(() => {
|
|
@@ -144,9 +300,11 @@ async function startDaemon(config) {
|
|
|
144
300
|
if (stopped || currentPeer !== peer) return;
|
|
145
301
|
if (state === "connected") {
|
|
146
302
|
connected = true;
|
|
303
|
+
flushQueuedAcks();
|
|
304
|
+
void replayStickyOutboundMessages();
|
|
147
305
|
return;
|
|
148
306
|
}
|
|
149
|
-
if (state === "disconnected" || state === "failed") {
|
|
307
|
+
if (state === "disconnected" || state === "failed" || state === "closed") {
|
|
150
308
|
connected = false;
|
|
151
309
|
scheduleRecovery();
|
|
152
310
|
}
|
|
@@ -169,10 +327,12 @@ async function startDaemon(config) {
|
|
|
169
327
|
openDataChannel(CHANNELS.CANVAS);
|
|
170
328
|
}
|
|
171
329
|
function closeCurrentPeer() {
|
|
330
|
+
failPendingAcks();
|
|
172
331
|
for (const dc of channels.values()) {
|
|
173
332
|
try {
|
|
174
333
|
dc.close();
|
|
175
|
-
} catch {
|
|
334
|
+
} catch (error) {
|
|
335
|
+
debugLog("failed to close data channel cleanly", error);
|
|
176
336
|
}
|
|
177
337
|
}
|
|
178
338
|
channels.clear();
|
|
@@ -180,7 +340,8 @@ async function startDaemon(config) {
|
|
|
180
340
|
if (peer) {
|
|
181
341
|
try {
|
|
182
342
|
peer.close();
|
|
183
|
-
} catch {
|
|
343
|
+
} catch (error) {
|
|
344
|
+
debugLog("failed to close peer connection cleanly", error);
|
|
184
345
|
}
|
|
185
346
|
peer = null;
|
|
186
347
|
}
|
|
@@ -194,21 +355,33 @@ async function startDaemon(config) {
|
|
|
194
355
|
}
|
|
195
356
|
async function pollSignalingOnce() {
|
|
196
357
|
const tunnel = await apiClient.get(tunnelId);
|
|
358
|
+
if (shouldRecoverForBrowserAnswerChange({
|
|
359
|
+
incomingBrowserAnswer: tunnel.browserAnswer,
|
|
360
|
+
lastAppliedBrowserAnswer,
|
|
361
|
+
remoteDescriptionApplied
|
|
362
|
+
})) {
|
|
363
|
+
connected = false;
|
|
364
|
+
scheduleRecovery(0, true);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
197
367
|
if (tunnel.browserAnswer && !remoteDescriptionApplied) {
|
|
198
368
|
if (!peer) return;
|
|
199
369
|
try {
|
|
200
370
|
const answer = JSON.parse(tunnel.browserAnswer);
|
|
201
371
|
peer.setRemoteDescription(answer.sdp, answer.type);
|
|
202
372
|
remoteDescriptionApplied = true;
|
|
373
|
+
lastAppliedBrowserAnswer = tunnel.browserAnswer;
|
|
203
374
|
while (pendingRemoteCandidates.length > 0) {
|
|
204
375
|
const next = pendingRemoteCandidates.shift();
|
|
205
376
|
if (!next) break;
|
|
206
377
|
try {
|
|
207
378
|
peer.addRemoteCandidate(next.candidate, next.sdpMid);
|
|
208
|
-
} catch {
|
|
379
|
+
} catch (error) {
|
|
380
|
+
debugLog("failed to apply queued remote ICE candidate", error);
|
|
209
381
|
}
|
|
210
382
|
}
|
|
211
|
-
} catch {
|
|
383
|
+
} catch (error) {
|
|
384
|
+
markError("failed to apply browser answer", error);
|
|
212
385
|
}
|
|
213
386
|
}
|
|
214
387
|
if (tunnel.browserCandidates.length > lastBrowserCandidateCount) {
|
|
@@ -225,7 +398,8 @@ async function startDaemon(config) {
|
|
|
225
398
|
}
|
|
226
399
|
if (!peer) continue;
|
|
227
400
|
peer.addRemoteCandidate(parsed.candidate, sdpMid);
|
|
228
|
-
} catch {
|
|
401
|
+
} catch (error) {
|
|
402
|
+
debugLog("failed to parse/apply browser ICE candidate", error);
|
|
229
403
|
}
|
|
230
404
|
}
|
|
231
405
|
}
|
|
@@ -234,7 +408,8 @@ async function startDaemon(config) {
|
|
|
234
408
|
if (stopped) return;
|
|
235
409
|
try {
|
|
236
410
|
await pollSignalingOnce();
|
|
237
|
-
} catch {
|
|
411
|
+
} catch (error) {
|
|
412
|
+
markError("signaling poll failed", error);
|
|
238
413
|
}
|
|
239
414
|
scheduleNextPoll(remoteDescriptionApplied ? SIGNAL_POLL_CONNECTED_MS : SIGNAL_POLL_WAITING_MS);
|
|
240
415
|
}
|
|
@@ -256,13 +431,14 @@ async function startDaemon(config) {
|
|
|
256
431
|
recovering = false;
|
|
257
432
|
}
|
|
258
433
|
}
|
|
259
|
-
function scheduleRecovery(delayMs = RECOVERY_DELAY_MS) {
|
|
434
|
+
function scheduleRecovery(delayMs = RECOVERY_DELAY_MS, force = false) {
|
|
260
435
|
if (stopped || recovering || recoveryTimer) return;
|
|
261
436
|
recoveryTimer = setTimeout(() => {
|
|
262
437
|
recoveryTimer = null;
|
|
263
|
-
if (stopped || connected) return;
|
|
264
|
-
void recoverPeer().catch(() => {
|
|
265
|
-
|
|
438
|
+
if (stopped || !force && connected) return;
|
|
439
|
+
void recoverPeer().catch((error) => {
|
|
440
|
+
markError("peer recovery failed", error);
|
|
441
|
+
if (!stopped) scheduleRecovery(delayMs, force);
|
|
266
442
|
});
|
|
267
443
|
}, delayMs);
|
|
268
444
|
}
|
|
@@ -279,7 +455,8 @@ async function startDaemon(config) {
|
|
|
279
455
|
if (stale) {
|
|
280
456
|
try {
|
|
281
457
|
fs.unlinkSync(socketPath);
|
|
282
|
-
} catch {
|
|
458
|
+
} catch (error) {
|
|
459
|
+
debugLog("failed to remove stale daemon socket", error);
|
|
283
460
|
}
|
|
284
461
|
} else {
|
|
285
462
|
throw new Error(`Daemon already running (socket: ${socketPath})`);
|
|
@@ -319,6 +496,7 @@ async function startDaemon(config) {
|
|
|
319
496
|
await runNegotiationCycle();
|
|
320
497
|
} catch (error) {
|
|
321
498
|
const message = error instanceof Error ? error.message : String(error);
|
|
499
|
+
markError("initial negotiation failed", error);
|
|
322
500
|
await cleanup();
|
|
323
501
|
throw new Error(`Failed to generate WebRTC offer: ${message}`);
|
|
324
502
|
}
|
|
@@ -332,13 +510,16 @@ async function startDaemon(config) {
|
|
|
332
510
|
ipcServer.close();
|
|
333
511
|
try {
|
|
334
512
|
fs.unlinkSync(socketPath);
|
|
335
|
-
} catch {
|
|
513
|
+
} catch (error) {
|
|
514
|
+
debugLog("failed to remove daemon socket during cleanup", error);
|
|
336
515
|
}
|
|
337
516
|
try {
|
|
338
517
|
fs.unlinkSync(infoPath);
|
|
339
|
-
} catch {
|
|
518
|
+
} catch (error) {
|
|
519
|
+
debugLog("failed to remove daemon info file during cleanup", error);
|
|
340
520
|
}
|
|
341
|
-
await apiClient.close(tunnelId).catch(() => {
|
|
521
|
+
await apiClient.close(tunnelId).catch((error) => {
|
|
522
|
+
markError("failed to close tunnel on API during cleanup", error);
|
|
342
523
|
});
|
|
343
524
|
}
|
|
344
525
|
async function shutdown() {
|
|
@@ -359,30 +540,50 @@ async function startDaemon(config) {
|
|
|
359
540
|
if (readinessError) return { ok: false, error: readinessError };
|
|
360
541
|
const msg = req.params.msg;
|
|
361
542
|
const binaryBase64 = typeof req.params.binaryBase64 === "string" ? req.params.binaryBase64 : void 0;
|
|
543
|
+
const binaryPayload = msg.type === "binary" && binaryBase64 ? Buffer.from(binaryBase64, "base64") : void 0;
|
|
362
544
|
let targetDc = channels.get(channel);
|
|
363
545
|
if (!targetDc) targetDc = openDataChannel(channel);
|
|
364
546
|
try {
|
|
365
547
|
await waitForChannelOpen(targetDc);
|
|
366
548
|
} catch (error) {
|
|
367
549
|
const message = error instanceof Error ? error.message : String(error);
|
|
550
|
+
markError(`channel "${channel}" failed to open`, error);
|
|
368
551
|
return { ok: false, error: `Channel "${channel}" not open: ${message}` };
|
|
369
552
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
553
|
+
const waitForAck = shouldAcknowledgeMessage(channel, msg) ? waitForDeliveryAck(msg.id, WRITE_ACK_TIMEOUT_MS) : null;
|
|
554
|
+
try {
|
|
555
|
+
if (msg.type === "binary" && binaryPayload) {
|
|
556
|
+
targetDc.sendMessage(
|
|
557
|
+
encodeMessage({
|
|
558
|
+
...msg,
|
|
559
|
+
meta: {
|
|
560
|
+
...msg.meta || {},
|
|
561
|
+
size: binaryPayload.length
|
|
562
|
+
}
|
|
563
|
+
})
|
|
564
|
+
);
|
|
565
|
+
targetDc.sendMessageBinary(binaryPayload);
|
|
566
|
+
} else {
|
|
567
|
+
targetDc.sendMessage(encodeMessage(msg));
|
|
568
|
+
}
|
|
569
|
+
} catch (error) {
|
|
570
|
+
if (waitForAck) settlePendingAck(msg.id, false);
|
|
571
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
572
|
+
markError(`failed to send message on channel "${channel}"`, error);
|
|
573
|
+
return { ok: false, error: `Failed to send on channel "${channel}": ${message}` };
|
|
384
574
|
}
|
|
385
|
-
|
|
575
|
+
if (waitForAck) {
|
|
576
|
+
const acked = await waitForAck;
|
|
577
|
+
if (!acked) {
|
|
578
|
+
markError(`delivery ack timeout for message ${msg.id}`);
|
|
579
|
+
return {
|
|
580
|
+
ok: false,
|
|
581
|
+
error: `Delivery not confirmed for message ${msg.id} within ${WRITE_ACK_TIMEOUT_MS}ms.`
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
maybePersistStickyOutbound(channel, msg, binaryPayload);
|
|
586
|
+
return { ok: true, delivered: true };
|
|
386
587
|
}
|
|
387
588
|
case "read": {
|
|
388
589
|
const channel = req.params.channel;
|
|
@@ -406,7 +607,8 @@ async function startDaemon(config) {
|
|
|
406
607
|
connected,
|
|
407
608
|
uptime: Math.floor((Date.now() - startTime) / 1e3),
|
|
408
609
|
channels: [...channels.keys()],
|
|
409
|
-
bufferedMessages: buffer.messages.length
|
|
610
|
+
bufferedMessages: buffer.messages.length,
|
|
611
|
+
lastError
|
|
410
612
|
};
|
|
411
613
|
}
|
|
412
614
|
case "close": {
|
|
@@ -452,5 +654,6 @@ function generateOffer(peer, timeoutMs) {
|
|
|
452
654
|
|
|
453
655
|
export {
|
|
454
656
|
getTunnelWriteReadinessError,
|
|
657
|
+
shouldRecoverForBrowserAnswerChange,
|
|
455
658
|
startDaemon
|
|
456
659
|
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/lib/bridge-protocol.ts
|
|
2
|
+
var CONTROL_CHANNEL = "_control";
|
|
3
|
+
var CHANNELS = {
|
|
4
|
+
CHAT: "chat",
|
|
5
|
+
CANVAS: "canvas",
|
|
6
|
+
AUDIO: "audio",
|
|
7
|
+
MEDIA: "media",
|
|
8
|
+
FILE: "file"
|
|
9
|
+
};
|
|
10
|
+
var idCounter = 0;
|
|
11
|
+
function generateMessageId() {
|
|
12
|
+
const ts = Date.now().toString(36);
|
|
13
|
+
const seq = (idCounter++).toString(36);
|
|
14
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
15
|
+
return `${ts}-${seq}-${rand}`;
|
|
16
|
+
}
|
|
17
|
+
function encodeMessage(msg) {
|
|
18
|
+
return JSON.stringify(msg);
|
|
19
|
+
}
|
|
20
|
+
function decodeMessage(raw) {
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(raw);
|
|
23
|
+
if (parsed && typeof parsed.id === "string" && typeof parsed.type === "string") {
|
|
24
|
+
return parsed;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function makeEventMessage(event, meta) {
|
|
32
|
+
return { id: generateMessageId(), type: "event", data: event, meta };
|
|
33
|
+
}
|
|
34
|
+
function makeAckMessage(messageId, channel) {
|
|
35
|
+
return makeEventMessage("ack", { messageId, channel, receivedAt: Date.now() });
|
|
36
|
+
}
|
|
37
|
+
function parseAckMessage(msg) {
|
|
38
|
+
if (msg.type !== "event" || msg.data !== "ack" || !msg.meta) return null;
|
|
39
|
+
const messageId = typeof msg.meta.messageId === "string" ? msg.meta.messageId : null;
|
|
40
|
+
const channel = typeof msg.meta.channel === "string" ? msg.meta.channel : null;
|
|
41
|
+
if (!messageId || !channel) return null;
|
|
42
|
+
const receivedAt = typeof msg.meta.receivedAt === "number" ? msg.meta.receivedAt : void 0;
|
|
43
|
+
return { messageId, channel, receivedAt };
|
|
44
|
+
}
|
|
45
|
+
function shouldAcknowledgeMessage(channel, msg) {
|
|
46
|
+
return channel !== CONTROL_CHANNEL && parseAckMessage(msg) === null;
|
|
47
|
+
}
|
|
48
|
+
var MAX_TUNNEL_EXPIRY_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
49
|
+
var DEFAULT_TUNNEL_EXPIRY_MS = 24 * 60 * 60 * 1e3;
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
CONTROL_CHANNEL,
|
|
53
|
+
CHANNELS,
|
|
54
|
+
generateMessageId,
|
|
55
|
+
encodeMessage,
|
|
56
|
+
decodeMessage,
|
|
57
|
+
makeAckMessage,
|
|
58
|
+
parseAckMessage,
|
|
59
|
+
shouldAcknowledgeMessage
|
|
60
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -3,8 +3,9 @@ import {
|
|
|
3
3
|
TunnelApiClient
|
|
4
4
|
} from "./chunk-BV423NLA.js";
|
|
5
5
|
import {
|
|
6
|
+
CHANNELS,
|
|
6
7
|
generateMessageId
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-MW35LBNH.js";
|
|
8
9
|
|
|
9
10
|
// src/index.ts
|
|
10
11
|
import * as fs3 from "fs";
|
|
@@ -185,6 +186,9 @@ function tunnelInfoDir() {
|
|
|
185
186
|
function tunnelInfoPath(tunnelId) {
|
|
186
187
|
return path2.join(tunnelInfoDir(), `${tunnelId}.json`);
|
|
187
188
|
}
|
|
189
|
+
function tunnelLogPath(tunnelId) {
|
|
190
|
+
return path2.join(tunnelInfoDir(), `${tunnelId}.log`);
|
|
191
|
+
}
|
|
188
192
|
function createApiClient() {
|
|
189
193
|
const config = getConfig();
|
|
190
194
|
return new TunnelApiClient(config.baseUrl, config.apiKey);
|
|
@@ -219,6 +223,9 @@ function getFollowReadDelayMs(disconnected, consecutiveFailures) {
|
|
|
219
223
|
if (!disconnected) return 1e3;
|
|
220
224
|
return Math.min(5e3, 1e3 * 2 ** Math.min(consecutiveFailures, 3));
|
|
221
225
|
}
|
|
226
|
+
function resolveTunnelIdSelection(tunnelIdArg, tunnelOpt) {
|
|
227
|
+
return tunnelOpt || tunnelIdArg;
|
|
228
|
+
}
|
|
222
229
|
function registerTunnelCommands(program2) {
|
|
223
230
|
const tunnel = program2.command("tunnel").description("P2P encrypted tunnel to browser");
|
|
224
231
|
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) => {
|
|
@@ -230,7 +237,7 @@ function registerTunnelCommands(program2) {
|
|
|
230
237
|
const socketPath = getSocketPath(result.tunnelId);
|
|
231
238
|
const infoPath = tunnelInfoPath(result.tunnelId);
|
|
232
239
|
if (opts.foreground) {
|
|
233
|
-
const { startDaemon } = await import("./tunnel-daemon-
|
|
240
|
+
const { startDaemon } = await import("./tunnel-daemon-DR4A65ME.js");
|
|
234
241
|
console.log(`Tunnel started: ${result.url}`);
|
|
235
242
|
console.log(`Tunnel ID: ${result.tunnelId}`);
|
|
236
243
|
console.log(`Expires: ${new Date(result.expiresAt).toISOString()}`);
|
|
@@ -250,9 +257,11 @@ function registerTunnelCommands(program2) {
|
|
|
250
257
|
} else {
|
|
251
258
|
const daemonScript = path2.join(import.meta.dirname, "tunnel-daemon-entry.js");
|
|
252
259
|
const config = getConfig();
|
|
260
|
+
const logPath = tunnelLogPath(result.tunnelId);
|
|
261
|
+
const daemonLogFd = fs2.openSync(logPath, "a");
|
|
253
262
|
const child = fork(daemonScript, [], {
|
|
254
263
|
detached: true,
|
|
255
|
-
stdio: "ignore",
|
|
264
|
+
stdio: ["ignore", daemonLogFd, daemonLogFd],
|
|
256
265
|
env: {
|
|
257
266
|
...process.env,
|
|
258
267
|
PUBBLUE_DAEMON_TUNNEL_ID: result.tunnelId,
|
|
@@ -262,10 +271,12 @@ function registerTunnelCommands(program2) {
|
|
|
262
271
|
PUBBLUE_DAEMON_INFO: infoPath
|
|
263
272
|
}
|
|
264
273
|
});
|
|
274
|
+
fs2.closeSync(daemonLogFd);
|
|
265
275
|
child.unref();
|
|
266
276
|
const ready = await waitForDaemonReady(infoPath, child, 5e3);
|
|
267
277
|
if (!ready) {
|
|
268
278
|
console.error("Daemon failed to start. Cleaning up tunnel...");
|
|
279
|
+
console.error(`Daemon log: ${logPath}`);
|
|
269
280
|
await apiClient.close(result.tunnelId).catch(() => {
|
|
270
281
|
});
|
|
271
282
|
process.exit(1);
|
|
@@ -273,6 +284,7 @@ function registerTunnelCommands(program2) {
|
|
|
273
284
|
console.log(`Tunnel started: ${result.url}`);
|
|
274
285
|
console.log(`Tunnel ID: ${result.tunnelId}`);
|
|
275
286
|
console.log(`Expires: ${new Date(result.expiresAt).toISOString()}`);
|
|
287
|
+
console.log(`Daemon log: ${logPath}`);
|
|
276
288
|
}
|
|
277
289
|
});
|
|
278
290
|
tunnel.command("write").description("Write data to a channel").argument("[message]", "Text message (or use --file)").option("-t, --tunnel <tunnelId>", "Tunnel ID (auto-detected if one active)").option("-c, --channel <channel>", "Channel name", "chat").option("-f, --file <file>", "Read content from file").action(
|
|
@@ -333,18 +345,24 @@ function registerTunnelCommands(program2) {
|
|
|
333
345
|
}
|
|
334
346
|
}
|
|
335
347
|
);
|
|
336
|
-
tunnel.command("read").description("Read buffered messages from channels").argument("[tunnelId]", "Tunnel ID (auto-detected if one active)").option("-c, --channel <channel>", "Filter by channel").option("--follow", "Stream messages continuously").action(
|
|
348
|
+
tunnel.command("read").description("Read buffered messages from channels").argument("[tunnelId]", "Tunnel ID (auto-detected if one active)").option("-t, --tunnel <tunnelId>", "Tunnel ID (alternative to positional arg)").option("-c, --channel <channel>", "Filter by channel").option("--follow", "Stream messages continuously").option("--all", "With --follow, include all channels instead of chat-only default").action(
|
|
337
349
|
async (tunnelIdArg, opts) => {
|
|
338
|
-
const tunnelId = tunnelIdArg || await resolveActiveTunnel();
|
|
350
|
+
const tunnelId = resolveTunnelIdSelection(tunnelIdArg, opts.tunnel) || await resolveActiveTunnel();
|
|
339
351
|
const socketPath = getSocketPath(tunnelId);
|
|
352
|
+
const readChannel = opts.channel || (opts.follow && !opts.all ? CHANNELS.CHAT : void 0);
|
|
340
353
|
if (opts.follow) {
|
|
354
|
+
if (!opts.channel && !opts.all) {
|
|
355
|
+
console.error(
|
|
356
|
+
"Following chat channel by default. Use `--all` to include binary/file channels."
|
|
357
|
+
);
|
|
358
|
+
}
|
|
341
359
|
let consecutiveFailures = 0;
|
|
342
360
|
let warnedDisconnected = false;
|
|
343
361
|
while (true) {
|
|
344
362
|
try {
|
|
345
363
|
const response = await ipcCall(socketPath, {
|
|
346
364
|
method: "read",
|
|
347
|
-
params: { channel:
|
|
365
|
+
params: { channel: readChannel }
|
|
348
366
|
});
|
|
349
367
|
if (warnedDisconnected) {
|
|
350
368
|
console.error("Daemon reconnected.");
|
|
@@ -356,10 +374,11 @@ function registerTunnelCommands(program2) {
|
|
|
356
374
|
console.log(JSON.stringify(m));
|
|
357
375
|
}
|
|
358
376
|
}
|
|
359
|
-
} catch {
|
|
377
|
+
} catch (error) {
|
|
360
378
|
consecutiveFailures += 1;
|
|
361
379
|
if (!warnedDisconnected) {
|
|
362
|
-
|
|
380
|
+
const detail = error instanceof Error ? ` ${error.message}` : "";
|
|
381
|
+
console.error(`Daemon disconnected. Waiting for recovery...${detail}`);
|
|
363
382
|
warnedDisconnected = true;
|
|
364
383
|
}
|
|
365
384
|
}
|
|
@@ -369,7 +388,7 @@ function registerTunnelCommands(program2) {
|
|
|
369
388
|
} else {
|
|
370
389
|
const response = await ipcCall(socketPath, {
|
|
371
390
|
method: "read",
|
|
372
|
-
params: { channel:
|
|
391
|
+
params: { channel: readChannel }
|
|
373
392
|
});
|
|
374
393
|
if (!response.ok) {
|
|
375
394
|
console.error(`Failed: ${response.error}`);
|
|
@@ -379,8 +398,8 @@ function registerTunnelCommands(program2) {
|
|
|
379
398
|
}
|
|
380
399
|
}
|
|
381
400
|
);
|
|
382
|
-
tunnel.command("channels").description("List active channels").argument("[tunnelId]", "Tunnel ID").action(async (tunnelIdArg) => {
|
|
383
|
-
const tunnelId = tunnelIdArg || await resolveActiveTunnel();
|
|
401
|
+
tunnel.command("channels").description("List active channels").argument("[tunnelId]", "Tunnel ID").option("-t, --tunnel <tunnelId>", "Tunnel ID (alternative to positional arg)").action(async (tunnelIdArg, opts) => {
|
|
402
|
+
const tunnelId = resolveTunnelIdSelection(tunnelIdArg, opts.tunnel) || await resolveActiveTunnel();
|
|
384
403
|
const socketPath = getSocketPath(tunnelId);
|
|
385
404
|
const response = await ipcCall(socketPath, { method: "channels", params: {} });
|
|
386
405
|
if (response.channels) {
|
|
@@ -389,8 +408,8 @@ function registerTunnelCommands(program2) {
|
|
|
389
408
|
}
|
|
390
409
|
}
|
|
391
410
|
});
|
|
392
|
-
tunnel.command("status").description("Check tunnel connection status").argument("[tunnelId]", "Tunnel ID").action(async (tunnelIdArg) => {
|
|
393
|
-
const tunnelId = tunnelIdArg || await resolveActiveTunnel();
|
|
411
|
+
tunnel.command("status").description("Check tunnel connection status").argument("[tunnelId]", "Tunnel ID").option("-t, --tunnel <tunnelId>", "Tunnel ID (alternative to positional arg)").action(async (tunnelIdArg, opts) => {
|
|
412
|
+
const tunnelId = resolveTunnelIdSelection(tunnelIdArg, opts.tunnel) || await resolveActiveTunnel();
|
|
394
413
|
const socketPath = getSocketPath(tunnelId);
|
|
395
414
|
const response = await ipcCall(socketPath, { method: "status", params: {} });
|
|
396
415
|
console.log(` Status: ${response.connected ? "connected" : "waiting"}`);
|
|
@@ -398,6 +417,13 @@ function registerTunnelCommands(program2) {
|
|
|
398
417
|
const chNames = Array.isArray(response.channels) ? response.channels.map((c) => typeof c === "string" ? c : String(c)) : [];
|
|
399
418
|
console.log(` Channels: ${chNames.join(", ")}`);
|
|
400
419
|
console.log(` Buffered: ${response.bufferedMessages ?? 0} messages`);
|
|
420
|
+
if (typeof response.lastError === "string" && response.lastError.length > 0) {
|
|
421
|
+
console.log(` Last error: ${response.lastError}`);
|
|
422
|
+
}
|
|
423
|
+
const logPath = tunnelLogPath(tunnelId);
|
|
424
|
+
if (fs2.existsSync(logPath)) {
|
|
425
|
+
console.log(` Log: ${logPath}`);
|
|
426
|
+
}
|
|
401
427
|
});
|
|
402
428
|
tunnel.command("list").description("List active tunnels").action(async () => {
|
|
403
429
|
const apiClient = createApiClient();
|
|
@@ -600,7 +626,7 @@ function readFile(filePath) {
|
|
|
600
626
|
basename: path3.basename(resolved)
|
|
601
627
|
};
|
|
602
628
|
}
|
|
603
|
-
program.name("pubblue").description("Publish static content and get shareable URLs").version("0.4.
|
|
629
|
+
program.name("pubblue").description("Publish static content and get shareable URLs").version("0.4.4");
|
|
604
630
|
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) => {
|
|
605
631
|
try {
|
|
606
632
|
const apiKey = await resolveConfigureApiKey(opts);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getTunnelWriteReadinessError,
|
|
3
|
+
shouldRecoverForBrowserAnswerChange,
|
|
4
|
+
startDaemon
|
|
5
|
+
} from "./chunk-AIEPM67G.js";
|
|
6
|
+
import "./chunk-MW35LBNH.js";
|
|
7
|
+
export {
|
|
8
|
+
getTunnelWriteReadinessError,
|
|
9
|
+
shouldRecoverForBrowserAnswerChange,
|
|
10
|
+
startDaemon
|
|
11
|
+
};
|
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
} from "./chunk-BV423NLA.js";
|
|
4
4
|
import {
|
|
5
5
|
startDaemon
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-AIEPM67G.js";
|
|
7
|
+
import "./chunk-MW35LBNH.js";
|
|
8
8
|
|
|
9
9
|
// src/tunnel-daemon-entry.ts
|
|
10
10
|
var tunnelId = process.env.PUBBLUE_DAEMON_TUNNEL_ID;
|
package/package.json
CHANGED
package/dist/chunk-56IKFMJ2.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
// src/lib/bridge-protocol.ts
|
|
2
|
-
var CONTROL_CHANNEL = "_control";
|
|
3
|
-
var CHANNELS = {
|
|
4
|
-
CHAT: "chat",
|
|
5
|
-
CANVAS: "canvas",
|
|
6
|
-
AUDIO: "audio",
|
|
7
|
-
MEDIA: "media",
|
|
8
|
-
FILE: "file"
|
|
9
|
-
};
|
|
10
|
-
var idCounter = 0;
|
|
11
|
-
function generateMessageId() {
|
|
12
|
-
const ts = Date.now().toString(36);
|
|
13
|
-
const seq = (idCounter++).toString(36);
|
|
14
|
-
const rand = Math.random().toString(36).slice(2, 6);
|
|
15
|
-
return `${ts}-${seq}-${rand}`;
|
|
16
|
-
}
|
|
17
|
-
function encodeMessage(msg) {
|
|
18
|
-
return JSON.stringify(msg);
|
|
19
|
-
}
|
|
20
|
-
function decodeMessage(raw) {
|
|
21
|
-
try {
|
|
22
|
-
const parsed = JSON.parse(raw);
|
|
23
|
-
if (parsed && typeof parsed.id === "string" && typeof parsed.type === "string") {
|
|
24
|
-
return parsed;
|
|
25
|
-
}
|
|
26
|
-
return null;
|
|
27
|
-
} catch {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
var MAX_TUNNEL_EXPIRY_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
32
|
-
var DEFAULT_TUNNEL_EXPIRY_MS = 24 * 60 * 60 * 1e3;
|
|
33
|
-
|
|
34
|
-
export {
|
|
35
|
-
CONTROL_CHANNEL,
|
|
36
|
-
CHANNELS,
|
|
37
|
-
generateMessageId,
|
|
38
|
-
encodeMessage,
|
|
39
|
-
decodeMessage
|
|
40
|
-
};
|