pubblue 0.4.12 → 0.5.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/dist/{chunk-UW7JILRJ.js → chunk-5GSMS3YU.js} +121 -22
- package/dist/{chunk-4YTJ2WKF.js → chunk-PFZT7M3E.js} +55 -1
- package/dist/chunk-YI45G6AG.js +759 -0
- package/dist/index.js +706 -1224
- package/dist/tunnel-bridge-entry.js +27 -27
- package/dist/tunnel-daemon-QN6TVUX6.js +8 -0
- package/dist/tunnel-daemon-entry.js +23 -9
- package/package.json +2 -2
- package/dist/chunk-7NFHPJ76.js +0 -79
- package/dist/chunk-HJ5LTUHS.js +0 -56
- package/dist/tunnel-daemon-RKWEA5BV.js +0 -14
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
PubApiError,
|
|
3
|
+
buildBridgeForkStdio,
|
|
4
|
+
isBridgeRunning,
|
|
5
|
+
latestCliVersionPath,
|
|
6
|
+
readLatestCliVersion,
|
|
7
|
+
stopBridge
|
|
8
|
+
} from "./chunk-YI45G6AG.js";
|
|
4
9
|
import {
|
|
5
10
|
CHANNELS,
|
|
6
11
|
CONTROL_CHANNEL,
|
|
@@ -9,9 +14,10 @@ import {
|
|
|
9
14
|
makeAckMessage,
|
|
10
15
|
parseAckMessage,
|
|
11
16
|
shouldAcknowledgeMessage
|
|
12
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-PFZT7M3E.js";
|
|
13
18
|
|
|
14
19
|
// src/lib/tunnel-daemon.ts
|
|
20
|
+
import { fork } from "child_process";
|
|
15
21
|
import * as fs from "fs";
|
|
16
22
|
import * as net from "net";
|
|
17
23
|
import * as path from "path";
|
|
@@ -57,13 +63,16 @@ function generateOffer(peer, timeoutMs) {
|
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
// src/lib/tunnel-daemon-shared.ts
|
|
66
|
+
var BRIDGE_CHECK_INTERVAL_MS = 3e4;
|
|
67
|
+
var BRIDGE_MAX_RAPID_RESTARTS = 3;
|
|
68
|
+
var BRIDGE_RAPID_RESTART_WINDOW_MS = 5 * 60 * 1e3;
|
|
60
69
|
var OFFER_TIMEOUT_MS = 1e4;
|
|
61
70
|
var SIGNAL_POLL_WAITING_MS = 5e3;
|
|
62
71
|
var SIGNAL_POLL_CONNECTED_MS = 15e3;
|
|
63
72
|
var LOCAL_CANDIDATE_FLUSH_MS = 2e3;
|
|
64
73
|
var RECOVERY_DELAY_MS = 1e3;
|
|
65
74
|
var WRITE_ACK_TIMEOUT_MS = 5e3;
|
|
66
|
-
var NOT_CONNECTED_WRITE_ERROR = "No browser connected. Ask the user to open the
|
|
75
|
+
var NOT_CONNECTED_WRITE_ERROR = "No browser connected. Ask the user to open the pub URL first, then retry.";
|
|
67
76
|
function getTunnelWriteReadinessError(isConnected) {
|
|
68
77
|
return isConnected ? null : NOT_CONNECTED_WRITE_ERROR;
|
|
69
78
|
}
|
|
@@ -83,8 +92,11 @@ function getSignalPollDelayMs(params) {
|
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
// src/lib/tunnel-daemon.ts
|
|
95
|
+
var IDLE_SLOWDOWN_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
96
|
+
var IDLE_SIGNAL_POLL_MS = 5 * 60 * 1e3;
|
|
97
|
+
var HEALTH_CHECK_INTERVAL_MS = 60 * 60 * 1e3;
|
|
86
98
|
async function startDaemon(config) {
|
|
87
|
-
const {
|
|
99
|
+
const { slug, apiClient, socketPath, infoPath, cliVersion } = config;
|
|
88
100
|
const ndc = await import("node-datachannel");
|
|
89
101
|
const buffer = { messages: [] };
|
|
90
102
|
const startTime = Date.now();
|
|
@@ -107,12 +119,15 @@ async function startDaemon(config) {
|
|
|
107
119
|
let localCandidateInterval = null;
|
|
108
120
|
let localCandidateStopTimer = null;
|
|
109
121
|
let recoveryTimer = null;
|
|
122
|
+
let healthCheckTimer = null;
|
|
110
123
|
let lastError = null;
|
|
111
124
|
const debugEnabled = process.env.PUBBLUE_TUNNEL_DEBUG === "1";
|
|
125
|
+
let lastConnectedAt = startTime;
|
|
126
|
+
const versionFilePath = latestCliVersionPath();
|
|
112
127
|
function debugLog(message, error) {
|
|
113
128
|
if (!debugEnabled) return;
|
|
114
129
|
const detail = error === void 0 ? "" : ` | ${error instanceof Error ? `${error.name}: ${error.message}` : typeof error === "string" ? error : JSON.stringify(error)}`;
|
|
115
|
-
console.error(`[pubblue-daemon:${
|
|
130
|
+
console.error(`[pubblue-daemon:${slug}] ${message}${detail}`);
|
|
116
131
|
}
|
|
117
132
|
function markError(message, error) {
|
|
118
133
|
const detail = error === void 0 ? message : `${message}: ${error instanceof Error ? error.message : typeof error === "string" ? error : JSON.stringify(error)}`;
|
|
@@ -141,6 +156,27 @@ async function startDaemon(config) {
|
|
|
141
156
|
recoveryTimer = null;
|
|
142
157
|
}
|
|
143
158
|
}
|
|
159
|
+
function clearHealthCheckTimer() {
|
|
160
|
+
if (healthCheckTimer) {
|
|
161
|
+
clearInterval(healthCheckTimer);
|
|
162
|
+
healthCheckTimer = null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function runHealthCheck() {
|
|
166
|
+
if (stopped) return;
|
|
167
|
+
if (cliVersion) {
|
|
168
|
+
const latest = readLatestCliVersion(versionFilePath);
|
|
169
|
+
if (latest && latest !== cliVersion) {
|
|
170
|
+
markError(`detected CLI upgrade (${cliVersion} \u2192 ${latest}); shutting down`);
|
|
171
|
+
void shutdown();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function startHealthCheckTimer() {
|
|
176
|
+
clearHealthCheckTimer();
|
|
177
|
+
healthCheckTimer = setInterval(runHealthCheck, HEALTH_CHECK_INTERVAL_MS);
|
|
178
|
+
runHealthCheck();
|
|
179
|
+
}
|
|
144
180
|
function setupChannel(name, dc) {
|
|
145
181
|
channels.set(name, dc);
|
|
146
182
|
dc.onOpen(() => {
|
|
@@ -330,7 +366,7 @@ async function startDaemon(config) {
|
|
|
330
366
|
if (localCandidates.length <= lastSentCandidateCount) return;
|
|
331
367
|
const newOnes = localCandidates.slice(lastSentCandidateCount);
|
|
332
368
|
lastSentCandidateCount = localCandidates.length;
|
|
333
|
-
await apiClient.signal(
|
|
369
|
+
await apiClient.signal(slug, { candidates: newOnes }).catch((error) => {
|
|
334
370
|
debugLog("failed to publish local ICE candidates", error);
|
|
335
371
|
});
|
|
336
372
|
}, LOCAL_CANDIDATE_FLUSH_MS);
|
|
@@ -347,6 +383,7 @@ async function startDaemon(config) {
|
|
|
347
383
|
if (stopped || currentPeer !== peer) return;
|
|
348
384
|
if (state === "connected") {
|
|
349
385
|
connected = true;
|
|
386
|
+
lastConnectedAt = Date.now();
|
|
350
387
|
flushQueuedAcks();
|
|
351
388
|
void replayStickyOutboundMessages();
|
|
352
389
|
return;
|
|
@@ -401,9 +438,9 @@ async function startDaemon(config) {
|
|
|
401
438
|
}, delayMs);
|
|
402
439
|
}
|
|
403
440
|
async function pollSignalingOnce() {
|
|
404
|
-
const
|
|
441
|
+
const session = await apiClient.getLive(slug);
|
|
405
442
|
if (shouldRecoverForBrowserAnswerChange({
|
|
406
|
-
incomingBrowserAnswer:
|
|
443
|
+
incomingBrowserAnswer: session.browserAnswer,
|
|
407
444
|
lastAppliedBrowserAnswer,
|
|
408
445
|
remoteDescriptionApplied
|
|
409
446
|
})) {
|
|
@@ -411,13 +448,13 @@ async function startDaemon(config) {
|
|
|
411
448
|
scheduleRecovery(0, true);
|
|
412
449
|
return;
|
|
413
450
|
}
|
|
414
|
-
if (
|
|
451
|
+
if (session.browserAnswer && !remoteDescriptionApplied) {
|
|
415
452
|
if (!peer) return;
|
|
416
453
|
try {
|
|
417
|
-
const answer = JSON.parse(
|
|
454
|
+
const answer = JSON.parse(session.browserAnswer);
|
|
418
455
|
peer.setRemoteDescription(answer.sdp, answer.type);
|
|
419
456
|
remoteDescriptionApplied = true;
|
|
420
|
-
lastAppliedBrowserAnswer =
|
|
457
|
+
lastAppliedBrowserAnswer = session.browserAnswer;
|
|
421
458
|
while (pendingRemoteCandidates.length > 0) {
|
|
422
459
|
const next = pendingRemoteCandidates.shift();
|
|
423
460
|
if (!next) break;
|
|
@@ -431,9 +468,9 @@ async function startDaemon(config) {
|
|
|
431
468
|
markError("failed to apply browser answer", error);
|
|
432
469
|
}
|
|
433
470
|
}
|
|
434
|
-
if (
|
|
435
|
-
const newCandidates =
|
|
436
|
-
lastBrowserCandidateCount =
|
|
471
|
+
if (session.browserCandidates.length > lastBrowserCandidateCount) {
|
|
472
|
+
const newCandidates = session.browserCandidates.slice(lastBrowserCandidateCount);
|
|
473
|
+
lastBrowserCandidateCount = session.browserCandidates.length;
|
|
437
474
|
for (const c of newCandidates) {
|
|
438
475
|
try {
|
|
439
476
|
const parsed = JSON.parse(c);
|
|
@@ -457,18 +494,20 @@ async function startDaemon(config) {
|
|
|
457
494
|
try {
|
|
458
495
|
await pollSignalingOnce();
|
|
459
496
|
} catch (error) {
|
|
460
|
-
if (error instanceof
|
|
497
|
+
if (error instanceof PubApiError && error.status === 429) {
|
|
461
498
|
retryAfterSeconds = error.retryAfterSeconds;
|
|
462
499
|
}
|
|
463
500
|
markError("signaling poll failed", error);
|
|
464
501
|
}
|
|
465
|
-
|
|
502
|
+
const baseDelay = getSignalPollDelayMs({ remoteDescriptionApplied, retryAfterSeconds });
|
|
503
|
+
const idleSlowdown = !connected && Date.now() - lastConnectedAt >= IDLE_SLOWDOWN_AFTER_MS ? IDLE_SIGNAL_POLL_MS : 0;
|
|
504
|
+
scheduleNextPoll(Math.max(baseDelay, idleSlowdown));
|
|
466
505
|
}
|
|
467
506
|
async function runNegotiationCycle() {
|
|
468
507
|
if (!peer) throw new Error("PeerConnection not initialized");
|
|
469
508
|
resetNegotiationState();
|
|
470
509
|
const offer = await generateOffer(peer, OFFER_TIMEOUT_MS);
|
|
471
|
-
await apiClient.signal(
|
|
510
|
+
await apiClient.signal(slug, { offer });
|
|
472
511
|
startLocalCandidateFlush();
|
|
473
512
|
}
|
|
474
513
|
async function recoverPeer() {
|
|
@@ -548,15 +587,78 @@ async function startDaemon(config) {
|
|
|
548
587
|
if (!fs.existsSync(infoDir)) fs.mkdirSync(infoDir, { recursive: true });
|
|
549
588
|
fs.writeFileSync(
|
|
550
589
|
infoPath,
|
|
551
|
-
JSON.stringify({ pid: process.pid,
|
|
590
|
+
JSON.stringify({ pid: process.pid, slug, socketPath, startedAt: startTime, cliVersion })
|
|
552
591
|
);
|
|
592
|
+
startHealthCheckTimer();
|
|
553
593
|
scheduleNextPoll(0);
|
|
594
|
+
let bridgeCheckTimer = null;
|
|
595
|
+
const bridgeRestartTimestamps = [];
|
|
596
|
+
function pruneBridgeRestartTimestamps() {
|
|
597
|
+
const cutoff = Date.now() - BRIDGE_RAPID_RESTART_WINDOW_MS;
|
|
598
|
+
while (bridgeRestartTimestamps.length > 0 && bridgeRestartTimestamps[0] < cutoff) {
|
|
599
|
+
bridgeRestartTimestamps.shift();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function restartBridge() {
|
|
603
|
+
if (stopped || !config.bridge) return;
|
|
604
|
+
const { bridgeMode, bridgeScript, bridgeInfoPath, bridgeLogPath, bridgeProcessEnv } = config.bridge;
|
|
605
|
+
pruneBridgeRestartTimestamps();
|
|
606
|
+
if (bridgeRestartTimestamps.length >= BRIDGE_MAX_RAPID_RESTARTS) {
|
|
607
|
+
markError(
|
|
608
|
+
`bridge crashed ${BRIDGE_MAX_RAPID_RESTARTS} times within ${BRIDGE_RAPID_RESTART_WINDOW_MS / 1e3}s; giving up`
|
|
609
|
+
);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
bridgeRestartTimestamps.push(Date.now());
|
|
613
|
+
debugLog("bridge process is dead; restarting");
|
|
614
|
+
try {
|
|
615
|
+
const logFd = fs.openSync(bridgeLogPath, "a");
|
|
616
|
+
const child = fork(bridgeScript, [], {
|
|
617
|
+
detached: true,
|
|
618
|
+
stdio: buildBridgeForkStdio(logFd),
|
|
619
|
+
env: {
|
|
620
|
+
...bridgeProcessEnv,
|
|
621
|
+
PUBBLUE_BRIDGE_MODE: bridgeMode,
|
|
622
|
+
PUBBLUE_BRIDGE_SLUG: slug,
|
|
623
|
+
PUBBLUE_BRIDGE_SOCKET: socketPath,
|
|
624
|
+
PUBBLUE_BRIDGE_INFO: bridgeInfoPath
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
fs.closeSync(logFd);
|
|
628
|
+
if (child.connected) child.disconnect();
|
|
629
|
+
child.unref();
|
|
630
|
+
} catch (error) {
|
|
631
|
+
markError("failed to restart bridge process", error);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
function checkBridgeLiveness() {
|
|
635
|
+
if (stopped || !config.bridge) return;
|
|
636
|
+
if (!isBridgeRunning(slug)) restartBridge();
|
|
637
|
+
}
|
|
638
|
+
function startBridgeCheckTimer() {
|
|
639
|
+
if (!config.bridge) return;
|
|
640
|
+
bridgeCheckTimer = setInterval(checkBridgeLiveness, BRIDGE_CHECK_INTERVAL_MS);
|
|
641
|
+
}
|
|
642
|
+
function clearBridgeCheckTimer() {
|
|
643
|
+
if (bridgeCheckTimer) {
|
|
644
|
+
clearInterval(bridgeCheckTimer);
|
|
645
|
+
bridgeCheckTimer = null;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
async function stopBridgeProcess() {
|
|
649
|
+
const error = await stopBridge(slug);
|
|
650
|
+
if (error) debugLog(error);
|
|
651
|
+
}
|
|
652
|
+
startBridgeCheckTimer();
|
|
554
653
|
async function cleanup() {
|
|
555
654
|
if (stopped) return;
|
|
556
655
|
stopped = true;
|
|
557
656
|
clearPollingTimer();
|
|
558
657
|
clearLocalCandidateTimers();
|
|
559
658
|
clearRecoveryTimer();
|
|
659
|
+
clearHealthCheckTimer();
|
|
660
|
+
clearBridgeCheckTimer();
|
|
661
|
+
await stopBridgeProcess();
|
|
560
662
|
closeCurrentPeer();
|
|
561
663
|
ipcServer.close();
|
|
562
664
|
try {
|
|
@@ -670,8 +772,5 @@ async function startDaemon(config) {
|
|
|
670
772
|
}
|
|
671
773
|
|
|
672
774
|
export {
|
|
673
|
-
getTunnelWriteReadinessError,
|
|
674
|
-
shouldRecoverForBrowserAnswerChange,
|
|
675
|
-
getSignalPollDelayMs,
|
|
676
775
|
startDaemon
|
|
677
776
|
};
|
|
@@ -48,6 +48,58 @@ function shouldAcknowledgeMessage(channel, msg) {
|
|
|
48
48
|
var MAX_TUNNEL_EXPIRY_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
49
49
|
var DEFAULT_TUNNEL_EXPIRY_MS = 24 * 60 * 60 * 1e3;
|
|
50
50
|
|
|
51
|
+
// src/lib/tunnel-ipc.ts
|
|
52
|
+
import * as net from "net";
|
|
53
|
+
function getSocketPath(slug) {
|
|
54
|
+
return `/tmp/pubblue-${slug}.sock`;
|
|
55
|
+
}
|
|
56
|
+
async function ipcCall(socketPath, request) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
let settled = false;
|
|
59
|
+
let timeoutId = null;
|
|
60
|
+
const finish = (fn) => {
|
|
61
|
+
if (settled) return;
|
|
62
|
+
settled = true;
|
|
63
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
64
|
+
fn();
|
|
65
|
+
};
|
|
66
|
+
const client = net.createConnection(socketPath, () => {
|
|
67
|
+
client.write(`${JSON.stringify(request)}
|
|
68
|
+
`);
|
|
69
|
+
});
|
|
70
|
+
let data = "";
|
|
71
|
+
client.on("data", (chunk) => {
|
|
72
|
+
data += chunk.toString();
|
|
73
|
+
const newlineIdx = data.indexOf("\n");
|
|
74
|
+
if (newlineIdx !== -1) {
|
|
75
|
+
const line = data.slice(0, newlineIdx);
|
|
76
|
+
client.end();
|
|
77
|
+
try {
|
|
78
|
+
finish(() => resolve(JSON.parse(line)));
|
|
79
|
+
} catch {
|
|
80
|
+
finish(() => reject(new Error("Invalid response from daemon")));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
client.on("error", (err) => {
|
|
85
|
+
if (err.code === "ECONNREFUSED" || err.code === "ENOENT") {
|
|
86
|
+
finish(() => reject(new Error("Daemon not running. Is the tunnel still active?")));
|
|
87
|
+
} else {
|
|
88
|
+
finish(() => reject(err));
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
client.on("end", () => {
|
|
92
|
+
if (!data.includes("\n")) {
|
|
93
|
+
finish(() => reject(new Error("Daemon closed connection unexpectedly")));
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
timeoutId = setTimeout(() => {
|
|
97
|
+
client.destroy();
|
|
98
|
+
finish(() => reject(new Error("Daemon request timed out")));
|
|
99
|
+
}, 1e4);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
51
103
|
export {
|
|
52
104
|
CONTROL_CHANNEL,
|
|
53
105
|
CHANNELS,
|
|
@@ -56,5 +108,7 @@ export {
|
|
|
56
108
|
decodeMessage,
|
|
57
109
|
makeAckMessage,
|
|
58
110
|
parseAckMessage,
|
|
59
|
-
shouldAcknowledgeMessage
|
|
111
|
+
shouldAcknowledgeMessage,
|
|
112
|
+
getSocketPath,
|
|
113
|
+
ipcCall
|
|
60
114
|
};
|