pubblue 0.4.11 → 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-HAIOMGND.js → chunk-5GSMS3YU.js} +137 -21
- package/dist/{chunk-4YTJ2WKF.js → chunk-PFZT7M3E.js} +55 -1
- package/dist/chunk-YI45G6AG.js +759 -0
- package/dist/index.js +823 -1247
- package/dist/tunnel-bridge-entry.js +84 -42
- package/dist/tunnel-daemon-QN6TVUX6.js +8 -0
- package/dist/tunnel-daemon-entry.js +24 -10
- package/package.json +2 -2
- package/dist/chunk-7NFHPJ76.js +0 -79
- package/dist/chunk-HJ5LTUHS.js +0 -56
- package/dist/tunnel-daemon-7B2QUHK5.js +0 -11
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PubApiError,
|
|
3
|
+
buildBridgeForkStdio,
|
|
4
|
+
isBridgeRunning,
|
|
5
|
+
latestCliVersionPath,
|
|
6
|
+
readLatestCliVersion,
|
|
7
|
+
stopBridge
|
|
8
|
+
} from "./chunk-YI45G6AG.js";
|
|
1
9
|
import {
|
|
2
10
|
CHANNELS,
|
|
3
11
|
CONTROL_CHANNEL,
|
|
@@ -6,9 +14,10 @@ import {
|
|
|
6
14
|
makeAckMessage,
|
|
7
15
|
parseAckMessage,
|
|
8
16
|
shouldAcknowledgeMessage
|
|
9
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-PFZT7M3E.js";
|
|
10
18
|
|
|
11
19
|
// src/lib/tunnel-daemon.ts
|
|
20
|
+
import { fork } from "child_process";
|
|
12
21
|
import * as fs from "fs";
|
|
13
22
|
import * as net from "net";
|
|
14
23
|
import * as path from "path";
|
|
@@ -54,12 +63,16 @@ function generateOffer(peer, timeoutMs) {
|
|
|
54
63
|
}
|
|
55
64
|
|
|
56
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;
|
|
57
69
|
var OFFER_TIMEOUT_MS = 1e4;
|
|
58
|
-
var SIGNAL_POLL_WAITING_MS =
|
|
59
|
-
var SIGNAL_POLL_CONNECTED_MS =
|
|
70
|
+
var SIGNAL_POLL_WAITING_MS = 5e3;
|
|
71
|
+
var SIGNAL_POLL_CONNECTED_MS = 15e3;
|
|
72
|
+
var LOCAL_CANDIDATE_FLUSH_MS = 2e3;
|
|
60
73
|
var RECOVERY_DELAY_MS = 1e3;
|
|
61
74
|
var WRITE_ACK_TIMEOUT_MS = 5e3;
|
|
62
|
-
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.";
|
|
63
76
|
function getTunnelWriteReadinessError(isConnected) {
|
|
64
77
|
return isConnected ? null : NOT_CONNECTED_WRITE_ERROR;
|
|
65
78
|
}
|
|
@@ -69,10 +82,21 @@ function shouldRecoverForBrowserAnswerChange(params) {
|
|
|
69
82
|
if (!incomingBrowserAnswer) return false;
|
|
70
83
|
return incomingBrowserAnswer !== lastAppliedBrowserAnswer;
|
|
71
84
|
}
|
|
85
|
+
function getSignalPollDelayMs(params) {
|
|
86
|
+
const baseDelay = params.remoteDescriptionApplied ? SIGNAL_POLL_CONNECTED_MS : SIGNAL_POLL_WAITING_MS;
|
|
87
|
+
if (params.retryAfterSeconds === void 0) return baseDelay;
|
|
88
|
+
if (!Number.isFinite(params.retryAfterSeconds) || params.retryAfterSeconds <= 0) {
|
|
89
|
+
return baseDelay;
|
|
90
|
+
}
|
|
91
|
+
return Math.max(baseDelay, Math.ceil(params.retryAfterSeconds * 1e3));
|
|
92
|
+
}
|
|
72
93
|
|
|
73
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;
|
|
74
98
|
async function startDaemon(config) {
|
|
75
|
-
const {
|
|
99
|
+
const { slug, apiClient, socketPath, infoPath, cliVersion } = config;
|
|
76
100
|
const ndc = await import("node-datachannel");
|
|
77
101
|
const buffer = { messages: [] };
|
|
78
102
|
const startTime = Date.now();
|
|
@@ -95,12 +119,15 @@ async function startDaemon(config) {
|
|
|
95
119
|
let localCandidateInterval = null;
|
|
96
120
|
let localCandidateStopTimer = null;
|
|
97
121
|
let recoveryTimer = null;
|
|
122
|
+
let healthCheckTimer = null;
|
|
98
123
|
let lastError = null;
|
|
99
124
|
const debugEnabled = process.env.PUBBLUE_TUNNEL_DEBUG === "1";
|
|
125
|
+
let lastConnectedAt = startTime;
|
|
126
|
+
const versionFilePath = latestCliVersionPath();
|
|
100
127
|
function debugLog(message, error) {
|
|
101
128
|
if (!debugEnabled) return;
|
|
102
129
|
const detail = error === void 0 ? "" : ` | ${error instanceof Error ? `${error.name}: ${error.message}` : typeof error === "string" ? error : JSON.stringify(error)}`;
|
|
103
|
-
console.error(`[pubblue-daemon:${
|
|
130
|
+
console.error(`[pubblue-daemon:${slug}] ${message}${detail}`);
|
|
104
131
|
}
|
|
105
132
|
function markError(message, error) {
|
|
106
133
|
const detail = error === void 0 ? message : `${message}: ${error instanceof Error ? error.message : typeof error === "string" ? error : JSON.stringify(error)}`;
|
|
@@ -129,6 +156,27 @@ async function startDaemon(config) {
|
|
|
129
156
|
recoveryTimer = null;
|
|
130
157
|
}
|
|
131
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
|
+
}
|
|
132
180
|
function setupChannel(name, dc) {
|
|
133
181
|
channels.set(name, dc);
|
|
134
182
|
dc.onOpen(() => {
|
|
@@ -318,10 +366,10 @@ async function startDaemon(config) {
|
|
|
318
366
|
if (localCandidates.length <= lastSentCandidateCount) return;
|
|
319
367
|
const newOnes = localCandidates.slice(lastSentCandidateCount);
|
|
320
368
|
lastSentCandidateCount = localCandidates.length;
|
|
321
|
-
await apiClient.signal(
|
|
369
|
+
await apiClient.signal(slug, { candidates: newOnes }).catch((error) => {
|
|
322
370
|
debugLog("failed to publish local ICE candidates", error);
|
|
323
371
|
});
|
|
324
|
-
},
|
|
372
|
+
}, LOCAL_CANDIDATE_FLUSH_MS);
|
|
325
373
|
localCandidateStopTimer = setTimeout(() => {
|
|
326
374
|
clearLocalCandidateTimers();
|
|
327
375
|
}, 3e4);
|
|
@@ -335,6 +383,7 @@ async function startDaemon(config) {
|
|
|
335
383
|
if (stopped || currentPeer !== peer) return;
|
|
336
384
|
if (state === "connected") {
|
|
337
385
|
connected = true;
|
|
386
|
+
lastConnectedAt = Date.now();
|
|
338
387
|
flushQueuedAcks();
|
|
339
388
|
void replayStickyOutboundMessages();
|
|
340
389
|
return;
|
|
@@ -389,9 +438,9 @@ async function startDaemon(config) {
|
|
|
389
438
|
}, delayMs);
|
|
390
439
|
}
|
|
391
440
|
async function pollSignalingOnce() {
|
|
392
|
-
const
|
|
441
|
+
const session = await apiClient.getLive(slug);
|
|
393
442
|
if (shouldRecoverForBrowserAnswerChange({
|
|
394
|
-
incomingBrowserAnswer:
|
|
443
|
+
incomingBrowserAnswer: session.browserAnswer,
|
|
395
444
|
lastAppliedBrowserAnswer,
|
|
396
445
|
remoteDescriptionApplied
|
|
397
446
|
})) {
|
|
@@ -399,13 +448,13 @@ async function startDaemon(config) {
|
|
|
399
448
|
scheduleRecovery(0, true);
|
|
400
449
|
return;
|
|
401
450
|
}
|
|
402
|
-
if (
|
|
451
|
+
if (session.browserAnswer && !remoteDescriptionApplied) {
|
|
403
452
|
if (!peer) return;
|
|
404
453
|
try {
|
|
405
|
-
const answer = JSON.parse(
|
|
454
|
+
const answer = JSON.parse(session.browserAnswer);
|
|
406
455
|
peer.setRemoteDescription(answer.sdp, answer.type);
|
|
407
456
|
remoteDescriptionApplied = true;
|
|
408
|
-
lastAppliedBrowserAnswer =
|
|
457
|
+
lastAppliedBrowserAnswer = session.browserAnswer;
|
|
409
458
|
while (pendingRemoteCandidates.length > 0) {
|
|
410
459
|
const next = pendingRemoteCandidates.shift();
|
|
411
460
|
if (!next) break;
|
|
@@ -419,9 +468,9 @@ async function startDaemon(config) {
|
|
|
419
468
|
markError("failed to apply browser answer", error);
|
|
420
469
|
}
|
|
421
470
|
}
|
|
422
|
-
if (
|
|
423
|
-
const newCandidates =
|
|
424
|
-
lastBrowserCandidateCount =
|
|
471
|
+
if (session.browserCandidates.length > lastBrowserCandidateCount) {
|
|
472
|
+
const newCandidates = session.browserCandidates.slice(lastBrowserCandidateCount);
|
|
473
|
+
lastBrowserCandidateCount = session.browserCandidates.length;
|
|
425
474
|
for (const c of newCandidates) {
|
|
426
475
|
try {
|
|
427
476
|
const parsed = JSON.parse(c);
|
|
@@ -441,18 +490,24 @@ async function startDaemon(config) {
|
|
|
441
490
|
}
|
|
442
491
|
async function runPollingLoop() {
|
|
443
492
|
if (stopped) return;
|
|
493
|
+
let retryAfterSeconds;
|
|
444
494
|
try {
|
|
445
495
|
await pollSignalingOnce();
|
|
446
496
|
} catch (error) {
|
|
497
|
+
if (error instanceof PubApiError && error.status === 429) {
|
|
498
|
+
retryAfterSeconds = error.retryAfterSeconds;
|
|
499
|
+
}
|
|
447
500
|
markError("signaling poll failed", error);
|
|
448
501
|
}
|
|
449
|
-
|
|
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));
|
|
450
505
|
}
|
|
451
506
|
async function runNegotiationCycle() {
|
|
452
507
|
if (!peer) throw new Error("PeerConnection not initialized");
|
|
453
508
|
resetNegotiationState();
|
|
454
509
|
const offer = await generateOffer(peer, OFFER_TIMEOUT_MS);
|
|
455
|
-
await apiClient.signal(
|
|
510
|
+
await apiClient.signal(slug, { offer });
|
|
456
511
|
startLocalCandidateFlush();
|
|
457
512
|
}
|
|
458
513
|
async function recoverPeer() {
|
|
@@ -532,15 +587,78 @@ async function startDaemon(config) {
|
|
|
532
587
|
if (!fs.existsSync(infoDir)) fs.mkdirSync(infoDir, { recursive: true });
|
|
533
588
|
fs.writeFileSync(
|
|
534
589
|
infoPath,
|
|
535
|
-
JSON.stringify({ pid: process.pid,
|
|
590
|
+
JSON.stringify({ pid: process.pid, slug, socketPath, startedAt: startTime, cliVersion })
|
|
536
591
|
);
|
|
592
|
+
startHealthCheckTimer();
|
|
537
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();
|
|
538
653
|
async function cleanup() {
|
|
539
654
|
if (stopped) return;
|
|
540
655
|
stopped = true;
|
|
541
656
|
clearPollingTimer();
|
|
542
657
|
clearLocalCandidateTimers();
|
|
543
658
|
clearRecoveryTimer();
|
|
659
|
+
clearHealthCheckTimer();
|
|
660
|
+
clearBridgeCheckTimer();
|
|
661
|
+
await stopBridgeProcess();
|
|
544
662
|
closeCurrentPeer();
|
|
545
663
|
ipcServer.close();
|
|
546
664
|
try {
|
|
@@ -654,7 +772,5 @@ async function startDaemon(config) {
|
|
|
654
772
|
}
|
|
655
773
|
|
|
656
774
|
export {
|
|
657
|
-
getTunnelWriteReadinessError,
|
|
658
|
-
shouldRecoverForBrowserAnswerChange,
|
|
659
775
|
startDaemon
|
|
660
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
|
};
|