lunel-cli 0.1.78 → 0.1.80
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/index.js +254 -566
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14,9 +14,8 @@ import { createServer, createConnection } from "net";
|
|
|
14
14
|
import { createInterface } from "readline";
|
|
15
15
|
const DEFAULT_PROXY_URL = normalizeGatewayUrl(process.env.LUNEL_PROXY_URL || "https://gateway.lunel.dev");
|
|
16
16
|
const MANAGER_URL = normalizeGatewayUrl(process.env.LUNEL_MANAGER_URL || "https://manager.lunel.dev");
|
|
17
|
-
const CLOUD_JOB_CONFIG_PATH = "/etc/lunel/job.json";
|
|
18
|
-
const VM_HEARTBEAT_INTERVAL_MS = 10_000;
|
|
19
17
|
const CLI_ARGS = process.argv.slice(2);
|
|
18
|
+
const APPLE_REVIEW_CODE = "abcd";
|
|
20
19
|
import { createRequire } from "module";
|
|
21
20
|
const __require = createRequire(import.meta.url);
|
|
22
21
|
const VERSION = __require("../package.json").version;
|
|
@@ -58,7 +57,6 @@ let aiManager = null;
|
|
|
58
57
|
let currentSessionCode = null;
|
|
59
58
|
let currentSessionPassword = null;
|
|
60
59
|
let currentPrimaryGateway = DEFAULT_PROXY_URL;
|
|
61
|
-
let currentBackupGateway = null;
|
|
62
60
|
let activeGatewayUrl = DEFAULT_PROXY_URL;
|
|
63
61
|
let shuttingDown = false;
|
|
64
62
|
let activeControlWs = null;
|
|
@@ -70,51 +68,6 @@ function logWithTimestamp(scope, message, fields) {
|
|
|
70
68
|
}
|
|
71
69
|
const activeTunnels = new Map();
|
|
72
70
|
const PORT_SYNC_INTERVAL_MS = 30_000;
|
|
73
|
-
let cloudJobConfig = null;
|
|
74
|
-
let vmHeartbeatTimer = null;
|
|
75
|
-
async function readCloudJobConfig() {
|
|
76
|
-
try {
|
|
77
|
-
const data = await fs.readFile(CLOUD_JOB_CONFIG_PATH, "utf-8");
|
|
78
|
-
return JSON.parse(data);
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
function startVmHeartbeat(resumeToken) {
|
|
85
|
-
if (!cloudJobConfig?.session_code)
|
|
86
|
-
return;
|
|
87
|
-
if (vmHeartbeatTimer)
|
|
88
|
-
return;
|
|
89
|
-
const cfg = cloudJobConfig;
|
|
90
|
-
const sendHeartbeat = () => {
|
|
91
|
-
if (shuttingDown)
|
|
92
|
-
return;
|
|
93
|
-
fetch(`${MANAGER_URL}/v1/vm/heartbeat`, {
|
|
94
|
-
method: "POST",
|
|
95
|
-
headers: { "Content-Type": "application/json" },
|
|
96
|
-
body: JSON.stringify({
|
|
97
|
-
sandboxId: cfg.sandbox_id,
|
|
98
|
-
resumeToken,
|
|
99
|
-
sandmanUrl: cfg.sandman_url || "",
|
|
100
|
-
repoUrl: cfg.repo_url,
|
|
101
|
-
branch: cfg.branch,
|
|
102
|
-
vmProfile: cfg.vm_profile || "",
|
|
103
|
-
}),
|
|
104
|
-
}).catch((err) => {
|
|
105
|
-
if (!shuttingDown)
|
|
106
|
-
console.warn("[vm] heartbeat failed:", err.message);
|
|
107
|
-
});
|
|
108
|
-
};
|
|
109
|
-
sendHeartbeat();
|
|
110
|
-
vmHeartbeatTimer = setInterval(sendHeartbeat, VM_HEARTBEAT_INTERVAL_MS);
|
|
111
|
-
}
|
|
112
|
-
function stopVmHeartbeat() {
|
|
113
|
-
if (vmHeartbeatTimer) {
|
|
114
|
-
clearInterval(vmHeartbeatTimer);
|
|
115
|
-
vmHeartbeatTimer = null;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
71
|
const CLI_LOCAL_TCP_CONNECT_TIMEOUT_MS = 2_500;
|
|
119
72
|
const PROXY_WS_CONNECT_TIMEOUT_MS = 12_000;
|
|
120
73
|
const TUNNEL_SETUP_BUDGET_MS = 18_000;
|
|
@@ -258,9 +211,12 @@ function parseExtraPortsFromArgs(args) {
|
|
|
258
211
|
}
|
|
259
212
|
return Array.from(parsed).sort((a, b) => a - b);
|
|
260
213
|
}
|
|
214
|
+
function hasFlag(args, flag) {
|
|
215
|
+
return args.includes(flag);
|
|
216
|
+
}
|
|
261
217
|
const EXTRA_PORTS = parseExtraPortsFromArgs(CLI_ARGS);
|
|
218
|
+
const USE_APPLE_REVIEW_CODE = hasFlag(CLI_ARGS, "--abcd-code");
|
|
262
219
|
const SCAN_PORTS = Array.from(new Set([...DEV_PORTS, ...EXTRA_PORTS])).sort((a, b) => a - b);
|
|
263
|
-
const RUN_ABCD_MODE = CLI_ARGS.includes("--run-abcd");
|
|
264
220
|
function samePortSet(a, b) {
|
|
265
221
|
if (a.length !== b.length)
|
|
266
222
|
return false;
|
|
@@ -340,45 +296,19 @@ function generatePersistentSecret(length) {
|
|
|
340
296
|
}
|
|
341
297
|
return out;
|
|
342
298
|
}
|
|
343
|
-
function normalizePairingRoot(input) {
|
|
344
|
-
try {
|
|
345
|
-
return fssync.realpathSync(input);
|
|
346
|
-
}
|
|
347
|
-
catch {
|
|
348
|
-
return path.resolve(input);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
299
|
async function readCliConfig() {
|
|
352
300
|
try {
|
|
353
301
|
const raw = await fs.readFile(CLI_CONFIG_PATH, "utf-8");
|
|
354
302
|
const parsed = JSON.parse(raw);
|
|
355
|
-
const pairings = Array.isArray(parsed.pairings)
|
|
356
|
-
? parsed.pairings
|
|
357
|
-
.filter((entry) => {
|
|
358
|
-
return Boolean(entry &&
|
|
359
|
-
typeof entry.secret === "string" &&
|
|
360
|
-
typeof entry.root === "string" &&
|
|
361
|
-
typeof entry.hostname === "string" &&
|
|
362
|
-
typeof entry.phoneId === "string" &&
|
|
363
|
-
typeof entry.pairedAt === "number" &&
|
|
364
|
-
typeof entry.lastUsedAt === "number");
|
|
365
|
-
})
|
|
366
|
-
.map((entry) => ({
|
|
367
|
-
...entry,
|
|
368
|
-
root: normalizePairingRoot(entry.root),
|
|
369
|
-
}))
|
|
370
|
-
: [];
|
|
371
303
|
return {
|
|
372
304
|
version: 1,
|
|
373
305
|
deviceId: typeof parsed.deviceId === "string" && parsed.deviceId ? parsed.deviceId : generatePersistentSecret(32),
|
|
374
|
-
pairings,
|
|
375
306
|
};
|
|
376
307
|
}
|
|
377
308
|
catch {
|
|
378
309
|
return {
|
|
379
310
|
version: 1,
|
|
380
311
|
deviceId: generatePersistentSecret(32),
|
|
381
|
-
pairings: [],
|
|
382
312
|
};
|
|
383
313
|
}
|
|
384
314
|
}
|
|
@@ -393,42 +323,6 @@ async function getCliConfig() {
|
|
|
393
323
|
}
|
|
394
324
|
return await cliConfigPromise;
|
|
395
325
|
}
|
|
396
|
-
async function persistCliConfig(mutator) {
|
|
397
|
-
const current = await getCliConfig();
|
|
398
|
-
const next = mutator({
|
|
399
|
-
version: 1,
|
|
400
|
-
deviceId: current.deviceId,
|
|
401
|
-
pairings: [...current.pairings],
|
|
402
|
-
});
|
|
403
|
-
cliConfigPromise = Promise.resolve(next);
|
|
404
|
-
await writeCliConfig(next);
|
|
405
|
-
return next;
|
|
406
|
-
}
|
|
407
|
-
async function getSavedPairingForRoot(root) {
|
|
408
|
-
const normalizedRoot = normalizePairingRoot(root);
|
|
409
|
-
const config = await getCliConfig();
|
|
410
|
-
const matches = config.pairings
|
|
411
|
-
.filter((entry) => entry.root === normalizedRoot)
|
|
412
|
-
.sort((a, b) => b.lastUsedAt - a.lastUsedAt);
|
|
413
|
-
return matches[0] || null;
|
|
414
|
-
}
|
|
415
|
-
async function savePairing(pairing) {
|
|
416
|
-
await persistCliConfig((config) => {
|
|
417
|
-
const deduped = config.pairings.filter((entry) => entry.secret !== pairing.secret);
|
|
418
|
-
deduped.push(pairing);
|
|
419
|
-
deduped.sort((a, b) => b.lastUsedAt - a.lastUsedAt);
|
|
420
|
-
return {
|
|
421
|
-
...config,
|
|
422
|
-
pairings: deduped.slice(0, 50),
|
|
423
|
-
};
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
async function removePairing(secret) {
|
|
427
|
-
await persistCliConfig((config) => ({
|
|
428
|
-
...config,
|
|
429
|
-
pairings: config.pairings.filter((entry) => entry.secret !== secret),
|
|
430
|
-
}));
|
|
431
|
-
}
|
|
432
326
|
// ============================================================================
|
|
433
327
|
// File System Handlers
|
|
434
328
|
// ============================================================================
|
|
@@ -2323,12 +2217,7 @@ async function processMessage(message) {
|
|
|
2323
2217
|
result = handleSystemPing();
|
|
2324
2218
|
break;
|
|
2325
2219
|
case "pairDevice": {
|
|
2326
|
-
|
|
2327
|
-
if (!phoneId) {
|
|
2328
|
-
throw Object.assign(new Error("phoneId is required"), { code: "EINVAL" });
|
|
2329
|
-
}
|
|
2330
|
-
result = { ...(await registerPersistentPairing(phoneId)) };
|
|
2331
|
-
break;
|
|
2220
|
+
throw Object.assign(new Error("pairDevice is no longer supported"), { code: "EINVAL" });
|
|
2332
2221
|
}
|
|
2333
2222
|
default:
|
|
2334
2223
|
throw Object.assign(new Error(`Unknown action: ${ns}.${action}`), { code: "EINVAL" });
|
|
@@ -2651,157 +2540,75 @@ function normalizeGatewayUrl(input) {
|
|
|
2651
2540
|
const path = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/+$/, "");
|
|
2652
2541
|
return `${parsed.protocol}//${parsed.host}${path}`;
|
|
2653
2542
|
}
|
|
2654
|
-
async function
|
|
2655
|
-
const response = await fetch(`${MANAGER_URL}/
|
|
2656
|
-
method: "POST",
|
|
2657
|
-
headers: { "Content-Type": "application/json" },
|
|
2658
|
-
body: JSON.stringify(opts.requestedCode ? { requestedCode: opts.requestedCode } : {}),
|
|
2659
|
-
});
|
|
2543
|
+
async function createQrCode() {
|
|
2544
|
+
const response = await fetch(`${MANAGER_URL}/v2/qr`);
|
|
2660
2545
|
if (!response.ok) {
|
|
2661
|
-
throw new Error(`Failed to create
|
|
2546
|
+
throw new Error(`Failed to create QR code from manager: ${response.status}`);
|
|
2662
2547
|
}
|
|
2663
2548
|
return (await response.json());
|
|
2664
2549
|
}
|
|
2665
|
-
async function
|
|
2666
|
-
const
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2550
|
+
async function assembleWithCode(code) {
|
|
2551
|
+
const wsUrl = `${MANAGER_URL.replace(/^https:/, "wss:")}/v2/assemble?code=${encodeURIComponent(code)}&role=cli`;
|
|
2552
|
+
return await new Promise((resolve, reject) => {
|
|
2553
|
+
const ws = new WebSocket(wsUrl);
|
|
2554
|
+
let settled = false;
|
|
2555
|
+
const fail = (error) => {
|
|
2556
|
+
if (settled)
|
|
2557
|
+
return;
|
|
2558
|
+
settled = true;
|
|
2559
|
+
try {
|
|
2560
|
+
ws.close();
|
|
2561
|
+
}
|
|
2562
|
+
catch {
|
|
2563
|
+
// ignore
|
|
2564
|
+
}
|
|
2565
|
+
reject(error);
|
|
2566
|
+
};
|
|
2567
|
+
ws.on("message", (data) => {
|
|
2568
|
+
try {
|
|
2569
|
+
const parsed = JSON.parse(data.toString());
|
|
2570
|
+
if (parsed.type !== "assembled" || typeof parsed.code !== "string" || typeof parsed.password !== "string") {
|
|
2571
|
+
fail(new Error("Invalid assemble payload"));
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
if (settled)
|
|
2575
|
+
return;
|
|
2576
|
+
settled = true;
|
|
2577
|
+
ws.send(JSON.stringify({ type: "ack" }));
|
|
2578
|
+
resolve({ code: parsed.code, password: parsed.password });
|
|
2579
|
+
}
|
|
2580
|
+
catch (error) {
|
|
2581
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
2582
|
+
}
|
|
2583
|
+
});
|
|
2584
|
+
ws.on("close", (codeValue, reason) => {
|
|
2585
|
+
if (settled)
|
|
2586
|
+
return;
|
|
2587
|
+
fail(new Error(`Assemble socket closed (${codeValue}: ${reason.toString()})`));
|
|
2588
|
+
});
|
|
2589
|
+
ws.on("error", (error) => {
|
|
2590
|
+
fail(new Error(`Assemble socket error: ${error.message}`));
|
|
2591
|
+
});
|
|
2693
2592
|
});
|
|
2694
|
-
if (!response.ok) {
|
|
2695
|
-
throw new Error(`Failed to resolve session from manager: ${response.status}`);
|
|
2696
|
-
}
|
|
2697
|
-
const snapshot = await response.json();
|
|
2698
|
-
if (!snapshot.exists || !snapshot.valid || !snapshot.primary || !snapshot.resumeToken || !snapshot.code) {
|
|
2699
|
-
if (snapshot.reason === "session_finalized" || snapshot.reason === "not_found") {
|
|
2700
|
-
return null;
|
|
2701
|
-
}
|
|
2702
|
-
if (snapshot.state === "ended" || snapshot.state === "expired") {
|
|
2703
|
-
return null;
|
|
2704
|
-
}
|
|
2705
|
-
throw new Error(`Session resolve returned invalid snapshot (${snapshot.reason || "unknown"})`);
|
|
2706
|
-
}
|
|
2707
|
-
return {
|
|
2708
|
-
sessionId: snapshot.sessionId || "",
|
|
2709
|
-
code: snapshot.code || "",
|
|
2710
|
-
password: snapshot.resumeToken,
|
|
2711
|
-
primary: snapshot.primary,
|
|
2712
|
-
backup: snapshot.backup ?? null,
|
|
2713
|
-
state: snapshot.state,
|
|
2714
|
-
expiresAt: snapshot.expiresAt,
|
|
2715
|
-
};
|
|
2716
2593
|
}
|
|
2717
|
-
async function
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
const config = await getCliConfig();
|
|
2722
|
-
const response = await fetch(`${MANAGER_URL}/v1/pairings/register`, {
|
|
2723
|
-
method: "POST",
|
|
2724
|
-
headers: { "Content-Type": "application/json" },
|
|
2725
|
-
body: JSON.stringify({
|
|
2726
|
-
activeSecret: currentSessionPassword,
|
|
2727
|
-
phoneId,
|
|
2728
|
-
pcId: config.deviceId,
|
|
2729
|
-
root: ROOT_DIR,
|
|
2730
|
-
hostname: os.hostname(),
|
|
2731
|
-
}),
|
|
2732
|
-
});
|
|
2594
|
+
async function getAssignedProxyUrl(password) {
|
|
2595
|
+
const url = new URL("/v2/proxy", MANAGER_URL);
|
|
2596
|
+
url.searchParams.set("password", password);
|
|
2597
|
+
const response = await fetch(url);
|
|
2733
2598
|
if (!response.ok) {
|
|
2734
|
-
throw new Error(`Failed to
|
|
2599
|
+
throw new Error(`Failed to get proxy from manager: ${response.status}`);
|
|
2735
2600
|
}
|
|
2736
2601
|
const payload = await response.json();
|
|
2737
|
-
if (typeof payload.
|
|
2738
|
-
|
|
2739
|
-
typeof payload.root !== "string" ||
|
|
2740
|
-
typeof payload.phoneId !== "string" ||
|
|
2741
|
-
typeof payload.pairedAt !== "number" ||
|
|
2742
|
-
typeof payload.lastUsedAt !== "number") {
|
|
2743
|
-
throw new Error("Manager returned invalid pairing response");
|
|
2744
|
-
}
|
|
2745
|
-
const normalized = {
|
|
2746
|
-
secret: payload.secret,
|
|
2747
|
-
hostname: payload.hostname,
|
|
2748
|
-
root: normalizePairingRoot(payload.root),
|
|
2749
|
-
phoneId: payload.phoneId,
|
|
2750
|
-
pairedAt: payload.pairedAt,
|
|
2751
|
-
lastUsedAt: payload.lastUsedAt,
|
|
2752
|
-
};
|
|
2753
|
-
await savePairing({
|
|
2754
|
-
secret: normalized.secret,
|
|
2755
|
-
root: normalized.root,
|
|
2756
|
-
hostname: normalized.hostname,
|
|
2757
|
-
phoneId: normalized.phoneId,
|
|
2758
|
-
pairedAt: normalized.pairedAt,
|
|
2759
|
-
lastUsedAt: normalized.lastUsedAt,
|
|
2760
|
-
});
|
|
2761
|
-
currentSessionPassword = normalized.secret;
|
|
2762
|
-
return normalized;
|
|
2763
|
-
}
|
|
2764
|
-
async function lookupPersistentPairings() {
|
|
2765
|
-
const config = await getCliConfig();
|
|
2766
|
-
const response = await fetch(`${MANAGER_URL}/v1/pairings/lookup`, {
|
|
2767
|
-
method: "POST",
|
|
2768
|
-
headers: { "Content-Type": "application/json" },
|
|
2769
|
-
body: JSON.stringify({
|
|
2770
|
-
pcId: config.deviceId,
|
|
2771
|
-
root: ROOT_DIR,
|
|
2772
|
-
}),
|
|
2773
|
-
});
|
|
2774
|
-
if (!response.ok) {
|
|
2775
|
-
throw new Error(`Failed to look up persistent pairings (${response.status})`);
|
|
2602
|
+
if (typeof payload.proxyUrl !== "string" || !payload.proxyUrl) {
|
|
2603
|
+
throw new Error("Manager returned invalid proxy assignment");
|
|
2776
2604
|
}
|
|
2777
|
-
|
|
2778
|
-
const pairings = Array.isArray(payload.pairings) ? payload.pairings : [];
|
|
2779
|
-
return pairings
|
|
2780
|
-
.filter((entry) => (entry &&
|
|
2781
|
-
typeof entry.secret === "string" &&
|
|
2782
|
-
typeof entry.hostname === "string" &&
|
|
2783
|
-
typeof entry.root === "string" &&
|
|
2784
|
-
typeof entry.phoneId === "string" &&
|
|
2785
|
-
typeof entry.pairedAt === "number" &&
|
|
2786
|
-
typeof entry.lastUsedAt === "number"))
|
|
2787
|
-
.map((entry) => ({
|
|
2788
|
-
secret: entry.secret,
|
|
2789
|
-
root: normalizePairingRoot(entry.root),
|
|
2790
|
-
hostname: entry.hostname,
|
|
2791
|
-
phoneId: entry.phoneId,
|
|
2792
|
-
pairedAt: entry.pairedAt,
|
|
2793
|
-
lastUsedAt: entry.lastUsedAt,
|
|
2794
|
-
}));
|
|
2605
|
+
return normalizeGatewayUrl(payload.proxyUrl);
|
|
2795
2606
|
}
|
|
2796
|
-
function displayQR(
|
|
2607
|
+
function displayQR(code) {
|
|
2797
2608
|
console.log("\n");
|
|
2798
2609
|
qrcode.generate(code, { small: true }, (qr) => {
|
|
2799
2610
|
console.log(qr);
|
|
2800
2611
|
console.log(`\n Session code: ${code}\n`);
|
|
2801
|
-
console.log(` Primary gateway: ${primaryGateway}`);
|
|
2802
|
-
if (backupGateway) {
|
|
2803
|
-
console.log(` Backup gateway: ${backupGateway}`);
|
|
2804
|
-
}
|
|
2805
2612
|
console.log(` Root directory: ${ROOT_DIR}\n`);
|
|
2806
2613
|
console.log(" Scan the QR code with the Lunel app to connect.");
|
|
2807
2614
|
console.log(" Press Ctrl+C to exit.\n");
|
|
@@ -2816,14 +2623,8 @@ function buildWsUrl(gatewayUrl, role, channel) {
|
|
|
2816
2623
|
if (currentSessionPassword) {
|
|
2817
2624
|
query.set("password", currentSessionPassword);
|
|
2818
2625
|
}
|
|
2819
|
-
else if (currentSessionCode) {
|
|
2820
|
-
query.set("code", currentSessionCode);
|
|
2821
|
-
if (currentBackupGateway) {
|
|
2822
|
-
query.set("backup", currentBackupGateway);
|
|
2823
|
-
}
|
|
2824
|
-
}
|
|
2825
2626
|
else {
|
|
2826
|
-
throw new Error("missing
|
|
2627
|
+
throw new Error("missing password for websocket connect");
|
|
2827
2628
|
}
|
|
2828
2629
|
return `${wsBase}/v1/ws/${role}/${channel}?${query.toString()}`;
|
|
2829
2630
|
}
|
|
@@ -2831,7 +2632,6 @@ function gracefulShutdown() {
|
|
|
2831
2632
|
shuttingDown = true;
|
|
2832
2633
|
console.log("\nShutting down...");
|
|
2833
2634
|
void aiManager?.destroy();
|
|
2834
|
-
stopVmHeartbeat();
|
|
2835
2635
|
stopPortSync();
|
|
2836
2636
|
if (ptyProcess) {
|
|
2837
2637
|
ptyProcess.kill();
|
|
@@ -2853,216 +2653,195 @@ function gracefulShutdown() {
|
|
|
2853
2653
|
process.exit(0);
|
|
2854
2654
|
}
|
|
2855
2655
|
async function connectWebSocket() {
|
|
2856
|
-
const
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2656
|
+
const gatewayUrl = currentPrimaryGateway;
|
|
2657
|
+
await new Promise((resolve, reject) => {
|
|
2658
|
+
activeGatewayUrl = gatewayUrl;
|
|
2659
|
+
const controlUrl = buildWsUrl(gatewayUrl, "cli", "control");
|
|
2660
|
+
const dataUrl = buildWsUrl(gatewayUrl, "cli", "data");
|
|
2661
|
+
console.log(`Connecting to gateway ${gatewayUrl}...`);
|
|
2662
|
+
const controlWs = new WebSocket(controlUrl);
|
|
2663
|
+
const dataWs = new WebSocket(dataUrl);
|
|
2664
|
+
activeControlWs = controlWs;
|
|
2665
|
+
activeDataWs = dataWs;
|
|
2666
|
+
dataChannel = dataWs;
|
|
2667
|
+
let controlConnected = false;
|
|
2668
|
+
let dataConnected = false;
|
|
2669
|
+
let settled = false;
|
|
2670
|
+
let closeHandled = false;
|
|
2671
|
+
let closeReason = "";
|
|
2672
|
+
const failConnection = (reason) => {
|
|
2673
|
+
if (settled)
|
|
2674
|
+
return;
|
|
2675
|
+
settled = true;
|
|
2676
|
+
reject(new Error(reason));
|
|
2677
|
+
};
|
|
2678
|
+
const checkFullyConnected = () => {
|
|
2679
|
+
if (controlConnected && dataConnected && !settled) {
|
|
2680
|
+
settled = true;
|
|
2681
|
+
console.log("Connected to gateway (control + data channels).\n");
|
|
2682
|
+
resolve();
|
|
2683
|
+
}
|
|
2684
|
+
};
|
|
2685
|
+
const handleClose = (reason) => {
|
|
2686
|
+
if (closeHandled || shuttingDown)
|
|
2687
|
+
return;
|
|
2688
|
+
closeHandled = true;
|
|
2689
|
+
closeReason = reason;
|
|
2690
|
+
stopPortSync();
|
|
2691
|
+
cleanupAllTunnels();
|
|
2692
|
+
setTimeout(() => {
|
|
2693
|
+
if (shuttingDown)
|
|
2694
|
+
return;
|
|
2695
|
+
void handleConnectionDrop(closeReason);
|
|
2696
|
+
}, 50);
|
|
2697
|
+
};
|
|
2698
|
+
controlWs.on("open", () => {
|
|
2699
|
+
controlConnected = true;
|
|
2700
|
+
checkFullyConnected();
|
|
2701
|
+
});
|
|
2702
|
+
controlWs.on("message", async (data) => {
|
|
2703
|
+
try {
|
|
2704
|
+
const message = JSON.parse(data.toString());
|
|
2705
|
+
if ("type" in message) {
|
|
2706
|
+
if (message.type === "connected")
|
|
2877
2707
|
return;
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
const checkFullyConnected = () => {
|
|
2882
|
-
if (controlConnected && dataConnected && !settled) {
|
|
2883
|
-
settled = true;
|
|
2884
|
-
console.log("Connected to gateway (control + data channels).\n");
|
|
2885
|
-
resolve();
|
|
2886
|
-
}
|
|
2887
|
-
};
|
|
2888
|
-
const handleClose = (reason) => {
|
|
2889
|
-
if (closeHandled || shuttingDown)
|
|
2708
|
+
if (message.type === "peer_connected") {
|
|
2709
|
+
console.log("App connected!\n");
|
|
2710
|
+
startPortSync(controlWs);
|
|
2890
2711
|
return;
|
|
2891
|
-
closeHandled = true;
|
|
2892
|
-
closeReason = reason;
|
|
2893
|
-
stopPortSync();
|
|
2894
|
-
cleanupAllTunnels();
|
|
2895
|
-
setTimeout(() => {
|
|
2896
|
-
if (shuttingDown)
|
|
2897
|
-
return;
|
|
2898
|
-
void handleConnectionDrop(closeReason);
|
|
2899
|
-
}, 50);
|
|
2900
|
-
};
|
|
2901
|
-
controlWs.on("open", () => {
|
|
2902
|
-
controlConnected = true;
|
|
2903
|
-
checkFullyConnected();
|
|
2904
|
-
});
|
|
2905
|
-
controlWs.on("message", async (data) => {
|
|
2906
|
-
try {
|
|
2907
|
-
const message = JSON.parse(data.toString());
|
|
2908
|
-
if ("type" in message) {
|
|
2909
|
-
if (message.type === "connected")
|
|
2910
|
-
return;
|
|
2911
|
-
if (message.type === "session_password" && message.password) {
|
|
2912
|
-
const nextPassword = message.password;
|
|
2913
|
-
if (currentSessionPassword && currentSessionPassword.length > nextPassword.length) {
|
|
2914
|
-
return;
|
|
2915
|
-
}
|
|
2916
|
-
if (!currentSessionPassword)
|
|
2917
|
-
resetReplayBuffer(); // new session
|
|
2918
|
-
currentSessionPassword = nextPassword;
|
|
2919
|
-
console.log("[session] received reconnect password");
|
|
2920
|
-
return;
|
|
2921
|
-
}
|
|
2922
|
-
if (message.type === "peer_connected") {
|
|
2923
|
-
console.log("App connected!\n");
|
|
2924
|
-
startPortSync(controlWs);
|
|
2925
|
-
return;
|
|
2926
|
-
}
|
|
2927
|
-
if (message.type === "peer_disconnected") {
|
|
2928
|
-
console.log("App disconnected. Waiting for reconnect window.\n");
|
|
2929
|
-
stopPortSync();
|
|
2930
|
-
return;
|
|
2931
|
-
}
|
|
2932
|
-
if (message.type === "app_disconnected") {
|
|
2933
|
-
if (message.reconnectDeadline) {
|
|
2934
|
-
console.log(`[session] app disconnected, waiting until ${new Date(message.reconnectDeadline).toISOString()}`);
|
|
2935
|
-
}
|
|
2936
|
-
return;
|
|
2937
|
-
}
|
|
2938
|
-
if (message.type === "close_connection") {
|
|
2939
|
-
const reason = message.reason || "expired";
|
|
2940
|
-
console.log(`[session] closed by gateway: ${reason}`);
|
|
2941
|
-
if (reason === "session ended from app") {
|
|
2942
|
-
console.log("[session] Run `npx lunel-cli` again and scan the new QR code to reconnect.");
|
|
2943
|
-
}
|
|
2944
|
-
gracefulShutdown();
|
|
2945
|
-
return;
|
|
2946
|
-
}
|
|
2947
|
-
}
|
|
2948
|
-
if (isProtocolResponse(message)) {
|
|
2949
|
-
// Ignore server/app responses forwarded over WS; CLI only processes requests.
|
|
2950
|
-
return;
|
|
2951
|
-
}
|
|
2952
|
-
if (isProtocolRequest(message)) {
|
|
2953
|
-
const response = await processMessage(message);
|
|
2954
|
-
sendResponseOnData(response, dataWs);
|
|
2955
|
-
return;
|
|
2956
|
-
}
|
|
2957
|
-
console.warn("[router] Ignoring non-request control frame");
|
|
2958
2712
|
}
|
|
2959
|
-
|
|
2960
|
-
console.
|
|
2961
|
-
|
|
2962
|
-
});
|
|
2963
|
-
controlWs.on("close", (code, reason) => {
|
|
2964
|
-
if (!settled) {
|
|
2965
|
-
failConnection(`control close before ready (${code}: ${reason.toString()})`);
|
|
2713
|
+
if (message.type === "peer_disconnected") {
|
|
2714
|
+
console.log("App disconnected. Waiting for reconnect window.\n");
|
|
2715
|
+
stopPortSync();
|
|
2966
2716
|
return;
|
|
2967
2717
|
}
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
failConnection(`control ws error: ${error.message}`);
|
|
2718
|
+
if (message.type === "app_disconnected") {
|
|
2719
|
+
if (message.reconnectDeadline) {
|
|
2720
|
+
console.log(`[session] app disconnected, waiting until ${new Date(message.reconnectDeadline).toISOString()}`);
|
|
2721
|
+
}
|
|
2973
2722
|
return;
|
|
2974
2723
|
}
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
});
|
|
2981
|
-
dataWs.on("message", async (data) => {
|
|
2982
|
-
try {
|
|
2983
|
-
const raw = JSON.parse(data.toString());
|
|
2984
|
-
if (raw.type === "connected")
|
|
2985
|
-
return;
|
|
2986
|
-
// E2EE handshake messages (always plaintext)
|
|
2987
|
-
if (raw.type === "e2ee_hello" && typeof raw.pubkey === "string") {
|
|
2988
|
-
e2eeHandlePeerHello(raw.pubkey, dataWs);
|
|
2989
|
-
return;
|
|
2990
|
-
}
|
|
2991
|
-
if (raw.type === "e2ee_secure_ready") {
|
|
2992
|
-
e2eeHandlePeerReady();
|
|
2993
|
-
return;
|
|
2994
|
-
}
|
|
2995
|
-
// Reconnect request: reset E2EE so fresh handshake happens, then replay
|
|
2996
|
-
if (raw.ns === "system" && raw.action === "reconnect") {
|
|
2997
|
-
e2eeReset();
|
|
2998
|
-
const lastSeq = Number(raw.payload?.lastSeq ?? 0);
|
|
2999
|
-
const toReplay = replayBuffer.filter((e) => e.seq > lastSeq);
|
|
3000
|
-
console.log(`[replay] replaying ${toReplay.length} messages after seq ${lastSeq}`);
|
|
3001
|
-
// Replay without encryption — E2EE handshake hasn't completed yet
|
|
3002
|
-
for (const entry of toReplay)
|
|
3003
|
-
dataWs.send(JSON.stringify(entry.msg));
|
|
3004
|
-
return;
|
|
2724
|
+
if (message.type === "close_connection") {
|
|
2725
|
+
const reason = message.reason || "expired";
|
|
2726
|
+
console.log(`[session] closed by gateway: ${reason}`);
|
|
2727
|
+
if (reason === "session ended from app") {
|
|
2728
|
+
console.log("[session] Run `npx lunel-cli` again and scan the new QR code to reconnect.");
|
|
3005
2729
|
}
|
|
3006
|
-
|
|
3007
|
-
let message = raw;
|
|
3008
|
-
if (e2eeActive && typeof raw.enc === "string") {
|
|
3009
|
-
try {
|
|
3010
|
-
message = { ...raw, payload: e2eeDecrypt(raw.enc) };
|
|
3011
|
-
delete message.enc;
|
|
3012
|
-
}
|
|
3013
|
-
catch (decErr) {
|
|
3014
|
-
console.error("[e2ee] decryption failed:", decErr.message);
|
|
3015
|
-
return;
|
|
3016
|
-
}
|
|
3017
|
-
}
|
|
3018
|
-
if (isProtocolResponse(message)) {
|
|
3019
|
-
// Ignore server/app responses forwarded over WS; CLI only processes requests.
|
|
3020
|
-
return;
|
|
3021
|
-
}
|
|
3022
|
-
if (isProtocolRequest(message)) {
|
|
3023
|
-
const response = await processMessage(message);
|
|
3024
|
-
sendResponseOnData(response, dataWs);
|
|
3025
|
-
return;
|
|
3026
|
-
}
|
|
3027
|
-
console.warn("[router] Ignoring non-request data frame");
|
|
3028
|
-
}
|
|
3029
|
-
catch (error) {
|
|
3030
|
-
console.error("Error processing data message:", error);
|
|
3031
|
-
}
|
|
3032
|
-
});
|
|
3033
|
-
dataWs.on("close", (code, reason) => {
|
|
3034
|
-
// Reset backpressure state so reconnect starts fresh
|
|
3035
|
-
dataChannelPaused = false;
|
|
3036
|
-
if (dataChannelDrainTimer) {
|
|
3037
|
-
clearInterval(dataChannelDrainTimer);
|
|
3038
|
-
dataChannelDrainTimer = null;
|
|
3039
|
-
}
|
|
3040
|
-
if (!settled) {
|
|
3041
|
-
failConnection(`data close before ready (${code}: ${reason.toString()})`);
|
|
2730
|
+
gracefulShutdown();
|
|
3042
2731
|
return;
|
|
3043
2732
|
}
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
2733
|
+
}
|
|
2734
|
+
if (isProtocolResponse(message)) {
|
|
2735
|
+
// Ignore server/app responses forwarded over WS; CLI only processes requests.
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
if (isProtocolRequest(message)) {
|
|
2739
|
+
const response = await processMessage(message);
|
|
2740
|
+
sendResponseOnData(response, dataWs);
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
console.warn("[router] Ignoring non-request control frame");
|
|
2744
|
+
}
|
|
2745
|
+
catch (error) {
|
|
2746
|
+
console.error("Error processing control message:", error);
|
|
2747
|
+
}
|
|
2748
|
+
});
|
|
2749
|
+
controlWs.on("close", (code, reason) => {
|
|
2750
|
+
if (!settled) {
|
|
2751
|
+
failConnection(`control close before ready (${code}: ${reason.toString()})`);
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
handleClose(`control closed (${code}: ${reason.toString()})`);
|
|
2755
|
+
});
|
|
2756
|
+
controlWs.on("error", (error) => {
|
|
2757
|
+
if (!settled) {
|
|
2758
|
+
failConnection(`control ws error: ${error.message}`);
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
console.error("Control WebSocket error:", error.message);
|
|
2762
|
+
});
|
|
2763
|
+
dataWs.on("open", () => {
|
|
2764
|
+
dataConnected = true;
|
|
2765
|
+
checkFullyConnected();
|
|
2766
|
+
});
|
|
2767
|
+
dataWs.on("message", async (data) => {
|
|
2768
|
+
try {
|
|
2769
|
+
const raw = JSON.parse(data.toString());
|
|
2770
|
+
if (raw.type === "connected")
|
|
2771
|
+
return;
|
|
2772
|
+
// E2EE handshake messages (always plaintext)
|
|
2773
|
+
if (raw.type === "e2ee_hello" && typeof raw.pubkey === "string") {
|
|
2774
|
+
e2eeHandlePeerHello(raw.pubkey, dataWs);
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2777
|
+
if (raw.type === "e2ee_secure_ready") {
|
|
2778
|
+
e2eeHandlePeerReady();
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
// Reconnect request: reset E2EE so fresh handshake happens, then replay
|
|
2782
|
+
if (raw.ns === "system" && raw.action === "reconnect") {
|
|
2783
|
+
e2eeReset();
|
|
2784
|
+
const lastSeq = Number(raw.payload?.lastSeq ?? 0);
|
|
2785
|
+
const toReplay = replayBuffer.filter((e) => e.seq > lastSeq);
|
|
2786
|
+
console.log(`[replay] replaying ${toReplay.length} messages after seq ${lastSeq}`);
|
|
2787
|
+
// Replay without encryption — E2EE handshake hasn't completed yet
|
|
2788
|
+
for (const entry of toReplay)
|
|
2789
|
+
dataWs.send(JSON.stringify(entry.msg));
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
// Decrypt payload if E2EE is active
|
|
2793
|
+
let message = raw;
|
|
2794
|
+
if (e2eeActive && typeof raw.enc === "string") {
|
|
2795
|
+
try {
|
|
2796
|
+
message = { ...raw, payload: e2eeDecrypt(raw.enc) };
|
|
2797
|
+
delete message.enc;
|
|
3050
2798
|
}
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
if (!settled) {
|
|
3055
|
-
failConnection("connection timeout");
|
|
2799
|
+
catch (decErr) {
|
|
2800
|
+
console.error("[e2ee] decryption failed:", decErr.message);
|
|
2801
|
+
return;
|
|
3056
2802
|
}
|
|
3057
|
-
}
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
2803
|
+
}
|
|
2804
|
+
if (isProtocolResponse(message)) {
|
|
2805
|
+
// Ignore server/app responses forwarded over WS; CLI only processes requests.
|
|
2806
|
+
return;
|
|
2807
|
+
}
|
|
2808
|
+
if (isProtocolRequest(message)) {
|
|
2809
|
+
const response = await processMessage(message);
|
|
2810
|
+
sendResponseOnData(response, dataWs);
|
|
2811
|
+
return;
|
|
2812
|
+
}
|
|
2813
|
+
console.warn("[router] Ignoring non-request data frame");
|
|
2814
|
+
}
|
|
2815
|
+
catch (error) {
|
|
2816
|
+
console.error("Error processing data message:", error);
|
|
2817
|
+
}
|
|
2818
|
+
});
|
|
2819
|
+
dataWs.on("close", (code, reason) => {
|
|
2820
|
+
// Reset backpressure state so reconnect starts fresh
|
|
2821
|
+
dataChannelPaused = false;
|
|
2822
|
+
if (dataChannelDrainTimer) {
|
|
2823
|
+
clearInterval(dataChannelDrainTimer);
|
|
2824
|
+
dataChannelDrainTimer = null;
|
|
2825
|
+
}
|
|
2826
|
+
if (!settled) {
|
|
2827
|
+
failConnection(`data close before ready (${code}: ${reason.toString()})`);
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
handleClose(`data closed (${code}: ${reason.toString()})`);
|
|
2831
|
+
});
|
|
2832
|
+
dataWs.on("error", (error) => {
|
|
2833
|
+
if (!settled) {
|
|
2834
|
+
failConnection(`data ws error: ${error.message}`);
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
console.error("Data WebSocket error:", error.message);
|
|
2838
|
+
});
|
|
2839
|
+
setTimeout(() => {
|
|
2840
|
+
if (!settled) {
|
|
2841
|
+
failConnection("connection timeout");
|
|
2842
|
+
}
|
|
2843
|
+
}, 10000);
|
|
2844
|
+
});
|
|
3066
2845
|
}
|
|
3067
2846
|
async function handleConnectionDrop(reason) {
|
|
3068
2847
|
if (shuttingDown)
|
|
@@ -3079,16 +2858,7 @@ async function handleConnectionDrop(reason) {
|
|
|
3079
2858
|
const base = Math.min(250 * 2 ** (attempt - 1), 30_000);
|
|
3080
2859
|
const delayMs = Math.round(base * (0.8 + Math.random() * 0.4));
|
|
3081
2860
|
try {
|
|
3082
|
-
|
|
3083
|
-
if (!resolved) {
|
|
3084
|
-
console.error("[reconnect] session no longer exists or is finalized");
|
|
3085
|
-
gracefulShutdown();
|
|
3086
|
-
return;
|
|
3087
|
-
}
|
|
3088
|
-
currentSessionCode = resolved.code;
|
|
3089
|
-
currentSessionPassword = resolved.password;
|
|
3090
|
-
currentPrimaryGateway = normalizeGatewayUrl(resolved.primary);
|
|
3091
|
-
currentBackupGateway = resolved.backup ? normalizeGatewayUrl(resolved.backup) : null;
|
|
2861
|
+
currentPrimaryGateway = await getAssignedProxyUrl(currentSessionPassword);
|
|
3092
2862
|
await connectWebSocket();
|
|
3093
2863
|
console.log(`[reconnect] connected via ${activeGatewayUrl}`);
|
|
3094
2864
|
return;
|
|
@@ -3105,14 +2875,6 @@ async function main() {
|
|
|
3105
2875
|
if (EXTRA_PORTS.length > 0) {
|
|
3106
2876
|
console.log(`Extra ports enabled: ${EXTRA_PORTS.join(", ")}`);
|
|
3107
2877
|
}
|
|
3108
|
-
// Detect cloud VM mode
|
|
3109
|
-
cloudJobConfig = await readCloudJobConfig();
|
|
3110
|
-
if (cloudJobConfig?.session_code) {
|
|
3111
|
-
console.log(`[cloud] Running in VM mode (sandbox: ${cloudJobConfig.sandbox_id})`);
|
|
3112
|
-
}
|
|
3113
|
-
if (RUN_ABCD_MODE) {
|
|
3114
|
-
console.log("[review] Running in pinned review session mode (code: abcd)");
|
|
3115
|
-
}
|
|
3116
2878
|
try {
|
|
3117
2879
|
await getCliConfig();
|
|
3118
2880
|
console.log("Checking PTY runtime...");
|
|
@@ -3133,98 +2895,24 @@ async function main() {
|
|
|
3133
2895
|
checkDataChannelBackpressure();
|
|
3134
2896
|
}
|
|
3135
2897
|
});
|
|
3136
|
-
let
|
|
3137
|
-
if (
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
else if (cloudJobConfig?.session_code) {
|
|
3142
|
-
// Cloud mode: connect to an existing session assigned by the manager
|
|
3143
|
-
session = await connectToCloudSession(cloudJobConfig.session_code);
|
|
3144
|
-
console.log(`[cloud] Connected to session ${session.code} via ${session.primary}`);
|
|
2898
|
+
let codeToAssemble;
|
|
2899
|
+
if (USE_APPLE_REVIEW_CODE) {
|
|
2900
|
+
codeToAssemble = APPLE_REVIEW_CODE;
|
|
2901
|
+
currentSessionCode = codeToAssemble;
|
|
2902
|
+
console.log(`Using fixed review code: ${APPLE_REVIEW_CODE}`);
|
|
3145
2903
|
}
|
|
3146
2904
|
else {
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
// fall back to QR
|
|
3158
|
-
}
|
|
3159
|
-
}
|
|
3160
|
-
if (savedPairing) {
|
|
3161
|
-
try {
|
|
3162
|
-
const resolved = await resolveSessionByResumeToken(savedPairing.secret);
|
|
3163
|
-
if (resolved) {
|
|
3164
|
-
session = resolved;
|
|
3165
|
-
console.log(`[pairing] Waiting on saved connection for ${savedPairing.hostname}`);
|
|
3166
|
-
}
|
|
3167
|
-
else {
|
|
3168
|
-
await removePairing(savedPairing.secret);
|
|
3169
|
-
const remotePairings = await lookupPersistentPairings().catch(() => []);
|
|
3170
|
-
const fallbackPairing = remotePairings.sort((a, b) => b.lastUsedAt - a.lastUsedAt)[0] || null;
|
|
3171
|
-
if (fallbackPairing) {
|
|
3172
|
-
await savePairing(fallbackPairing);
|
|
3173
|
-
const fallbackResolved = await resolveSessionByResumeToken(fallbackPairing.secret);
|
|
3174
|
-
if (fallbackResolved) {
|
|
3175
|
-
session = fallbackResolved;
|
|
3176
|
-
console.log(`[pairing] Waiting on saved connection for ${fallbackPairing.hostname}`);
|
|
3177
|
-
}
|
|
3178
|
-
else {
|
|
3179
|
-
await removePairing(fallbackPairing.secret);
|
|
3180
|
-
session = await createSessionFromManager();
|
|
3181
|
-
displayQR(normalizeGatewayUrl(session.primary), session.backup ? normalizeGatewayUrl(session.backup) : null, session.code);
|
|
3182
|
-
}
|
|
3183
|
-
}
|
|
3184
|
-
else {
|
|
3185
|
-
session = await createSessionFromManager();
|
|
3186
|
-
displayQR(normalizeGatewayUrl(session.primary), session.backup ? normalizeGatewayUrl(session.backup) : null, session.code);
|
|
3187
|
-
}
|
|
3188
|
-
}
|
|
3189
|
-
}
|
|
3190
|
-
catch {
|
|
3191
|
-
await removePairing(savedPairing.secret);
|
|
3192
|
-
const remotePairings = await lookupPersistentPairings().catch(() => []);
|
|
3193
|
-
const fallbackPairing = remotePairings.sort((a, b) => b.lastUsedAt - a.lastUsedAt)[0] || null;
|
|
3194
|
-
if (fallbackPairing) {
|
|
3195
|
-
await savePairing(fallbackPairing);
|
|
3196
|
-
const fallbackResolved = await resolveSessionByResumeToken(fallbackPairing.secret);
|
|
3197
|
-
if (fallbackResolved) {
|
|
3198
|
-
session = fallbackResolved;
|
|
3199
|
-
console.log(`[pairing] Waiting on saved connection for ${fallbackPairing.hostname}`);
|
|
3200
|
-
}
|
|
3201
|
-
else {
|
|
3202
|
-
await removePairing(fallbackPairing.secret);
|
|
3203
|
-
session = await createSessionFromManager();
|
|
3204
|
-
displayQR(normalizeGatewayUrl(session.primary), session.backup ? normalizeGatewayUrl(session.backup) : null, session.code);
|
|
3205
|
-
}
|
|
3206
|
-
}
|
|
3207
|
-
else {
|
|
3208
|
-
session = await createSessionFromManager();
|
|
3209
|
-
displayQR(normalizeGatewayUrl(session.primary), session.backup ? normalizeGatewayUrl(session.backup) : null, session.code);
|
|
3210
|
-
}
|
|
3211
|
-
}
|
|
3212
|
-
}
|
|
3213
|
-
else {
|
|
3214
|
-
// Public mode: create a new session and show QR code
|
|
3215
|
-
session = await createSessionFromManager();
|
|
3216
|
-
displayQR(normalizeGatewayUrl(session.primary), session.backup ? normalizeGatewayUrl(session.backup) : null, session.code);
|
|
3217
|
-
}
|
|
3218
|
-
}
|
|
3219
|
-
currentPrimaryGateway = normalizeGatewayUrl(session.primary);
|
|
3220
|
-
currentBackupGateway = session.backup ? normalizeGatewayUrl(session.backup) : null;
|
|
3221
|
-
currentSessionPassword = session.password;
|
|
2905
|
+
const qr = await createQrCode();
|
|
2906
|
+
codeToAssemble = qr.code;
|
|
2907
|
+
currentSessionCode = qr.code;
|
|
2908
|
+
displayQR(qr.code);
|
|
2909
|
+
}
|
|
2910
|
+
const assembled = await assembleWithCode(codeToAssemble);
|
|
2911
|
+
resetReplayBuffer();
|
|
2912
|
+
currentSessionCode = assembled.code;
|
|
2913
|
+
currentSessionPassword = assembled.password;
|
|
2914
|
+
currentPrimaryGateway = await getAssignedProxyUrl(assembled.password);
|
|
3222
2915
|
activeGatewayUrl = currentPrimaryGateway;
|
|
3223
|
-
currentSessionCode = session.code;
|
|
3224
|
-
// Start VM heartbeat in cloud mode (session.password = resumeToken)
|
|
3225
|
-
if (cloudJobConfig?.session_code) {
|
|
3226
|
-
startVmHeartbeat(session.password);
|
|
3227
|
-
}
|
|
3228
2916
|
await connectWebSocket();
|
|
3229
2917
|
}
|
|
3230
2918
|
catch (error) {
|