dsh-mobile 0.4.2 → 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/CHANGELOG.md +17 -3
- package/CONTRIBUTORS.md +53 -0
- package/README.en.md +50 -20
- package/README.md +48 -20
- package/bin/dsh-mobile-funnel-linux-arm64 +0 -0
- package/bin/dsh-mobile-funnel-linux-x64 +0 -0
- package/docs/CLOUDFLARE_TUNNEL.en.md +2 -2
- package/docs/CLOUDFLARE_TUNNEL.md +2 -2
- package/lib/cli.js.map +1 -1
- package/lib/client.js +6 -6
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +63 -15
- package/lib/index.mjs +599 -148
- package/lib/index.mjs.map +1 -1
- package/package.json +11 -8
package/lib/index.mjs
CHANGED
|
@@ -877,6 +877,17 @@ var FollowingMobileAccessRuntime = class {
|
|
|
877
877
|
async initialize(refreshIntervalMs) {
|
|
878
878
|
await this.refresh();
|
|
879
879
|
if (refreshIntervalMs === void 0) return;
|
|
880
|
+
this.beginPolling(refreshIntervalMs);
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Arm the refresh poller without requiring an initial selection.
|
|
884
|
+
* Boot-degraded path: the first selection already failed (e.g. the saved
|
|
885
|
+
* LAN is down), but later refreshes must still get their chance so a
|
|
886
|
+
* returning network recovers without a restart. Failures keep flowing to
|
|
887
|
+
* onRefreshError; close() stays the single teardown.
|
|
888
|
+
*/
|
|
889
|
+
beginPolling(refreshIntervalMs) {
|
|
890
|
+
if (this.timer !== void 0) return;
|
|
880
891
|
this.timer = setInterval(() => {
|
|
881
892
|
this.refresh().catch(this.onRefreshError);
|
|
882
893
|
}, refreshIntervalMs);
|
|
@@ -2424,6 +2435,19 @@ const MOBILE_LAYOUT_PATH = `${AUTH_PREFIX}/mobile-layout.js`;
|
|
|
2424
2435
|
const MOBILE_BOOT_BATCH_PREFIX = `${AUTH_PREFIX}/mobile-boot/`;
|
|
2425
2436
|
const MAX_MOBILE_BOOT_BATCH_BYTES = 33554432;
|
|
2426
2437
|
const MAX_MOBILE_BOOT_ENTRY_BYTES = 8388608;
|
|
2438
|
+
/**
|
|
2439
|
+
* Per-merged-batch byte budget. One upstream application batch can carry dozens
|
|
2440
|
+
* of client bundles whose combined size exceeds the hard batch cap; the layout
|
|
2441
|
+
* batch is therefore chunked into multiple merged batches, each kept safely
|
|
2442
|
+
* below {@link MAX_MOBILE_BOOT_BATCH_BYTES} so assembly can never reject it.
|
|
2443
|
+
*/
|
|
2444
|
+
const MOBILE_BOOT_CHUNK_BYTES = 16777216;
|
|
2445
|
+
/** Bytes the assembly emits after each entry (`body` plus `\n;\n`). */
|
|
2446
|
+
const MOBILE_BOOT_SEPARATOR_BYTES = 3;
|
|
2447
|
+
/** Freshness window for cached upstream bundle byte sizes. */
|
|
2448
|
+
const MOBILE_BOOT_SIZE_CACHE_TTL_MS = 3e4;
|
|
2449
|
+
/** Marker header distinguishing size probes from ordinary proxied bundle fetches. */
|
|
2450
|
+
const MOBILE_BOOT_SIZE_PROBE_HEADER = "x-dsh-mobile-size-probe";
|
|
2427
2451
|
const MAX_MOBILE_BOOT_BATCHES = 8;
|
|
2428
2452
|
const MOBILE_BOOT_UPSTREAM_ATTEMPTS = 4;
|
|
2429
2453
|
const MOBILE_BOOT_RETRY_DELAY_MS = 150;
|
|
@@ -2508,11 +2532,17 @@ function revisionedMobileBatchPath(entries) {
|
|
|
2508
2532
|
path: `${MOBILE_BOOT_BATCH_PREFIX}${key}.js`
|
|
2509
2533
|
};
|
|
2510
2534
|
}
|
|
2511
|
-
|
|
2535
|
+
/**
|
|
2536
|
+
* Parse one upstream boot manifest and rewire the layout entry onto the
|
|
2537
|
+
* gateway-owned layout module. The layout entry's URL and revision become the
|
|
2538
|
+
* local `mobile-layout.js` path; the dedicated frontend is authenticated in the
|
|
2539
|
+
* gateway, so the requested layout needs no upstream address.
|
|
2540
|
+
*/
|
|
2541
|
+
function parseMobileBootPlan(html) {
|
|
2512
2542
|
const assignment = /(?:window\.__DSH_BOOT__|globalThis\["__DSH_BOOT__"\])\s*=\s*/u.exec(html);
|
|
2513
2543
|
if (assignment?.index === void 0) throw new Error("upstream DSH index has no boot manifest");
|
|
2514
|
-
const
|
|
2515
|
-
const valueStart =
|
|
2544
|
+
const replaceStart = assignment.index;
|
|
2545
|
+
const valueStart = replaceStart + assignment[0].length;
|
|
2516
2546
|
const scriptEnd = html.indexOf("<\/script>", valueStart);
|
|
2517
2547
|
if (scriptEnd < 0) throw new Error("upstream DSH boot manifest script is incomplete");
|
|
2518
2548
|
const source = html.slice(valueStart, scriptEnd).trim().replace(/;$/u, "");
|
|
@@ -2526,45 +2556,122 @@ function rewriteMobileIndexWithBatch(html) {
|
|
|
2526
2556
|
if (dependencyProfile === void 0) throw new Error("upstream DSH layout module has unsupported dependencies");
|
|
2527
2557
|
layout[0].url = MOBILE_LAYOUT_PATH;
|
|
2528
2558
|
layout[0].rev = `dsh-mobile-layout-${DSH_MOBILE_VERSION}`;
|
|
2529
|
-
const
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2559
|
+
const parsedBatches = parsed.batches;
|
|
2560
|
+
if (parsedBatches === void 0) return Object.freeze({
|
|
2561
|
+
parsed,
|
|
2562
|
+
entries,
|
|
2563
|
+
slotProvider: dependencyProfile.slots,
|
|
2564
|
+
assignment: assignment[0],
|
|
2565
|
+
replaceStart,
|
|
2566
|
+
replaceEnd: scriptEnd
|
|
2567
|
+
});
|
|
2568
|
+
if (!Array.isArray(parsedBatches)) throw new Error("upstream DSH boot manifest batches are malformed");
|
|
2569
|
+
const batches = parsedBatches;
|
|
2570
|
+
const entryById = new Map(entries.map((entry) => [entry.id, entry]));
|
|
2571
|
+
if (entryById.size !== entries.length) throw new Error("upstream DSH boot manifest has duplicate entries");
|
|
2572
|
+
const layoutBatches = [];
|
|
2573
|
+
for (const batch of batches) {
|
|
2574
|
+
if (batch === null || typeof batch !== "object" || batch.phase !== "bootstrap" && batch.phase !== "application" || typeof batch.url !== "string" || typeof batch.rev !== "string" || !Array.isArray(batch.entries) || batch.entries.length === 0 || batch.entries.some((id) => typeof id !== "string" || !entryById.has(id))) throw new Error("upstream DSH boot manifest batches are malformed");
|
|
2575
|
+
if (batch.entries.includes(MOBILE_LAYOUT_MODULE)) layoutBatches.push(batch);
|
|
2576
|
+
}
|
|
2577
|
+
if (layoutBatches.length !== 1 || layoutBatches[0]?.phase !== "application") throw new Error("upstream DSH boot manifest has no unique application layout batch");
|
|
2578
|
+
const layoutBatch = layoutBatches[0];
|
|
2579
|
+
const planEntries = layoutBatch.entries.map((id) => {
|
|
2580
|
+
const entry = entryById.get(id);
|
|
2581
|
+
if (entry === void 0 || typeof entry.url !== "string" || typeof entry.rev !== "string") throw new Error("upstream DSH boot manifest batches are malformed");
|
|
2582
|
+
return Object.freeze({
|
|
2583
|
+
id,
|
|
2584
|
+
url: entry.url,
|
|
2585
|
+
rev: entry.rev
|
|
2586
|
+
});
|
|
2587
|
+
});
|
|
2588
|
+
return Object.freeze({
|
|
2589
|
+
parsed,
|
|
2590
|
+
entries,
|
|
2591
|
+
batches,
|
|
2592
|
+
slotProvider: dependencyProfile.slots,
|
|
2593
|
+
assignment: assignment[0],
|
|
2594
|
+
replaceStart,
|
|
2595
|
+
replaceEnd: scriptEnd,
|
|
2596
|
+
layoutBatch,
|
|
2597
|
+
planEntries
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
/**
|
|
2601
|
+
* Partition one upstream application batch into merged batches plus pass-through
|
|
2602
|
+
* rows. Every entry whose bundle is at or above the per-entry cap keeps its own
|
|
2603
|
+
* upstream `/plugins` row (the gateway proxies it verbatim), because a merged
|
|
2604
|
+
* batch cannot carry it. The remaining entries are greedily packed into merged
|
|
2605
|
+
* batches under {@link MOBILE_BOOT_CHUNK_BYTES}; the layout module always merges,
|
|
2606
|
+
* since its body is the gateway-owned layout file, never an upstream fetch.
|
|
2607
|
+
*/
|
|
2608
|
+
function splitMobileBootBatch(entries, sizes, passThrough) {
|
|
2609
|
+
const rows = [];
|
|
2610
|
+
const plans = [];
|
|
2611
|
+
let chunk;
|
|
2612
|
+
const flush = () => {
|
|
2613
|
+
if (chunk === void 0 || chunk.entries.length === 0) return;
|
|
2614
|
+
const revision = revisionedMobileBatchPath(chunk.entries);
|
|
2615
|
+
rows.push({
|
|
2616
|
+
phase: "application",
|
|
2617
|
+
url: revision.path,
|
|
2618
|
+
rev: revision.key,
|
|
2619
|
+
entries: chunk.ids
|
|
2620
|
+
});
|
|
2621
|
+
plans.push(Object.freeze({
|
|
2622
|
+
key: revision.key,
|
|
2623
|
+
path: revision.path,
|
|
2624
|
+
entries: Object.freeze(chunk.entries)
|
|
2625
|
+
}));
|
|
2626
|
+
chunk = void 0;
|
|
2627
|
+
};
|
|
2628
|
+
for (const entry of entries) {
|
|
2629
|
+
const size = sizes.get(entry.url);
|
|
2630
|
+
if ((passThrough.has(entry.url) || size !== void 0 && size >= MAX_MOBILE_BOOT_ENTRY_BYTES) && entry.id !== MOBILE_LAYOUT_MODULE) {
|
|
2631
|
+
rows.push({
|
|
2632
|
+
phase: "application",
|
|
2548
2633
|
url: entry.url,
|
|
2549
|
-
rev: entry.rev
|
|
2634
|
+
rev: entry.rev,
|
|
2635
|
+
entries: [entry.id]
|
|
2550
2636
|
});
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
entries:
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2637
|
+
continue;
|
|
2638
|
+
}
|
|
2639
|
+
const bytes = (size ?? 0) + MOBILE_BOOT_SEPARATOR_BYTES;
|
|
2640
|
+
if (chunk !== void 0 && chunk.entries.length > 0 && chunk.bytes + bytes > MOBILE_BOOT_CHUNK_BYTES) flush();
|
|
2641
|
+
chunk ??= {
|
|
2642
|
+
ids: [],
|
|
2643
|
+
entries: [],
|
|
2644
|
+
bytes: 0
|
|
2645
|
+
};
|
|
2646
|
+
chunk.ids.push(entry.id);
|
|
2647
|
+
chunk.entries.push(entry);
|
|
2648
|
+
chunk.bytes += bytes;
|
|
2649
|
+
}
|
|
2650
|
+
flush();
|
|
2651
|
+
return {
|
|
2652
|
+
plans,
|
|
2653
|
+
rows
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2656
|
+
function rewriteMobileIndexWithBatch(html, options = {}) {
|
|
2657
|
+
const plan = parseMobileBootPlan(html);
|
|
2658
|
+
const remoteSettings = orderAuthenticatedSettings(plan.entries, plan.slotProvider);
|
|
2659
|
+
let batches = [];
|
|
2660
|
+
if (plan.batches !== void 0 && plan.layoutBatch !== void 0 && plan.planEntries !== void 0) {
|
|
2661
|
+
const split = splitMobileBootBatch(plan.planEntries, options.sizes ?? /* @__PURE__ */ new Map(), options.passThrough ?? /* @__PURE__ */ new Set());
|
|
2662
|
+
const at = plan.batches.indexOf(plan.layoutBatch);
|
|
2663
|
+
if (at < 0) throw new Error("upstream DSH boot manifest has no unique application layout batch");
|
|
2664
|
+
plan.batches.splice(at, 1, ...split.rows);
|
|
2665
|
+
batches = Object.freeze(split.plans);
|
|
2666
|
+
plan.parsed.rev = createHash("sha256").update(JSON.stringify({
|
|
2667
|
+
entries: plan.entries,
|
|
2668
|
+
batches: plan.batches
|
|
2562
2669
|
})).digest("hex").slice(0, 16);
|
|
2563
2670
|
}
|
|
2564
|
-
const replacement = `${remoteSettings ? MOBILE_AUTHENTICATED_TRANSPORT_BOOTSTRAP : ""}${MOBILE_CSRF_FETCH_BOOTSTRAP}window.__DSH_MOBILE_FRONTEND__="dedicated";${assignment
|
|
2671
|
+
const replacement = `${remoteSettings ? MOBILE_AUTHENTICATED_TRANSPORT_BOOTSTRAP : ""}${MOBILE_CSRF_FETCH_BOOTSTRAP}window.__DSH_MOBILE_FRONTEND__="dedicated";${plan.assignment}${JSON.stringify(plan.parsed)};`;
|
|
2565
2672
|
return Object.freeze({
|
|
2566
|
-
html: ensureMobileViewport(ensureMobileCompatibility(`${html.slice(0,
|
|
2567
|
-
|
|
2673
|
+
html: ensureMobileViewport(ensureMobileCompatibility(`${html.slice(0, plan.replaceStart)}${replacement}${html.slice(plan.replaceEnd)}`)),
|
|
2674
|
+
batches
|
|
2568
2675
|
});
|
|
2569
2676
|
}
|
|
2570
2677
|
/** Replace only DSH's layout client module while retaining its complete plugin graph. */
|
|
@@ -2884,6 +2991,13 @@ function discoveryBroadcastTargets(cidrs) {
|
|
|
2884
2991
|
}
|
|
2885
2992
|
return [...targets];
|
|
2886
2993
|
}
|
|
2994
|
+
function discoveryFailure(error) {
|
|
2995
|
+
const code = error instanceof Error && typeof error.code === "string" ? error.code : "unknown";
|
|
2996
|
+
return Object.freeze({
|
|
2997
|
+
code,
|
|
2998
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
2887
3001
|
function extensionTarget(pathname) {
|
|
2888
3002
|
const prefix = `${AUTH_PREFIX}/extensions`;
|
|
2889
3003
|
if (pathname === prefix || pathname === `${prefix}/` || pathname === `${prefix}/manifest`) return { kind: "manifest" };
|
|
@@ -2981,6 +3095,7 @@ var MobileAccessGateway = class {
|
|
|
2981
3095
|
upstreamAuthenticatedUrl;
|
|
2982
3096
|
extraWebSocketPaths;
|
|
2983
3097
|
blockedUpgradeLog;
|
|
3098
|
+
onDiscoveryDegraded;
|
|
2984
3099
|
access;
|
|
2985
3100
|
listenerTlsEnabled;
|
|
2986
3101
|
tlsEnabled;
|
|
@@ -2995,6 +3110,7 @@ var MobileAccessGateway = class {
|
|
|
2995
3110
|
* it, "the phone cannot find this computer" has no diagnosable cause on the host.
|
|
2996
3111
|
*/
|
|
2997
3112
|
discoveryError;
|
|
3113
|
+
mdnsError;
|
|
2998
3114
|
/** Names of the bundled mobile assets that could not be read, if any. */
|
|
2999
3115
|
mobileAssetError;
|
|
3000
3116
|
bonjour;
|
|
@@ -3004,6 +3120,8 @@ var MobileAccessGateway = class {
|
|
|
3004
3120
|
activeRequests = /* @__PURE__ */ new Map();
|
|
3005
3121
|
activeWebSockets = /* @__PURE__ */ new Map();
|
|
3006
3122
|
mobileBootBatches = /* @__PURE__ */ new Map();
|
|
3123
|
+
/** Cached upstream bundle byte sizes (HEAD probes), keyed by resource URL. */
|
|
3124
|
+
bootEntrySizeCache = /* @__PURE__ */ new Map();
|
|
3007
3125
|
extensionEventListeners = /* @__PURE__ */ new Set();
|
|
3008
3126
|
extensionEventRevision = 0;
|
|
3009
3127
|
taskEventListeners = /* @__PURE__ */ new Set();
|
|
@@ -3024,12 +3142,13 @@ var MobileAccessGateway = class {
|
|
|
3024
3142
|
removeExtensionContentListener;
|
|
3025
3143
|
renewLimiter;
|
|
3026
3144
|
probeLimiter;
|
|
3027
|
-
constructor(config, store, extensions, upstreamAuthenticatedUrl, extraWebSocketPaths, blockedUpgradeLog) {
|
|
3145
|
+
constructor(config, store, extensions, upstreamAuthenticatedUrl, extraWebSocketPaths, blockedUpgradeLog, onDiscoveryDegraded) {
|
|
3028
3146
|
this.config = config;
|
|
3029
3147
|
this.extensions = extensions;
|
|
3030
3148
|
this.upstreamAuthenticatedUrl = upstreamAuthenticatedUrl;
|
|
3031
3149
|
this.extraWebSocketPaths = extraWebSocketPaths;
|
|
3032
3150
|
this.blockedUpgradeLog = blockedUpgradeLog;
|
|
3151
|
+
this.onDiscoveryDegraded = onDiscoveryDegraded;
|
|
3033
3152
|
this.listenerTlsEnabled = config.tls.mode === "provided";
|
|
3034
3153
|
this.tlsEnabled = config.publicTls;
|
|
3035
3154
|
this.access = new AccessController(store, {
|
|
@@ -3143,33 +3262,56 @@ var MobileAccessGateway = class {
|
|
|
3143
3262
|
const socket = createSocket("udp4");
|
|
3144
3263
|
this.discoverySocket = socket;
|
|
3145
3264
|
const announcement = this.discoveryAnnouncement(port);
|
|
3265
|
+
let binding = true;
|
|
3266
|
+
socket.on("error", (error) => {
|
|
3267
|
+
if (!binding) this.recordBroadcastFailure(error, true);
|
|
3268
|
+
});
|
|
3269
|
+
const sendAnnouncement = (targetPort, address) => {
|
|
3270
|
+
if (this.closing || this.discoveryError !== void 0) return;
|
|
3271
|
+
try {
|
|
3272
|
+
socket.send(announcement, targetPort, address, (error) => {
|
|
3273
|
+
if (error !== null) this.recordBroadcastFailure(error, true);
|
|
3274
|
+
});
|
|
3275
|
+
} catch (error) {
|
|
3276
|
+
this.recordBroadcastFailure(error, true);
|
|
3277
|
+
}
|
|
3278
|
+
};
|
|
3146
3279
|
socket.on("message", (message, remote) => {
|
|
3147
|
-
if (this.closing || !message.equals(DISCOVERY_QUERY) || !addressAllowed(remote.address, this.config.allowedCidrs)) return;
|
|
3148
|
-
|
|
3280
|
+
if (this.closing || this.discoveryError !== void 0 || !message.equals(DISCOVERY_QUERY) || !addressAllowed(remote.address, this.config.allowedCidrs)) return;
|
|
3281
|
+
sendAnnouncement(remote.port, remote.address);
|
|
3149
3282
|
});
|
|
3150
3283
|
const bindHost = isIP(this.config.listenHost) === 4 && isLoopbackAddress(this.config.listenHost) ? this.config.listenHost : "0.0.0.0";
|
|
3151
3284
|
if (await new Promise((resolve) => {
|
|
3152
3285
|
const onBindError = (error) => {
|
|
3153
3286
|
socket.off("error", onBindError);
|
|
3154
|
-
this.
|
|
3155
|
-
code: typeof error.code === "string" ? error.code : "unknown",
|
|
3156
|
-
message: typeof error.message === "string" ? error.message : String(error)
|
|
3157
|
-
});
|
|
3287
|
+
this.recordBroadcastFailure(error, false);
|
|
3158
3288
|
resolve(false);
|
|
3159
3289
|
};
|
|
3160
3290
|
socket.once("error", onBindError);
|
|
3161
|
-
|
|
3162
|
-
socket.
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3291
|
+
try {
|
|
3292
|
+
socket.bind(port, bindHost, () => {
|
|
3293
|
+
socket.off("error", onBindError);
|
|
3294
|
+
binding = false;
|
|
3295
|
+
try {
|
|
3296
|
+
socket.setBroadcast(true);
|
|
3297
|
+
resolve(true);
|
|
3298
|
+
} catch (error) {
|
|
3299
|
+
this.recordBroadcastFailure(error, true);
|
|
3300
|
+
resolve(false);
|
|
3301
|
+
}
|
|
3302
|
+
});
|
|
3303
|
+
} catch (error) {
|
|
3304
|
+
onBindError(error);
|
|
3305
|
+
}
|
|
3306
|
+
}) && this.discoveryError === void 0) {
|
|
3167
3307
|
const announce = () => {
|
|
3168
|
-
for (const target of discoveryBroadcastTargets(this.config.allowedCidrs))
|
|
3308
|
+
for (const target of discoveryBroadcastTargets(this.config.allowedCidrs)) sendAnnouncement(port, target);
|
|
3169
3309
|
};
|
|
3170
3310
|
announce();
|
|
3171
|
-
this.
|
|
3172
|
-
|
|
3311
|
+
if (this.discoveryError === void 0) {
|
|
3312
|
+
this.discoveryTimer = setInterval(announce, DISCOVERY_INTERVAL_MS);
|
|
3313
|
+
this.discoveryTimer.unref();
|
|
3314
|
+
}
|
|
3173
3315
|
} else {
|
|
3174
3316
|
this.discoverySocket = void 0;
|
|
3175
3317
|
try {
|
|
@@ -3177,8 +3319,12 @@ var MobileAccessGateway = class {
|
|
|
3177
3319
|
} catch {}
|
|
3178
3320
|
}
|
|
3179
3321
|
const deviceName = discoveryDeviceName();
|
|
3180
|
-
const
|
|
3322
|
+
const onMdnsError = (error) => {
|
|
3323
|
+
this.recordMdnsFailure(error);
|
|
3324
|
+
};
|
|
3325
|
+
const bonjour = new Bonjour({ disableIPv6: true }, onMdnsError);
|
|
3181
3326
|
this.bonjour = bonjour;
|
|
3327
|
+
bonjour.server.mdns.on("error", onMdnsError);
|
|
3182
3328
|
bonjour.publish({
|
|
3183
3329
|
name: `${deviceName} (${this.config.instanceId.slice(0, 8)})`,
|
|
3184
3330
|
type: MDNS_SERVICE_TYPE,
|
|
@@ -3194,6 +3340,25 @@ var MobileAccessGateway = class {
|
|
|
3194
3340
|
}
|
|
3195
3341
|
});
|
|
3196
3342
|
}
|
|
3343
|
+
reportDiscoveryDegraded(source, code) {
|
|
3344
|
+
try {
|
|
3345
|
+
this.onDiscoveryDegraded?.(source, code);
|
|
3346
|
+
} catch (error) {
|
|
3347
|
+
process.emitWarning(`DSH Mobile could not log ${source} discovery error: ${String(error)}`, { code: "DSH_MOBILE_DISCOVERY_LOG_FAILED" });
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
recordBroadcastFailure(error, notify) {
|
|
3351
|
+
if (this.closing || this.discoveryError !== void 0) return;
|
|
3352
|
+
this.discoveryError = discoveryFailure(error);
|
|
3353
|
+
if (this.discoveryTimer !== void 0) clearInterval(this.discoveryTimer);
|
|
3354
|
+
this.discoveryTimer = void 0;
|
|
3355
|
+
if (notify) this.reportDiscoveryDegraded("broadcast", this.discoveryError.code);
|
|
3356
|
+
}
|
|
3357
|
+
recordMdnsFailure(error) {
|
|
3358
|
+
if (this.closing || this.mdnsError !== void 0) return;
|
|
3359
|
+
this.mdnsError = discoveryFailure(error);
|
|
3360
|
+
this.reportDiscoveryDegraded("mdns", this.mdnsError.code);
|
|
3361
|
+
}
|
|
3197
3362
|
discoveryAnnouncement(port) {
|
|
3198
3363
|
return Buffer.from(JSON.stringify({
|
|
3199
3364
|
deviceName: discoveryDeviceName(),
|
|
@@ -3251,8 +3416,9 @@ var MobileAccessGateway = class {
|
|
|
3251
3416
|
discoveryStatus() {
|
|
3252
3417
|
return Object.freeze({
|
|
3253
3418
|
broadcast: this.discoverySocket !== void 0 && this.discoveryTimer !== void 0,
|
|
3254
|
-
mdns: this.bonjour !== void 0,
|
|
3419
|
+
mdns: this.bonjour !== void 0 && this.mdnsError === void 0,
|
|
3255
3420
|
...this.discoveryError === void 0 ? {} : { errorCode: `discovery_broadcast_${this.discoveryError.code}` },
|
|
3421
|
+
...this.mdnsError === void 0 ? {} : { mdnsErrorCode: `discovery_mdns_${this.mdnsError.code}` },
|
|
3256
3422
|
...this.mobileAssetError === void 0 ? {} : { mobileAssetsErrorCode: this.mobileAssetError }
|
|
3257
3423
|
});
|
|
3258
3424
|
}
|
|
@@ -3904,8 +4070,10 @@ var MobileAccessGateway = class {
|
|
|
3904
4070
|
}
|
|
3905
4071
|
let body;
|
|
3906
4072
|
try {
|
|
3907
|
-
const
|
|
3908
|
-
|
|
4073
|
+
const html = Buffer.concat(chunks).toString("utf8");
|
|
4074
|
+
const plan = parseMobileBootPlan(html);
|
|
4075
|
+
const rewritten = rewriteMobileIndexWithBatch(html, plan.planEntries === void 0 ? {} : await this.resolveMobileBootSizes(plan.planEntries));
|
|
4076
|
+
for (const batch of rewritten.batches) this.rememberMobileBootBatch(batch);
|
|
3909
4077
|
body = Buffer.from(rewritten.html);
|
|
3910
4078
|
} catch {
|
|
3911
4079
|
throw new HttpError(502, "upstream_unavailable");
|
|
@@ -3941,6 +4109,98 @@ var MobileAccessGateway = class {
|
|
|
3941
4109
|
this.mobileBootBatches.delete(oldest);
|
|
3942
4110
|
}
|
|
3943
4111
|
}
|
|
4112
|
+
/**
|
|
4113
|
+
* Determine which layout-batch entries can share a merged boot batch and which
|
|
4114
|
+
* must pass through. An entry at or above the per-entry cap, or one whose size
|
|
4115
|
+
* could not be probed, keeps its own upstream `/plugins` row: a merged batch
|
|
4116
|
+
* cannot carry it, and an unknown bundle must not risk the whole batch.
|
|
4117
|
+
*/
|
|
4118
|
+
async resolveMobileBootSizes(planEntries) {
|
|
4119
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
4120
|
+
const passThrough = /* @__PURE__ */ new Set();
|
|
4121
|
+
const candidates = planEntries.filter((entry) => entry.id !== MOBILE_LAYOUT_MODULE && entry.url.startsWith("/plugins/"));
|
|
4122
|
+
let cursor = 0;
|
|
4123
|
+
const worker = async () => {
|
|
4124
|
+
while (cursor < candidates.length) {
|
|
4125
|
+
const index = cursor++;
|
|
4126
|
+
const source = candidates[index].url;
|
|
4127
|
+
const size = await this.upstreamBundleSize(source);
|
|
4128
|
+
if (size === void 0 || size >= MAX_MOBILE_BOOT_ENTRY_BYTES) passThrough.add(source);
|
|
4129
|
+
else sizes.set(source, size);
|
|
4130
|
+
}
|
|
4131
|
+
};
|
|
4132
|
+
await Promise.all(Array.from({ length: Math.min(8, candidates.length) }, worker));
|
|
4133
|
+
return {
|
|
4134
|
+
sizes,
|
|
4135
|
+
passThrough
|
|
4136
|
+
};
|
|
4137
|
+
}
|
|
4138
|
+
/**
|
|
4139
|
+
* Measure one upstream bundle by reading its body. The upstream serves
|
|
4140
|
+
* `/plugins` bundles as chunked streams without a Content-Length, so a probe
|
|
4141
|
+
* counts bytes; it aborts instantly past the per-entry cap. Measurements are
|
|
4142
|
+
* cached for a short window.
|
|
4143
|
+
*/
|
|
4144
|
+
async upstreamBundleSize(source) {
|
|
4145
|
+
if (!source.startsWith("/plugins/") || source.includes("#")) return void 0;
|
|
4146
|
+
const cached = this.bootEntrySizeCache.get(source);
|
|
4147
|
+
if (cached !== void 0 && Date.now() - cached.at < MOBILE_BOOT_SIZE_CACHE_TTL_MS) return cached.size;
|
|
4148
|
+
const target = new URL(source, this.config.upstreamOrigin);
|
|
4149
|
+
if (target.origin !== this.config.upstreamOrigin.origin) return void 0;
|
|
4150
|
+
const upstreamCookie = await this.upstreamCookieHeader();
|
|
4151
|
+
let upstreamRequest;
|
|
4152
|
+
try {
|
|
4153
|
+
const proxied = await new Promise((resolve, reject) => {
|
|
4154
|
+
upstreamRequest = request({
|
|
4155
|
+
protocol: "http:",
|
|
4156
|
+
hostname: stripIpv6Brackets(this.config.upstreamOrigin.hostname),
|
|
4157
|
+
port: Number(this.config.upstreamOrigin.port),
|
|
4158
|
+
method: "GET",
|
|
4159
|
+
path: `${target.pathname}${target.search}`,
|
|
4160
|
+
headers: {
|
|
4161
|
+
host: this.config.upstreamOrigin.host,
|
|
4162
|
+
accept: "text/javascript",
|
|
4163
|
+
"accept-encoding": "identity",
|
|
4164
|
+
[MOBILE_BOOT_SIZE_PROBE_HEADER]: "1",
|
|
4165
|
+
...upstreamCookie === void 0 ? {} : { cookie: upstreamCookie }
|
|
4166
|
+
},
|
|
4167
|
+
agent: false
|
|
4168
|
+
});
|
|
4169
|
+
upstreamRequest.setTimeout(this.config.upstreamTimeoutMs, () => {
|
|
4170
|
+
upstreamRequest?.destroy(upstreamTimeoutError());
|
|
4171
|
+
});
|
|
4172
|
+
upstreamRequest.once("response", resolve);
|
|
4173
|
+
upstreamRequest.once("error", reject);
|
|
4174
|
+
upstreamRequest.end();
|
|
4175
|
+
});
|
|
4176
|
+
if (proxied.statusCode !== 200) {
|
|
4177
|
+
proxied.resume();
|
|
4178
|
+
return;
|
|
4179
|
+
}
|
|
4180
|
+
const size = await new Promise((resolve, reject) => {
|
|
4181
|
+
let bytes = 0;
|
|
4182
|
+
proxied.on("data", (chunk) => {
|
|
4183
|
+
bytes += chunk.length;
|
|
4184
|
+
if (bytes > MAX_MOBILE_BOOT_ENTRY_BYTES) {
|
|
4185
|
+
proxied.destroy();
|
|
4186
|
+
resolve(void 0);
|
|
4187
|
+
}
|
|
4188
|
+
});
|
|
4189
|
+
proxied.once("end", () => resolve(bytes));
|
|
4190
|
+
proxied.once("error", reject);
|
|
4191
|
+
});
|
|
4192
|
+
if (size === void 0) return void 0;
|
|
4193
|
+
this.bootEntrySizeCache.set(source, {
|
|
4194
|
+
size,
|
|
4195
|
+
at: Date.now()
|
|
4196
|
+
});
|
|
4197
|
+
return size;
|
|
4198
|
+
} catch {
|
|
4199
|
+
return;
|
|
4200
|
+
} finally {
|
|
4201
|
+
upstreamRequest?.destroy();
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
3944
4204
|
async serveMobileBootBatch(key, request, response, authorization) {
|
|
3945
4205
|
if (request.method !== "GET" && request.method !== "HEAD") throw new HttpError(405, "method_not_allowed");
|
|
3946
4206
|
const stored = this.mobileBootBatches.get(key);
|
|
@@ -4022,7 +4282,7 @@ var MobileAccessGateway = class {
|
|
|
4022
4282
|
}
|
|
4023
4283
|
};
|
|
4024
4284
|
await Promise.all(Array.from({ length: Math.min(3, plan.entries.length) }, worker));
|
|
4025
|
-
if (bodies.reduce((bytes, body) => bytes + body.byteLength +
|
|
4285
|
+
if (bodies.reduce((bytes, body) => bytes + body.byteLength + MOBILE_BOOT_SEPARATOR_BYTES, 0) > MAX_MOBILE_BOOT_BATCH_BYTES) throw new HttpError(502, "upstream_unavailable");
|
|
4026
4286
|
return Buffer.concat(bodies.flatMap((body) => [body, Buffer.from("\n;\n")]));
|
|
4027
4287
|
}
|
|
4028
4288
|
/**
|
|
@@ -4131,7 +4391,39 @@ var MobileAccessGateway = class {
|
|
|
4131
4391
|
}
|
|
4132
4392
|
};
|
|
4133
4393
|
}
|
|
4134
|
-
|
|
4394
|
+
/**
|
|
4395
|
+
* Proxy one request upstream. Pass-through client bundles (`GET /plugins`)
|
|
4396
|
+
* receive bounded transient retries — the upstream resets a fraction of fresh
|
|
4397
|
+
* connections — matching the resilience the merged-batch assembly already has.
|
|
4398
|
+
* A request is only retried before any byte reached the client.
|
|
4399
|
+
*/
|
|
4400
|
+
async proxyHttp(request, response, authorization) {
|
|
4401
|
+
if (!(request.method === "GET" && request.url?.split("?", 1)[0]?.startsWith("/plugins/") === true)) {
|
|
4402
|
+
try {
|
|
4403
|
+
await this.proxyHttpOnce(request, response, authorization);
|
|
4404
|
+
} catch (error) {
|
|
4405
|
+
if (error instanceof HttpError) throw error;
|
|
4406
|
+
if (response.headersSent) response.destroy();
|
|
4407
|
+
else throw new HttpError(502, "upstream_unavailable");
|
|
4408
|
+
}
|
|
4409
|
+
return;
|
|
4410
|
+
}
|
|
4411
|
+
const delay = (attempt) => new Promise((resolve) => setTimeout(resolve, MOBILE_BOOT_RETRY_DELAY_MS * attempt));
|
|
4412
|
+
for (let attempt = 1; attempt <= MOBILE_BOOT_UPSTREAM_ATTEMPTS; attempt++) try {
|
|
4413
|
+
await this.proxyHttpOnce(request, response, authorization);
|
|
4414
|
+
return;
|
|
4415
|
+
} catch (error) {
|
|
4416
|
+
if (response.headersSent) {
|
|
4417
|
+
response.destroy();
|
|
4418
|
+
return;
|
|
4419
|
+
}
|
|
4420
|
+
if (error instanceof HttpError || !isTransientUpstreamError(error)) throw error instanceof HttpError ? error : new HttpError(502, "upstream_unavailable");
|
|
4421
|
+
if (attempt === MOBILE_BOOT_UPSTREAM_ATTEMPTS) throw new HttpError(502, "upstream_unavailable");
|
|
4422
|
+
await delay(attempt);
|
|
4423
|
+
}
|
|
4424
|
+
throw new HttpError(502, "upstream_unavailable");
|
|
4425
|
+
}
|
|
4426
|
+
async proxyHttpOnce(request$2, response, authorization) {
|
|
4135
4427
|
const declared = request$2.headers["content-length"];
|
|
4136
4428
|
if (declared !== void 0 && (!/^\d+$/u.test(declared) || Number(declared) > this.config.maxBodyBytes)) throw new HttpError(413, "payload_too_large");
|
|
4137
4429
|
const holder = {};
|
|
@@ -4185,7 +4477,7 @@ var MobileAccessGateway = class {
|
|
|
4185
4477
|
await bodyDone?.catch(() => void 0);
|
|
4186
4478
|
if (error instanceof HttpError) throw error;
|
|
4187
4479
|
if (response.headersSent) response.destroy();
|
|
4188
|
-
|
|
4480
|
+
throw error;
|
|
4189
4481
|
} finally {
|
|
4190
4482
|
operation.release();
|
|
4191
4483
|
}
|
|
@@ -6486,10 +6778,23 @@ async function attemptDownload(request) {
|
|
|
6486
6778
|
});
|
|
6487
6779
|
const response = first.status >= 300 && first.status < 400 ? await followValidatedRedirect(first, request) : first;
|
|
6488
6780
|
if (!response.ok) throw new Error(`${request.errorPrefix}_download_http_${String(response.status)}`);
|
|
6489
|
-
const
|
|
6490
|
-
if (
|
|
6491
|
-
|
|
6492
|
-
|
|
6781
|
+
const contentLength = response.headers.get("content-length");
|
|
6782
|
+
if (contentLength !== null && (!/^\d+$/u.test(contentLength) || Number(contentLength) !== request.expectedBytes)) throw new Error(`${request.errorPrefix}_download_size_mismatch`);
|
|
6783
|
+
if (response.body === null) throw new Error(`${request.errorPrefix}_download_size_mismatch`);
|
|
6784
|
+
const bytes = new Uint8Array(request.expectedBytes);
|
|
6785
|
+
const reader = response.body.getReader();
|
|
6786
|
+
let received = 0;
|
|
6787
|
+
while (true) {
|
|
6788
|
+
const chunk = await reader.read();
|
|
6789
|
+
if (chunk.done) break;
|
|
6790
|
+
if (chunk.value.byteLength > bytes.byteLength - received) {
|
|
6791
|
+
await reader.cancel();
|
|
6792
|
+
throw new Error(`${request.errorPrefix}_download_size_mismatch`);
|
|
6793
|
+
}
|
|
6794
|
+
bytes.set(chunk.value, received);
|
|
6795
|
+
received += chunk.value.byteLength;
|
|
6796
|
+
}
|
|
6797
|
+
if (received !== request.expectedBytes) throw new Error(`${request.errorPrefix}_download_size_mismatch`);
|
|
6493
6798
|
return bytes;
|
|
6494
6799
|
}
|
|
6495
6800
|
/**
|
|
@@ -6512,24 +6817,45 @@ async function downloadPinnedArtifact(request) {
|
|
|
6512
6817
|
}
|
|
6513
6818
|
//#endregion
|
|
6514
6819
|
//#region src/cloudflared-component.ts
|
|
6820
|
+
const CLOUDFLARED_VERSION = "2026.9.1";
|
|
6821
|
+
/** Pinned official cloudflared release metadata for supported desktop targets. */
|
|
6822
|
+
const CLOUDFLARED_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
|
|
6823
|
+
{
|
|
6824
|
+
version: CLOUDFLARED_VERSION,
|
|
6825
|
+
platform: "win32",
|
|
6826
|
+
arch: "x64",
|
|
6827
|
+
downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-windows-amd64.exe`,
|
|
6828
|
+
downloadBytes: 54976432,
|
|
6829
|
+
downloadSha256: "2837888cc0f5d58f15b6dc478376de90b4d3ba5241c7947455d1e0a0df429712",
|
|
6830
|
+
executableName: "cloudflared.exe"
|
|
6831
|
+
},
|
|
6832
|
+
{
|
|
6833
|
+
version: CLOUDFLARED_VERSION,
|
|
6834
|
+
platform: "linux",
|
|
6835
|
+
arch: "x64",
|
|
6836
|
+
downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64`,
|
|
6837
|
+
downloadBytes: 39838488,
|
|
6838
|
+
downloadSha256: "03f1f25d1cc93b9ad6c60569d44060bc4f17ed97075760ed8cfca4b12dcd68cc",
|
|
6839
|
+
executableName: "cloudflared"
|
|
6840
|
+
},
|
|
6841
|
+
{
|
|
6842
|
+
version: CLOUDFLARED_VERSION,
|
|
6843
|
+
platform: "linux",
|
|
6844
|
+
arch: "arm64",
|
|
6845
|
+
downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64`,
|
|
6846
|
+
downloadBytes: 37466252,
|
|
6847
|
+
downloadSha256: "3d97437c71848bd8df68041e12436b484a661d95073ea1937f01a845ce88faa3",
|
|
6848
|
+
executableName: "cloudflared"
|
|
6849
|
+
}
|
|
6850
|
+
].map((release) => [`${release.platform}-${release.arch}`, Object.freeze(release)])));
|
|
6515
6851
|
/**
|
|
6516
|
-
*
|
|
6517
|
-
*
|
|
6518
|
-
*
|
|
6519
|
-
* unpack, so the download digest and the installed digest are the same pair.
|
|
6520
|
-
* `--no-autoupdate` is passed at runtime as well, so the pinned bytes stay the
|
|
6521
|
-
* bytes that were verified.
|
|
6852
|
+
* Canonical release metadata. New code should select from
|
|
6853
|
+
* {@link CLOUDFLARED_COMPONENT_RELEASES} by platform and architecture; this
|
|
6854
|
+
* alias preserves the original Windows x64 entry for existing callers.
|
|
6522
6855
|
*/
|
|
6523
|
-
const CLOUDFLARED_COMPONENT_RELEASE =
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
arch: "x64",
|
|
6527
|
-
downloadUrl: "https://github.com/cloudflare/cloudflared/releases/download/2026.9.1/cloudflared-windows-amd64.exe",
|
|
6528
|
-
downloadBytes: 54976432,
|
|
6529
|
-
downloadSha256: "2837888cc0f5d58f15b6dc478376de90b4d3ba5241c7947455d1e0a0df429712",
|
|
6530
|
-
downloadPage: "https://github.com/cloudflare/cloudflared/releases",
|
|
6531
|
-
termsUrl: "https://www.cloudflare.com/website-terms/"
|
|
6532
|
-
});
|
|
6856
|
+
const CLOUDFLARED_COMPONENT_RELEASE = CLOUDFLARED_COMPONENT_RELEASES["win32-x64"];
|
|
6857
|
+
const DOWNLOAD_PAGE$1 = "https://github.com/cloudflare/cloudflared/releases";
|
|
6858
|
+
const TERMS_URL$1 = "https://www.cloudflare.com/website-terms/";
|
|
6533
6859
|
function inside$1(parent, child) {
|
|
6534
6860
|
const candidate = relative(parent, child);
|
|
6535
6861
|
return candidate !== "" && !candidate.startsWith("..") && !isAbsolute(candidate);
|
|
@@ -6546,14 +6872,18 @@ async function regularFile$1(file, expectedBytes) {
|
|
|
6546
6872
|
throw error;
|
|
6547
6873
|
}
|
|
6548
6874
|
}
|
|
6549
|
-
async function defaultFetchArtifact$1(url, signal) {
|
|
6875
|
+
async function defaultFetchArtifact$1(url, signal, expectedBytes) {
|
|
6550
6876
|
return downloadPinnedArtifact({
|
|
6551
6877
|
url,
|
|
6552
|
-
expectedBytes
|
|
6878
|
+
expectedBytes,
|
|
6553
6879
|
errorPrefix: "cloudflared",
|
|
6554
6880
|
signal
|
|
6555
6881
|
});
|
|
6556
6882
|
}
|
|
6883
|
+
/** Select the pinned artifact for one host, or undefined where unsupported. */
|
|
6884
|
+
function lookupRelease(platform, arch) {
|
|
6885
|
+
return CLOUDFLARED_COMPONENT_RELEASES[`${platform}-${arch}`];
|
|
6886
|
+
}
|
|
6557
6887
|
/** Owns the optional cloudflared binary inside DSH Mobile state. */
|
|
6558
6888
|
var CloudflaredComponentManager = class {
|
|
6559
6889
|
executable;
|
|
@@ -6564,6 +6894,7 @@ var CloudflaredComponentManager = class {
|
|
|
6564
6894
|
stagingRoot;
|
|
6565
6895
|
platform;
|
|
6566
6896
|
arch;
|
|
6897
|
+
release;
|
|
6567
6898
|
fetchArtifact;
|
|
6568
6899
|
installed = false;
|
|
6569
6900
|
errorCode;
|
|
@@ -6573,9 +6904,10 @@ var CloudflaredComponentManager = class {
|
|
|
6573
6904
|
if (!isAbsolute(stateDirectory)) throw new Error("cloudflared state directory must be absolute");
|
|
6574
6905
|
this.platform = options.platform ?? process.platform;
|
|
6575
6906
|
this.arch = options.arch ?? process.arch;
|
|
6907
|
+
this.release = lookupRelease(this.platform, this.arch);
|
|
6576
6908
|
this.componentRoot = join(stateDirectory, "components", "cloudflared");
|
|
6577
|
-
this.componentStorage = join(this.componentRoot,
|
|
6578
|
-
this.executable = join(this.componentStorage, "cloudflared
|
|
6909
|
+
this.componentStorage = join(this.componentRoot, this.release?.version ?? CLOUDFLARED_VERSION);
|
|
6910
|
+
this.executable = join(this.componentStorage, this.release?.executableName ?? "cloudflared");
|
|
6579
6911
|
this.stateRoot = join(stateDirectory, "state", "cloudflared");
|
|
6580
6912
|
this.logRoot = join(stateDirectory, "logs", "cloudflared");
|
|
6581
6913
|
this.stagingRoot = join(stateDirectory, "staging", "cloudflared");
|
|
@@ -6586,27 +6918,30 @@ var CloudflaredComponentManager = class {
|
|
|
6586
6918
|
this.logRoot,
|
|
6587
6919
|
this.stagingRoot
|
|
6588
6920
|
]) if (!inside$1(stateDirectory, child)) throw new Error("cloudflared component path escaped its state directory");
|
|
6589
|
-
|
|
6921
|
+
const release = this.release;
|
|
6922
|
+
this.fetchArtifact = options.fetchArtifact ?? ((url, signal) => defaultFetchArtifact$1(url, signal, release?.downloadBytes ?? 0));
|
|
6590
6923
|
}
|
|
6591
6924
|
/** Inspect the managed binary without using any global cloudflared state. */
|
|
6592
6925
|
async initialize() {
|
|
6593
|
-
|
|
6594
|
-
|
|
6926
|
+
const release = this.release;
|
|
6927
|
+
this.installed = release !== void 0 && await regularFile$1(this.executable, release.downloadBytes);
|
|
6928
|
+
if (this.installed && release !== void 0 && await sha256$1(this.executable) !== release.downloadSha256) {
|
|
6595
6929
|
this.installed = false;
|
|
6596
6930
|
this.errorCode = "cloudflared_component_invalid";
|
|
6597
6931
|
}
|
|
6598
6932
|
}
|
|
6599
6933
|
/** Return a safe status that never includes machine-specific account data. */
|
|
6600
6934
|
status() {
|
|
6935
|
+
const release = this.release ?? CLOUDFLARED_COMPONENT_RELEASE;
|
|
6601
6936
|
return Object.freeze({
|
|
6602
|
-
supported: this.
|
|
6937
|
+
supported: this.release !== void 0,
|
|
6603
6938
|
installed: this.installed,
|
|
6604
|
-
version:
|
|
6605
|
-
downloadBytes:
|
|
6606
|
-
installedBytes:
|
|
6607
|
-
sourceUrl:
|
|
6608
|
-
downloadPage:
|
|
6609
|
-
termsUrl:
|
|
6939
|
+
version: release.version,
|
|
6940
|
+
downloadBytes: release.downloadBytes,
|
|
6941
|
+
installedBytes: release.downloadBytes,
|
|
6942
|
+
sourceUrl: release.downloadUrl,
|
|
6943
|
+
downloadPage: DOWNLOAD_PAGE$1,
|
|
6944
|
+
termsUrl: TERMS_URL$1,
|
|
6610
6945
|
storagePath: this.componentRoot,
|
|
6611
6946
|
...this.errorCode === void 0 ? {} : { errorCode: this.errorCode }
|
|
6612
6947
|
});
|
|
@@ -6614,7 +6949,8 @@ var CloudflaredComponentManager = class {
|
|
|
6614
6949
|
/** Download, verify, and install the pinned cloudflared executable after explicit confirmation. */
|
|
6615
6950
|
install() {
|
|
6616
6951
|
return this.enqueue(async () => {
|
|
6617
|
-
|
|
6952
|
+
const release = this.release;
|
|
6953
|
+
if (release === void 0) throw new Error("cloudflared_component_unsupported");
|
|
6618
6954
|
await mkdir(this.stagingRoot, {
|
|
6619
6955
|
recursive: true,
|
|
6620
6956
|
mode: 448
|
|
@@ -6628,26 +6964,26 @@ var CloudflaredComponentManager = class {
|
|
|
6628
6964
|
timeout.unref();
|
|
6629
6965
|
let bytes;
|
|
6630
6966
|
try {
|
|
6631
|
-
bytes = await this.fetchArtifact(
|
|
6967
|
+
bytes = await this.fetchArtifact(release.downloadUrl, controller.signal);
|
|
6632
6968
|
} finally {
|
|
6633
6969
|
clearTimeout(timeout);
|
|
6634
6970
|
}
|
|
6635
|
-
if (bytes.byteLength !==
|
|
6636
|
-
if (createHash("sha256").update(bytes).digest("hex") !==
|
|
6637
|
-
const staged = join(staging,
|
|
6971
|
+
if (bytes.byteLength !== release.downloadBytes) throw new Error("cloudflared_download_size_mismatch");
|
|
6972
|
+
if (createHash("sha256").update(bytes).digest("hex") !== release.downloadSha256) throw new Error("cloudflared_download_hash_mismatch");
|
|
6973
|
+
const staged = join(staging, release.executableName);
|
|
6638
6974
|
await writeFile(staged, bytes, {
|
|
6639
6975
|
flag: "wx",
|
|
6640
6976
|
mode: 384
|
|
6641
6977
|
});
|
|
6642
6978
|
await chmod(staged, 448);
|
|
6643
|
-
if (!await regularFile$1(staged,
|
|
6979
|
+
if (!await regularFile$1(staged, release.downloadBytes) || await sha256$1(staged) !== release.downloadSha256) throw new Error("cloudflared_executable_hash_mismatch");
|
|
6644
6980
|
const candidate = join(this.componentRoot, `.install-${randomBytes(12).toString("hex")}`);
|
|
6645
6981
|
await mkdir(candidate, {
|
|
6646
6982
|
recursive: true,
|
|
6647
6983
|
mode: 448
|
|
6648
6984
|
});
|
|
6649
|
-
await copyFile(staged, join(candidate,
|
|
6650
|
-
await chmod(join(candidate,
|
|
6985
|
+
await copyFile(staged, join(candidate, release.executableName));
|
|
6986
|
+
await chmod(join(candidate, release.executableName), 448);
|
|
6651
6987
|
await rm(this.componentStorage, {
|
|
6652
6988
|
recursive: true,
|
|
6653
6989
|
force: true
|
|
@@ -6704,6 +7040,15 @@ const MAX_LOG_BUFFER_BYTES$1 = 65536;
|
|
|
6704
7040
|
* looking healthy for longer than a user will wait.
|
|
6705
7041
|
*/
|
|
6706
7042
|
const START_TIMEOUT_MS$1 = 6e4;
|
|
7043
|
+
/**
|
|
7044
|
+
* How many startup-timeout rounds a named tunnel may wait through while its
|
|
7045
|
+
* connector process is still alive. A rebooted PC often autostarts DSH before
|
|
7046
|
+
* Wi-Fi / VPN / DNS is usable; cloudflared retries the edge on its own, so
|
|
7047
|
+
* killing it after a single 60 s window turns a slow boot into a manual
|
|
7048
|
+
* reconnect loop. An exited connector (bad token, bad config) still fails fast
|
|
7049
|
+
* on the first round.
|
|
7050
|
+
*/
|
|
7051
|
+
const NAMED_STARTUP_ROUNDS = 5;
|
|
6707
7052
|
const CLOUDFLARED_HOST_SUFFIX = ".trycloudflare.com";
|
|
6708
7053
|
/**
|
|
6709
7054
|
* Subdomains Cloudflare reserves for its own control plane.
|
|
@@ -6846,6 +7191,7 @@ var CloudflaredController = class {
|
|
|
6846
7191
|
});
|
|
6847
7192
|
queue = Promise.resolve();
|
|
6848
7193
|
startupTimer;
|
|
7194
|
+
startupRounds = 0;
|
|
6849
7195
|
constructor(options) {
|
|
6850
7196
|
this.options = options;
|
|
6851
7197
|
if (!isAbsolute(options.executable)) throw new Error("cloudflared executable path must be absolute");
|
|
@@ -6977,6 +7323,7 @@ var CloudflaredController = class {
|
|
|
6977
7323
|
}
|
|
6978
7324
|
this.reservation = reservation;
|
|
6979
7325
|
this.buffer = "";
|
|
7326
|
+
this.startupRounds = 0;
|
|
6980
7327
|
this.publish({
|
|
6981
7328
|
enabled: true,
|
|
6982
7329
|
state: "starting"
|
|
@@ -7038,11 +7385,32 @@ var CloudflaredController = class {
|
|
|
7038
7385
|
this.child = void 0;
|
|
7039
7386
|
if (this.enabled) this.enqueue(() => this.failGeneration(generation, code === 0 ? "cloudflared_stopped" : "cloudflared_exited"));
|
|
7040
7387
|
});
|
|
7388
|
+
this.armStartupTimer(generation);
|
|
7389
|
+
}
|
|
7390
|
+
armStartupTimer(generation) {
|
|
7391
|
+
if (this.startupTimer !== void 0) clearTimeout(this.startupTimer);
|
|
7041
7392
|
this.startupTimer = setTimeout(() => {
|
|
7042
|
-
this.enqueue(() => this.
|
|
7393
|
+
this.enqueue(() => this.checkStartupTimeout(generation));
|
|
7043
7394
|
}, this.options.startupTimeoutMs ?? START_TIMEOUT_MS$1);
|
|
7044
7395
|
this.startupTimer.unref();
|
|
7045
7396
|
}
|
|
7397
|
+
/**
|
|
7398
|
+
* Give a live named connector more time instead of killing it: it retries the
|
|
7399
|
+
* edge on its own and usually registers once the rebooted network is usable.
|
|
7400
|
+
* A dead connector, or an exhausted budget, fails the generation as before.
|
|
7401
|
+
*/
|
|
7402
|
+
async checkStartupTimeout(generation) {
|
|
7403
|
+
if (generation !== this.generation || !this.enabled || this.disposed) return;
|
|
7404
|
+
if (this.latest.state === "ready") return;
|
|
7405
|
+
const childAlive = this.child !== void 0 && this.child.exitCode === null;
|
|
7406
|
+
const budget = this.options.maxStartupRounds ?? NAMED_STARTUP_ROUNDS;
|
|
7407
|
+
if (this.mode === "named" && childAlive && this.startupRounds + 1 < budget) {
|
|
7408
|
+
this.startupRounds += 1;
|
|
7409
|
+
this.armStartupTimer(generation);
|
|
7410
|
+
return;
|
|
7411
|
+
}
|
|
7412
|
+
await this.failGeneration(generation, "cloudflared_start_timeout");
|
|
7413
|
+
}
|
|
7046
7414
|
consume(generation, chunk) {
|
|
7047
7415
|
if (generation !== this.generation) return;
|
|
7048
7416
|
this.buffer += chunk;
|
|
@@ -9526,21 +9894,56 @@ var CpolarController = class {
|
|
|
9526
9894
|
};
|
|
9527
9895
|
//#endregion
|
|
9528
9896
|
//#region src/cpolar-component.ts
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
}
|
|
9897
|
+
const CPOLAR_VERSION = "3.3.18";
|
|
9898
|
+
/** Pinned official cpolar release metadata for supported desktop targets. */
|
|
9899
|
+
const CPOLAR_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
|
|
9900
|
+
{
|
|
9901
|
+
version: CPOLAR_VERSION,
|
|
9902
|
+
platform: "win32",
|
|
9903
|
+
arch: "x64",
|
|
9904
|
+
downloadUrl: `https://www.cpolar.com/static/downloads/releases/${CPOLAR_VERSION}/cpolar-stable-windows-amd64-setup.zip`,
|
|
9905
|
+
downloadBytes: 7603505,
|
|
9906
|
+
downloadSha256: "fb8cf60289058ee26079f995d2eeea0b21768a742d90c93015afe96e83428830",
|
|
9907
|
+
executableName: "cpolar.exe",
|
|
9908
|
+
executableBytes: 19637680,
|
|
9909
|
+
executableSha256: "b2d865ee505e842d22ceca5493a872efa893a79b079a7a8ee2bd3aa5343a5c41",
|
|
9910
|
+
archiveKind: "msi-zip"
|
|
9911
|
+
},
|
|
9912
|
+
{
|
|
9913
|
+
version: CPOLAR_VERSION,
|
|
9914
|
+
platform: "linux",
|
|
9915
|
+
arch: "x64",
|
|
9916
|
+
downloadUrl: `https://www.cpolar.com/static/downloads/releases/${CPOLAR_VERSION}/cpolar-stable-linux-amd64.tar.gz`,
|
|
9917
|
+
downloadBytes: 7404781,
|
|
9918
|
+
downloadSha256: "5cd3320c4369928ccb509c4f5fa2ec3b86151ead77e1b77c1694aa64c43e32e7",
|
|
9919
|
+
executableName: "cpolar",
|
|
9920
|
+
executableBytes: 19328632,
|
|
9921
|
+
executableSha256: "c076e1109372a3f88031841c5989030e49b6871b54aee3260c796da9122dec05",
|
|
9922
|
+
archiveKind: "tar.gz"
|
|
9923
|
+
},
|
|
9924
|
+
{
|
|
9925
|
+
version: CPOLAR_VERSION,
|
|
9926
|
+
platform: "linux",
|
|
9927
|
+
arch: "arm64",
|
|
9928
|
+
downloadUrl: `https://www.cpolar.com/static/downloads/releases/${CPOLAR_VERSION}/cpolar-stable-linux-arm64.tar.gz`,
|
|
9929
|
+
downloadBytes: 6855666,
|
|
9930
|
+
downloadSha256: "8a61a97983f18ae5ffb4b8bac4c9b3d8e2399a6ee101844e7b0b30d7f326157c",
|
|
9931
|
+
executableName: "cpolar",
|
|
9932
|
+
executableBytes: 19017169,
|
|
9933
|
+
executableSha256: "8d76a1b7e518df45f387f107c7a428079c67ecd9b971a2915c53b947ea9b443a",
|
|
9934
|
+
archiveKind: "tar.gz"
|
|
9935
|
+
}
|
|
9936
|
+
].map((release) => [`${release.platform}-${release.arch}`, Object.freeze(release)])));
|
|
9937
|
+
/**
|
|
9938
|
+
* Canonical release metadata. New code should select from
|
|
9939
|
+
* {@link CPOLAR_COMPONENT_RELEASES} by platform and architecture; this alias
|
|
9940
|
+
* preserves the original Windows x64 entry for existing callers.
|
|
9941
|
+
*/
|
|
9942
|
+
const CPOLAR_COMPONENT_RELEASE = CPOLAR_COMPONENT_RELEASES["win32-x64"];
|
|
9943
|
+
const DOWNLOAD_PAGE = "https://www.cpolar.com/download";
|
|
9944
|
+
const SIGNUP_URL = "https://dashboard.cpolar.com/signup";
|
|
9945
|
+
const DASHBOARD_URL = "https://dashboard.cpolar.com/auth";
|
|
9946
|
+
const TERMS_URL = "https://www.cpolar.com/tos";
|
|
9544
9947
|
function inside(parent, child) {
|
|
9545
9948
|
const candidate = relative(parent, child);
|
|
9546
9949
|
return candidate !== "" && !candidate.startsWith("..") && !isAbsolute(candidate);
|
|
@@ -9568,15 +9971,15 @@ async function run(file, args) {
|
|
|
9568
9971
|
});
|
|
9569
9972
|
});
|
|
9570
9973
|
}
|
|
9571
|
-
async function defaultFetchArtifact(url, signal) {
|
|
9974
|
+
async function defaultFetchArtifact(url, signal, expectedBytes) {
|
|
9572
9975
|
return downloadPinnedArtifact({
|
|
9573
9976
|
url,
|
|
9574
|
-
expectedBytes
|
|
9977
|
+
expectedBytes,
|
|
9575
9978
|
errorPrefix: "cpolar",
|
|
9576
9979
|
signal
|
|
9577
9980
|
});
|
|
9578
9981
|
}
|
|
9579
|
-
async function
|
|
9982
|
+
async function extractWindowsMsi(archive, destination, executableName) {
|
|
9580
9983
|
if (process.platform !== "win32") throw new Error("cpolar_component_unsupported");
|
|
9581
9984
|
const unpacked = join(destination, "archive");
|
|
9582
9985
|
const administrative = join(destination, "administrative");
|
|
@@ -9602,9 +10005,29 @@ async function defaultExtractArtifact(archive, destination) {
|
|
|
9602
10005
|
"/qn",
|
|
9603
10006
|
`TARGETDIR=${administrative}`
|
|
9604
10007
|
]);
|
|
9605
|
-
const executableRelative = (await readdir(administrative, { recursive: true })).find((entry) => basename(entry).toLowerCase() ===
|
|
10008
|
+
const executableRelative = (await readdir(administrative, { recursive: true })).find((entry) => basename(entry).toLowerCase() === executableName.toLowerCase());
|
|
10009
|
+
if (executableRelative === void 0) throw new Error("cpolar_executable_missing");
|
|
10010
|
+
await copyFile(join(administrative, executableRelative), join(destination, executableName));
|
|
10011
|
+
}
|
|
10012
|
+
async function extractLinuxTarball(archive, destination, executableName) {
|
|
10013
|
+
const unpacked = join(destination, "archive");
|
|
10014
|
+
await mkdir(unpacked, {
|
|
10015
|
+
recursive: true,
|
|
10016
|
+
mode: 448
|
|
10017
|
+
});
|
|
10018
|
+
await run(process.platform === "win32" ? "tar.exe" : "tar", [
|
|
10019
|
+
"-xzf",
|
|
10020
|
+
archive,
|
|
10021
|
+
"-C",
|
|
10022
|
+
unpacked
|
|
10023
|
+
]);
|
|
10024
|
+
const executableRelative = (await readdir(unpacked, { recursive: true })).find((entry) => basename(entry).toLowerCase() === executableName.toLowerCase());
|
|
9606
10025
|
if (executableRelative === void 0) throw new Error("cpolar_executable_missing");
|
|
9607
|
-
await copyFile(join(
|
|
10026
|
+
await copyFile(join(unpacked, executableRelative), join(destination, executableName));
|
|
10027
|
+
}
|
|
10028
|
+
async function defaultExtractArtifact(archive, destination, release) {
|
|
10029
|
+
if (release.archiveKind === "tar.gz") return extractLinuxTarball(archive, destination, release.executableName);
|
|
10030
|
+
return extractWindowsMsi(archive, destination, release.executableName);
|
|
9608
10031
|
}
|
|
9609
10032
|
/** Validate a cpolar Authtoken before it crosses the durable-file boundary. */
|
|
9610
10033
|
function validateCpolarAuthtoken(value) {
|
|
@@ -9622,6 +10045,7 @@ var CpolarComponentManager = class {
|
|
|
9622
10045
|
stagingRoot;
|
|
9623
10046
|
platform;
|
|
9624
10047
|
arch;
|
|
10048
|
+
release;
|
|
9625
10049
|
fetchArtifact;
|
|
9626
10050
|
extractArtifact;
|
|
9627
10051
|
installed = false;
|
|
@@ -9633,9 +10057,10 @@ var CpolarComponentManager = class {
|
|
|
9633
10057
|
if (!isAbsolute(stateDirectory)) throw new Error("cpolar state directory must be absolute");
|
|
9634
10058
|
this.platform = options.platform ?? process.platform;
|
|
9635
10059
|
this.arch = options.arch ?? process.arch;
|
|
10060
|
+
this.release = CPOLAR_COMPONENT_RELEASES[`${this.platform}-${this.arch}`];
|
|
9636
10061
|
this.componentRoot = join(stateDirectory, "components", "cpolar");
|
|
9637
|
-
this.componentStorage = join(this.componentRoot,
|
|
9638
|
-
this.executable = join(this.componentStorage, "cpolar
|
|
10062
|
+
this.componentStorage = join(this.componentRoot, this.release?.version ?? CPOLAR_VERSION);
|
|
10063
|
+
this.executable = join(this.componentStorage, this.release?.executableName ?? "cpolar");
|
|
9639
10064
|
this.stateRoot = join(stateDirectory, "state", "cpolar");
|
|
9640
10065
|
this.configFile = join(this.stateRoot, "cpolar.yml");
|
|
9641
10066
|
this.logRoot = join(stateDirectory, "logs", "cpolar");
|
|
@@ -9647,13 +10072,18 @@ var CpolarComponentManager = class {
|
|
|
9647
10072
|
this.logRoot,
|
|
9648
10073
|
this.stagingRoot
|
|
9649
10074
|
]) if (!inside(stateDirectory, child)) throw new Error("cpolar component path escaped its state directory");
|
|
9650
|
-
|
|
9651
|
-
this.
|
|
10075
|
+
const release = this.release;
|
|
10076
|
+
this.fetchArtifact = options.fetchArtifact ?? ((url, signal) => defaultFetchArtifact(url, signal, release?.downloadBytes ?? 0));
|
|
10077
|
+
this.extractArtifact = options.extractArtifact ?? ((archive, destination) => {
|
|
10078
|
+
if (release === void 0) throw new Error("cpolar_component_unsupported");
|
|
10079
|
+
return defaultExtractArtifact(archive, destination, release);
|
|
10080
|
+
});
|
|
9652
10081
|
}
|
|
9653
10082
|
/** Inspect the managed binary and configuration without using global cpolar state. */
|
|
9654
10083
|
async initialize() {
|
|
9655
|
-
|
|
9656
|
-
|
|
10084
|
+
const release = this.release;
|
|
10085
|
+
this.installed = release !== void 0 && await regularFile(this.executable, release.executableBytes);
|
|
10086
|
+
if (this.installed && release !== void 0 && await sha256(this.executable) !== release.executableSha256) {
|
|
9657
10087
|
this.installed = false;
|
|
9658
10088
|
this.errorCode = "cpolar_component_invalid";
|
|
9659
10089
|
}
|
|
@@ -9662,18 +10092,19 @@ var CpolarComponentManager = class {
|
|
|
9662
10092
|
}
|
|
9663
10093
|
/** Return a safe status that never includes the account token. */
|
|
9664
10094
|
status() {
|
|
10095
|
+
const release = this.release ?? CPOLAR_COMPONENT_RELEASE;
|
|
9665
10096
|
return Object.freeze({
|
|
9666
|
-
supported: this.
|
|
10097
|
+
supported: this.release !== void 0,
|
|
9667
10098
|
installed: this.installed,
|
|
9668
10099
|
configured: this.configured,
|
|
9669
|
-
version:
|
|
9670
|
-
downloadBytes:
|
|
9671
|
-
installedBytes:
|
|
9672
|
-
sourceUrl:
|
|
9673
|
-
downloadPage:
|
|
9674
|
-
signupUrl:
|
|
9675
|
-
dashboardUrl:
|
|
9676
|
-
termsUrl:
|
|
10100
|
+
version: release.version,
|
|
10101
|
+
downloadBytes: release.downloadBytes,
|
|
10102
|
+
installedBytes: release.executableBytes,
|
|
10103
|
+
sourceUrl: release.downloadUrl,
|
|
10104
|
+
downloadPage: DOWNLOAD_PAGE,
|
|
10105
|
+
signupUrl: SIGNUP_URL,
|
|
10106
|
+
dashboardUrl: DASHBOARD_URL,
|
|
10107
|
+
termsUrl: TERMS_URL,
|
|
9677
10108
|
storagePath: this.componentRoot,
|
|
9678
10109
|
...this.errorCode === void 0 ? {} : { errorCode: this.errorCode }
|
|
9679
10110
|
});
|
|
@@ -9681,7 +10112,8 @@ var CpolarComponentManager = class {
|
|
|
9681
10112
|
/** Download, verify, and administratively extract cpolar after explicit confirmation. */
|
|
9682
10113
|
install() {
|
|
9683
10114
|
return this.enqueue(async () => {
|
|
9684
|
-
|
|
10115
|
+
const release = this.release;
|
|
10116
|
+
if (release === void 0) throw new Error("cpolar_component_unsupported");
|
|
9685
10117
|
await mkdir(this.stagingRoot, {
|
|
9686
10118
|
recursive: true,
|
|
9687
10119
|
mode: 448
|
|
@@ -9695,26 +10127,26 @@ var CpolarComponentManager = class {
|
|
|
9695
10127
|
timeout.unref();
|
|
9696
10128
|
let bytes;
|
|
9697
10129
|
try {
|
|
9698
|
-
bytes = await this.fetchArtifact(
|
|
10130
|
+
bytes = await this.fetchArtifact(release.downloadUrl, controller.signal);
|
|
9699
10131
|
} finally {
|
|
9700
10132
|
clearTimeout(timeout);
|
|
9701
10133
|
}
|
|
9702
|
-
if (createHash("sha256").update(bytes).digest("hex") !==
|
|
9703
|
-
const archive = join(staging, "cpolar.zip");
|
|
10134
|
+
if (createHash("sha256").update(bytes).digest("hex") !== release.downloadSha256) throw new Error("cpolar_download_hash_mismatch");
|
|
10135
|
+
const archive = join(staging, release.archiveKind === "tar.gz" ? "cpolar.tar.gz" : "cpolar.zip");
|
|
9704
10136
|
await writeFile(archive, bytes, {
|
|
9705
10137
|
flag: "wx",
|
|
9706
10138
|
mode: 384
|
|
9707
10139
|
});
|
|
9708
10140
|
await this.extractArtifact(archive, staging);
|
|
9709
|
-
const extracted = join(staging,
|
|
9710
|
-
if (!await regularFile(extracted,
|
|
10141
|
+
const extracted = join(staging, release.executableName);
|
|
10142
|
+
if (!await regularFile(extracted, release.executableBytes) || await sha256(extracted) !== release.executableSha256) throw new Error("cpolar_executable_hash_mismatch");
|
|
9711
10143
|
const candidate = join(this.componentRoot, `.install-${randomBytes(12).toString("hex")}`);
|
|
9712
10144
|
await mkdir(candidate, {
|
|
9713
10145
|
recursive: true,
|
|
9714
10146
|
mode: 448
|
|
9715
10147
|
});
|
|
9716
|
-
await copyFile(extracted, join(candidate,
|
|
9717
|
-
await chmod(join(candidate,
|
|
10148
|
+
await copyFile(extracted, join(candidate, release.executableName));
|
|
10149
|
+
await chmod(join(candidate, release.executableName), 448);
|
|
9718
10150
|
await rm(this.componentStorage, {
|
|
9719
10151
|
recursive: true,
|
|
9720
10152
|
force: true
|
|
@@ -10404,6 +10836,16 @@ function selectLanNetwork(requestedAddress, requestedInterface, table, preferred
|
|
|
10404
10836
|
if (physicalCandidates.length === 1) return physicalCandidates[0];
|
|
10405
10837
|
throw new Error(`more than one LAN address is active; rerun with --address and one of: ${candidates.map((entry) => `${entry.name}=${entry.address}`).join(", ")}`);
|
|
10406
10838
|
}
|
|
10839
|
+
/**
|
|
10840
|
+
* True when the failure came from LAN selection (the configured network is
|
|
10841
|
+
* gone, nothing is up, or several are) rather than from config/TLS code.
|
|
10842
|
+
* Callers use it to degrade (warn + stay down) instead of failing boot.
|
|
10843
|
+
* Pinned to the selectLanNetwork message contract — keep in sync.
|
|
10844
|
+
*/
|
|
10845
|
+
function isNetworkSelectionError(error) {
|
|
10846
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
10847
|
+
return message.includes("is not connected") || message.includes("no active private LAN address was found") || message.includes("more than one LAN address is active") || message.includes("has more than one private IPv4 address") || message.includes("is not an active private LAN address");
|
|
10848
|
+
}
|
|
10407
10849
|
function assertMatchingCa(certPem, keyPem) {
|
|
10408
10850
|
const certificate = new X509Certificate(certPem);
|
|
10409
10851
|
if (!certificate.ca || certificate.subject !== certificate.issuer || !certificate.verify(certificate.publicKey)) throw new Error("managed TLS CA must be a self-signed CA certificate");
|
|
@@ -10940,7 +11382,9 @@ async function apply(ctx, config) {
|
|
|
10940
11382
|
...candidateConfig,
|
|
10941
11383
|
upstreamOrigin: template.upstreamOrigin.origin
|
|
10942
11384
|
});
|
|
10943
|
-
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths)
|
|
11385
|
+
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths, (source, code) => {
|
|
11386
|
+
logger.warn("%s discovery degraded while DSH remains available: %s", source, code);
|
|
11387
|
+
});
|
|
10944
11388
|
await candidate.start();
|
|
10945
11389
|
lanGateway = candidate;
|
|
10946
11390
|
const removeTaskSink = taskEventHub.add(candidate);
|
|
@@ -10965,7 +11409,14 @@ async function apply(ctx, config) {
|
|
|
10965
11409
|
}, (error) => {
|
|
10966
11410
|
process.emitWarning(`DSH Mobile could not follow the current LAN address: ${error instanceof Error ? error.message : String(error)}`, { code: "DSH_MOBILE_NETWORK_REFRESH" });
|
|
10967
11411
|
});
|
|
10968
|
-
|
|
11412
|
+
try {
|
|
11413
|
+
await following.initialize(2e3);
|
|
11414
|
+
} catch (error) {
|
|
11415
|
+
if (!isNetworkSelectionError(error)) throw error;
|
|
11416
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
11417
|
+
process.emitWarning(`DSH Mobile: ${detail}; mobile access stays dormant until the network returns or setup is re-run — DSH itself keeps running.`, { code: "DSH_MOBILE_NETWORK_UNAVAILABLE" });
|
|
11418
|
+
following.beginPolling(2e3);
|
|
11419
|
+
}
|
|
10969
11420
|
return following;
|
|
10970
11421
|
};
|
|
10971
11422
|
const lanControlFile = parseControlFile(config.controlFile);
|