dsh-mobile 0.4.2 → 0.4.3
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 +11 -3
- package/README.en.md +44 -20
- package/README.md +42 -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 +56 -14
- package/lib/index.mjs +504 -125
- package/lib/index.mjs.map +1 -1
- package/package.json +9 -7
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. */
|
|
@@ -3004,6 +3111,8 @@ var MobileAccessGateway = class {
|
|
|
3004
3111
|
activeRequests = /* @__PURE__ */ new Map();
|
|
3005
3112
|
activeWebSockets = /* @__PURE__ */ new Map();
|
|
3006
3113
|
mobileBootBatches = /* @__PURE__ */ new Map();
|
|
3114
|
+
/** Cached upstream bundle byte sizes (HEAD probes), keyed by resource URL. */
|
|
3115
|
+
bootEntrySizeCache = /* @__PURE__ */ new Map();
|
|
3007
3116
|
extensionEventListeners = /* @__PURE__ */ new Set();
|
|
3008
3117
|
extensionEventRevision = 0;
|
|
3009
3118
|
taskEventListeners = /* @__PURE__ */ new Set();
|
|
@@ -3904,8 +4013,10 @@ var MobileAccessGateway = class {
|
|
|
3904
4013
|
}
|
|
3905
4014
|
let body;
|
|
3906
4015
|
try {
|
|
3907
|
-
const
|
|
3908
|
-
|
|
4016
|
+
const html = Buffer.concat(chunks).toString("utf8");
|
|
4017
|
+
const plan = parseMobileBootPlan(html);
|
|
4018
|
+
const rewritten = rewriteMobileIndexWithBatch(html, plan.planEntries === void 0 ? {} : await this.resolveMobileBootSizes(plan.planEntries));
|
|
4019
|
+
for (const batch of rewritten.batches) this.rememberMobileBootBatch(batch);
|
|
3909
4020
|
body = Buffer.from(rewritten.html);
|
|
3910
4021
|
} catch {
|
|
3911
4022
|
throw new HttpError(502, "upstream_unavailable");
|
|
@@ -3941,6 +4052,98 @@ var MobileAccessGateway = class {
|
|
|
3941
4052
|
this.mobileBootBatches.delete(oldest);
|
|
3942
4053
|
}
|
|
3943
4054
|
}
|
|
4055
|
+
/**
|
|
4056
|
+
* Determine which layout-batch entries can share a merged boot batch and which
|
|
4057
|
+
* must pass through. An entry at or above the per-entry cap, or one whose size
|
|
4058
|
+
* could not be probed, keeps its own upstream `/plugins` row: a merged batch
|
|
4059
|
+
* cannot carry it, and an unknown bundle must not risk the whole batch.
|
|
4060
|
+
*/
|
|
4061
|
+
async resolveMobileBootSizes(planEntries) {
|
|
4062
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
4063
|
+
const passThrough = /* @__PURE__ */ new Set();
|
|
4064
|
+
const candidates = planEntries.filter((entry) => entry.id !== MOBILE_LAYOUT_MODULE && entry.url.startsWith("/plugins/"));
|
|
4065
|
+
let cursor = 0;
|
|
4066
|
+
const worker = async () => {
|
|
4067
|
+
while (cursor < candidates.length) {
|
|
4068
|
+
const index = cursor++;
|
|
4069
|
+
const source = candidates[index].url;
|
|
4070
|
+
const size = await this.upstreamBundleSize(source);
|
|
4071
|
+
if (size === void 0 || size >= MAX_MOBILE_BOOT_ENTRY_BYTES) passThrough.add(source);
|
|
4072
|
+
else sizes.set(source, size);
|
|
4073
|
+
}
|
|
4074
|
+
};
|
|
4075
|
+
await Promise.all(Array.from({ length: Math.min(8, candidates.length) }, worker));
|
|
4076
|
+
return {
|
|
4077
|
+
sizes,
|
|
4078
|
+
passThrough
|
|
4079
|
+
};
|
|
4080
|
+
}
|
|
4081
|
+
/**
|
|
4082
|
+
* Measure one upstream bundle by reading its body. The upstream serves
|
|
4083
|
+
* `/plugins` bundles as chunked streams without a Content-Length, so a probe
|
|
4084
|
+
* counts bytes; it aborts instantly past the per-entry cap. Measurements are
|
|
4085
|
+
* cached for a short window.
|
|
4086
|
+
*/
|
|
4087
|
+
async upstreamBundleSize(source) {
|
|
4088
|
+
if (!source.startsWith("/plugins/") || source.includes("#")) return void 0;
|
|
4089
|
+
const cached = this.bootEntrySizeCache.get(source);
|
|
4090
|
+
if (cached !== void 0 && Date.now() - cached.at < MOBILE_BOOT_SIZE_CACHE_TTL_MS) return cached.size;
|
|
4091
|
+
const target = new URL(source, this.config.upstreamOrigin);
|
|
4092
|
+
if (target.origin !== this.config.upstreamOrigin.origin) return void 0;
|
|
4093
|
+
const upstreamCookie = await this.upstreamCookieHeader();
|
|
4094
|
+
let upstreamRequest;
|
|
4095
|
+
try {
|
|
4096
|
+
const proxied = await new Promise((resolve, reject) => {
|
|
4097
|
+
upstreamRequest = request({
|
|
4098
|
+
protocol: "http:",
|
|
4099
|
+
hostname: stripIpv6Brackets(this.config.upstreamOrigin.hostname),
|
|
4100
|
+
port: Number(this.config.upstreamOrigin.port),
|
|
4101
|
+
method: "GET",
|
|
4102
|
+
path: `${target.pathname}${target.search}`,
|
|
4103
|
+
headers: {
|
|
4104
|
+
host: this.config.upstreamOrigin.host,
|
|
4105
|
+
accept: "text/javascript",
|
|
4106
|
+
"accept-encoding": "identity",
|
|
4107
|
+
[MOBILE_BOOT_SIZE_PROBE_HEADER]: "1",
|
|
4108
|
+
...upstreamCookie === void 0 ? {} : { cookie: upstreamCookie }
|
|
4109
|
+
},
|
|
4110
|
+
agent: false
|
|
4111
|
+
});
|
|
4112
|
+
upstreamRequest.setTimeout(this.config.upstreamTimeoutMs, () => {
|
|
4113
|
+
upstreamRequest?.destroy(upstreamTimeoutError());
|
|
4114
|
+
});
|
|
4115
|
+
upstreamRequest.once("response", resolve);
|
|
4116
|
+
upstreamRequest.once("error", reject);
|
|
4117
|
+
upstreamRequest.end();
|
|
4118
|
+
});
|
|
4119
|
+
if (proxied.statusCode !== 200) {
|
|
4120
|
+
proxied.resume();
|
|
4121
|
+
return;
|
|
4122
|
+
}
|
|
4123
|
+
const size = await new Promise((resolve, reject) => {
|
|
4124
|
+
let bytes = 0;
|
|
4125
|
+
proxied.on("data", (chunk) => {
|
|
4126
|
+
bytes += chunk.length;
|
|
4127
|
+
if (bytes > MAX_MOBILE_BOOT_ENTRY_BYTES) {
|
|
4128
|
+
proxied.destroy();
|
|
4129
|
+
resolve(void 0);
|
|
4130
|
+
}
|
|
4131
|
+
});
|
|
4132
|
+
proxied.once("end", () => resolve(bytes));
|
|
4133
|
+
proxied.once("error", reject);
|
|
4134
|
+
});
|
|
4135
|
+
if (size === void 0) return void 0;
|
|
4136
|
+
this.bootEntrySizeCache.set(source, {
|
|
4137
|
+
size,
|
|
4138
|
+
at: Date.now()
|
|
4139
|
+
});
|
|
4140
|
+
return size;
|
|
4141
|
+
} catch {
|
|
4142
|
+
return;
|
|
4143
|
+
} finally {
|
|
4144
|
+
upstreamRequest?.destroy();
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
3944
4147
|
async serveMobileBootBatch(key, request, response, authorization) {
|
|
3945
4148
|
if (request.method !== "GET" && request.method !== "HEAD") throw new HttpError(405, "method_not_allowed");
|
|
3946
4149
|
const stored = this.mobileBootBatches.get(key);
|
|
@@ -4022,7 +4225,7 @@ var MobileAccessGateway = class {
|
|
|
4022
4225
|
}
|
|
4023
4226
|
};
|
|
4024
4227
|
await Promise.all(Array.from({ length: Math.min(3, plan.entries.length) }, worker));
|
|
4025
|
-
if (bodies.reduce((bytes, body) => bytes + body.byteLength +
|
|
4228
|
+
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
4229
|
return Buffer.concat(bodies.flatMap((body) => [body, Buffer.from("\n;\n")]));
|
|
4027
4230
|
}
|
|
4028
4231
|
/**
|
|
@@ -4131,7 +4334,39 @@ var MobileAccessGateway = class {
|
|
|
4131
4334
|
}
|
|
4132
4335
|
};
|
|
4133
4336
|
}
|
|
4134
|
-
|
|
4337
|
+
/**
|
|
4338
|
+
* Proxy one request upstream. Pass-through client bundles (`GET /plugins`)
|
|
4339
|
+
* receive bounded transient retries — the upstream resets a fraction of fresh
|
|
4340
|
+
* connections — matching the resilience the merged-batch assembly already has.
|
|
4341
|
+
* A request is only retried before any byte reached the client.
|
|
4342
|
+
*/
|
|
4343
|
+
async proxyHttp(request, response, authorization) {
|
|
4344
|
+
if (!(request.method === "GET" && request.url?.split("?", 1)[0]?.startsWith("/plugins/") === true)) {
|
|
4345
|
+
try {
|
|
4346
|
+
await this.proxyHttpOnce(request, response, authorization);
|
|
4347
|
+
} catch (error) {
|
|
4348
|
+
if (error instanceof HttpError) throw error;
|
|
4349
|
+
if (response.headersSent) response.destroy();
|
|
4350
|
+
else throw new HttpError(502, "upstream_unavailable");
|
|
4351
|
+
}
|
|
4352
|
+
return;
|
|
4353
|
+
}
|
|
4354
|
+
const delay = (attempt) => new Promise((resolve) => setTimeout(resolve, MOBILE_BOOT_RETRY_DELAY_MS * attempt));
|
|
4355
|
+
for (let attempt = 1; attempt <= MOBILE_BOOT_UPSTREAM_ATTEMPTS; attempt++) try {
|
|
4356
|
+
await this.proxyHttpOnce(request, response, authorization);
|
|
4357
|
+
return;
|
|
4358
|
+
} catch (error) {
|
|
4359
|
+
if (response.headersSent) {
|
|
4360
|
+
response.destroy();
|
|
4361
|
+
return;
|
|
4362
|
+
}
|
|
4363
|
+
if (error instanceof HttpError || !isTransientUpstreamError(error)) throw error instanceof HttpError ? error : new HttpError(502, "upstream_unavailable");
|
|
4364
|
+
if (attempt === MOBILE_BOOT_UPSTREAM_ATTEMPTS) throw new HttpError(502, "upstream_unavailable");
|
|
4365
|
+
await delay(attempt);
|
|
4366
|
+
}
|
|
4367
|
+
throw new HttpError(502, "upstream_unavailable");
|
|
4368
|
+
}
|
|
4369
|
+
async proxyHttpOnce(request$2, response, authorization) {
|
|
4135
4370
|
const declared = request$2.headers["content-length"];
|
|
4136
4371
|
if (declared !== void 0 && (!/^\d+$/u.test(declared) || Number(declared) > this.config.maxBodyBytes)) throw new HttpError(413, "payload_too_large");
|
|
4137
4372
|
const holder = {};
|
|
@@ -4185,7 +4420,7 @@ var MobileAccessGateway = class {
|
|
|
4185
4420
|
await bodyDone?.catch(() => void 0);
|
|
4186
4421
|
if (error instanceof HttpError) throw error;
|
|
4187
4422
|
if (response.headersSent) response.destroy();
|
|
4188
|
-
|
|
4423
|
+
throw error;
|
|
4189
4424
|
} finally {
|
|
4190
4425
|
operation.release();
|
|
4191
4426
|
}
|
|
@@ -6512,24 +6747,45 @@ async function downloadPinnedArtifact(request) {
|
|
|
6512
6747
|
}
|
|
6513
6748
|
//#endregion
|
|
6514
6749
|
//#region src/cloudflared-component.ts
|
|
6750
|
+
const CLOUDFLARED_VERSION = "2026.9.1";
|
|
6751
|
+
/** Pinned official cloudflared release metadata for supported desktop targets. */
|
|
6752
|
+
const CLOUDFLARED_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
|
|
6753
|
+
{
|
|
6754
|
+
version: CLOUDFLARED_VERSION,
|
|
6755
|
+
platform: "win32",
|
|
6756
|
+
arch: "x64",
|
|
6757
|
+
downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-windows-amd64.exe`,
|
|
6758
|
+
downloadBytes: 54976432,
|
|
6759
|
+
downloadSha256: "2837888cc0f5d58f15b6dc478376de90b4d3ba5241c7947455d1e0a0df429712",
|
|
6760
|
+
executableName: "cloudflared.exe"
|
|
6761
|
+
},
|
|
6762
|
+
{
|
|
6763
|
+
version: CLOUDFLARED_VERSION,
|
|
6764
|
+
platform: "linux",
|
|
6765
|
+
arch: "x64",
|
|
6766
|
+
downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64`,
|
|
6767
|
+
downloadBytes: 39838488,
|
|
6768
|
+
downloadSha256: "03f1f25d1cc93b9ad6c60569d44060bc4f17ed97075760ed8cfca4b12dcd68cc",
|
|
6769
|
+
executableName: "cloudflared"
|
|
6770
|
+
},
|
|
6771
|
+
{
|
|
6772
|
+
version: CLOUDFLARED_VERSION,
|
|
6773
|
+
platform: "linux",
|
|
6774
|
+
arch: "arm64",
|
|
6775
|
+
downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64`,
|
|
6776
|
+
downloadBytes: 37466252,
|
|
6777
|
+
downloadSha256: "3d97437c71848bd8df68041e12436b484a661d95073ea1937f01a845ce88faa3",
|
|
6778
|
+
executableName: "cloudflared"
|
|
6779
|
+
}
|
|
6780
|
+
].map((release) => [`${release.platform}-${release.arch}`, Object.freeze(release)])));
|
|
6515
6781
|
/**
|
|
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.
|
|
6782
|
+
* Canonical release metadata. New code should select from
|
|
6783
|
+
* {@link CLOUDFLARED_COMPONENT_RELEASES} by platform and architecture; this
|
|
6784
|
+
* alias preserves the original Windows x64 entry for existing callers.
|
|
6522
6785
|
*/
|
|
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
|
-
});
|
|
6786
|
+
const CLOUDFLARED_COMPONENT_RELEASE = CLOUDFLARED_COMPONENT_RELEASES["win32-x64"];
|
|
6787
|
+
const DOWNLOAD_PAGE$1 = "https://github.com/cloudflare/cloudflared/releases";
|
|
6788
|
+
const TERMS_URL$1 = "https://www.cloudflare.com/website-terms/";
|
|
6533
6789
|
function inside$1(parent, child) {
|
|
6534
6790
|
const candidate = relative(parent, child);
|
|
6535
6791
|
return candidate !== "" && !candidate.startsWith("..") && !isAbsolute(candidate);
|
|
@@ -6546,14 +6802,18 @@ async function regularFile$1(file, expectedBytes) {
|
|
|
6546
6802
|
throw error;
|
|
6547
6803
|
}
|
|
6548
6804
|
}
|
|
6549
|
-
async function defaultFetchArtifact$1(url, signal) {
|
|
6805
|
+
async function defaultFetchArtifact$1(url, signal, expectedBytes) {
|
|
6550
6806
|
return downloadPinnedArtifact({
|
|
6551
6807
|
url,
|
|
6552
|
-
expectedBytes
|
|
6808
|
+
expectedBytes,
|
|
6553
6809
|
errorPrefix: "cloudflared",
|
|
6554
6810
|
signal
|
|
6555
6811
|
});
|
|
6556
6812
|
}
|
|
6813
|
+
/** Select the pinned artifact for one host, or undefined where unsupported. */
|
|
6814
|
+
function lookupRelease(platform, arch) {
|
|
6815
|
+
return CLOUDFLARED_COMPONENT_RELEASES[`${platform}-${arch}`];
|
|
6816
|
+
}
|
|
6557
6817
|
/** Owns the optional cloudflared binary inside DSH Mobile state. */
|
|
6558
6818
|
var CloudflaredComponentManager = class {
|
|
6559
6819
|
executable;
|
|
@@ -6564,6 +6824,7 @@ var CloudflaredComponentManager = class {
|
|
|
6564
6824
|
stagingRoot;
|
|
6565
6825
|
platform;
|
|
6566
6826
|
arch;
|
|
6827
|
+
release;
|
|
6567
6828
|
fetchArtifact;
|
|
6568
6829
|
installed = false;
|
|
6569
6830
|
errorCode;
|
|
@@ -6573,9 +6834,10 @@ var CloudflaredComponentManager = class {
|
|
|
6573
6834
|
if (!isAbsolute(stateDirectory)) throw new Error("cloudflared state directory must be absolute");
|
|
6574
6835
|
this.platform = options.platform ?? process.platform;
|
|
6575
6836
|
this.arch = options.arch ?? process.arch;
|
|
6837
|
+
this.release = lookupRelease(this.platform, this.arch);
|
|
6576
6838
|
this.componentRoot = join(stateDirectory, "components", "cloudflared");
|
|
6577
|
-
this.componentStorage = join(this.componentRoot,
|
|
6578
|
-
this.executable = join(this.componentStorage, "cloudflared
|
|
6839
|
+
this.componentStorage = join(this.componentRoot, this.release?.version ?? CLOUDFLARED_VERSION);
|
|
6840
|
+
this.executable = join(this.componentStorage, this.release?.executableName ?? "cloudflared");
|
|
6579
6841
|
this.stateRoot = join(stateDirectory, "state", "cloudflared");
|
|
6580
6842
|
this.logRoot = join(stateDirectory, "logs", "cloudflared");
|
|
6581
6843
|
this.stagingRoot = join(stateDirectory, "staging", "cloudflared");
|
|
@@ -6586,27 +6848,30 @@ var CloudflaredComponentManager = class {
|
|
|
6586
6848
|
this.logRoot,
|
|
6587
6849
|
this.stagingRoot
|
|
6588
6850
|
]) if (!inside$1(stateDirectory, child)) throw new Error("cloudflared component path escaped its state directory");
|
|
6589
|
-
|
|
6851
|
+
const release = this.release;
|
|
6852
|
+
this.fetchArtifact = options.fetchArtifact ?? ((url, signal) => defaultFetchArtifact$1(url, signal, release?.downloadBytes ?? 0));
|
|
6590
6853
|
}
|
|
6591
6854
|
/** Inspect the managed binary without using any global cloudflared state. */
|
|
6592
6855
|
async initialize() {
|
|
6593
|
-
|
|
6594
|
-
|
|
6856
|
+
const release = this.release;
|
|
6857
|
+
this.installed = release !== void 0 && await regularFile$1(this.executable, release.downloadBytes);
|
|
6858
|
+
if (this.installed && release !== void 0 && await sha256$1(this.executable) !== release.downloadSha256) {
|
|
6595
6859
|
this.installed = false;
|
|
6596
6860
|
this.errorCode = "cloudflared_component_invalid";
|
|
6597
6861
|
}
|
|
6598
6862
|
}
|
|
6599
6863
|
/** Return a safe status that never includes machine-specific account data. */
|
|
6600
6864
|
status() {
|
|
6865
|
+
const release = this.release ?? CLOUDFLARED_COMPONENT_RELEASE;
|
|
6601
6866
|
return Object.freeze({
|
|
6602
|
-
supported: this.
|
|
6867
|
+
supported: this.release !== void 0,
|
|
6603
6868
|
installed: this.installed,
|
|
6604
|
-
version:
|
|
6605
|
-
downloadBytes:
|
|
6606
|
-
installedBytes:
|
|
6607
|
-
sourceUrl:
|
|
6608
|
-
downloadPage:
|
|
6609
|
-
termsUrl:
|
|
6869
|
+
version: release.version,
|
|
6870
|
+
downloadBytes: release.downloadBytes,
|
|
6871
|
+
installedBytes: release.downloadBytes,
|
|
6872
|
+
sourceUrl: release.downloadUrl,
|
|
6873
|
+
downloadPage: DOWNLOAD_PAGE$1,
|
|
6874
|
+
termsUrl: TERMS_URL$1,
|
|
6610
6875
|
storagePath: this.componentRoot,
|
|
6611
6876
|
...this.errorCode === void 0 ? {} : { errorCode: this.errorCode }
|
|
6612
6877
|
});
|
|
@@ -6614,7 +6879,8 @@ var CloudflaredComponentManager = class {
|
|
|
6614
6879
|
/** Download, verify, and install the pinned cloudflared executable after explicit confirmation. */
|
|
6615
6880
|
install() {
|
|
6616
6881
|
return this.enqueue(async () => {
|
|
6617
|
-
|
|
6882
|
+
const release = this.release;
|
|
6883
|
+
if (release === void 0) throw new Error("cloudflared_component_unsupported");
|
|
6618
6884
|
await mkdir(this.stagingRoot, {
|
|
6619
6885
|
recursive: true,
|
|
6620
6886
|
mode: 448
|
|
@@ -6628,26 +6894,26 @@ var CloudflaredComponentManager = class {
|
|
|
6628
6894
|
timeout.unref();
|
|
6629
6895
|
let bytes;
|
|
6630
6896
|
try {
|
|
6631
|
-
bytes = await this.fetchArtifact(
|
|
6897
|
+
bytes = await this.fetchArtifact(release.downloadUrl, controller.signal);
|
|
6632
6898
|
} finally {
|
|
6633
6899
|
clearTimeout(timeout);
|
|
6634
6900
|
}
|
|
6635
|
-
if (bytes.byteLength !==
|
|
6636
|
-
if (createHash("sha256").update(bytes).digest("hex") !==
|
|
6637
|
-
const staged = join(staging,
|
|
6901
|
+
if (bytes.byteLength !== release.downloadBytes) throw new Error("cloudflared_download_size_mismatch");
|
|
6902
|
+
if (createHash("sha256").update(bytes).digest("hex") !== release.downloadSha256) throw new Error("cloudflared_download_hash_mismatch");
|
|
6903
|
+
const staged = join(staging, release.executableName);
|
|
6638
6904
|
await writeFile(staged, bytes, {
|
|
6639
6905
|
flag: "wx",
|
|
6640
6906
|
mode: 384
|
|
6641
6907
|
});
|
|
6642
6908
|
await chmod(staged, 448);
|
|
6643
|
-
if (!await regularFile$1(staged,
|
|
6909
|
+
if (!await regularFile$1(staged, release.downloadBytes) || await sha256$1(staged) !== release.downloadSha256) throw new Error("cloudflared_executable_hash_mismatch");
|
|
6644
6910
|
const candidate = join(this.componentRoot, `.install-${randomBytes(12).toString("hex")}`);
|
|
6645
6911
|
await mkdir(candidate, {
|
|
6646
6912
|
recursive: true,
|
|
6647
6913
|
mode: 448
|
|
6648
6914
|
});
|
|
6649
|
-
await copyFile(staged, join(candidate,
|
|
6650
|
-
await chmod(join(candidate,
|
|
6915
|
+
await copyFile(staged, join(candidate, release.executableName));
|
|
6916
|
+
await chmod(join(candidate, release.executableName), 448);
|
|
6651
6917
|
await rm(this.componentStorage, {
|
|
6652
6918
|
recursive: true,
|
|
6653
6919
|
force: true
|
|
@@ -6704,6 +6970,15 @@ const MAX_LOG_BUFFER_BYTES$1 = 65536;
|
|
|
6704
6970
|
* looking healthy for longer than a user will wait.
|
|
6705
6971
|
*/
|
|
6706
6972
|
const START_TIMEOUT_MS$1 = 6e4;
|
|
6973
|
+
/**
|
|
6974
|
+
* How many startup-timeout rounds a named tunnel may wait through while its
|
|
6975
|
+
* connector process is still alive. A rebooted PC often autostarts DSH before
|
|
6976
|
+
* Wi-Fi / VPN / DNS is usable; cloudflared retries the edge on its own, so
|
|
6977
|
+
* killing it after a single 60 s window turns a slow boot into a manual
|
|
6978
|
+
* reconnect loop. An exited connector (bad token, bad config) still fails fast
|
|
6979
|
+
* on the first round.
|
|
6980
|
+
*/
|
|
6981
|
+
const NAMED_STARTUP_ROUNDS = 5;
|
|
6707
6982
|
const CLOUDFLARED_HOST_SUFFIX = ".trycloudflare.com";
|
|
6708
6983
|
/**
|
|
6709
6984
|
* Subdomains Cloudflare reserves for its own control plane.
|
|
@@ -6846,6 +7121,7 @@ var CloudflaredController = class {
|
|
|
6846
7121
|
});
|
|
6847
7122
|
queue = Promise.resolve();
|
|
6848
7123
|
startupTimer;
|
|
7124
|
+
startupRounds = 0;
|
|
6849
7125
|
constructor(options) {
|
|
6850
7126
|
this.options = options;
|
|
6851
7127
|
if (!isAbsolute(options.executable)) throw new Error("cloudflared executable path must be absolute");
|
|
@@ -6977,6 +7253,7 @@ var CloudflaredController = class {
|
|
|
6977
7253
|
}
|
|
6978
7254
|
this.reservation = reservation;
|
|
6979
7255
|
this.buffer = "";
|
|
7256
|
+
this.startupRounds = 0;
|
|
6980
7257
|
this.publish({
|
|
6981
7258
|
enabled: true,
|
|
6982
7259
|
state: "starting"
|
|
@@ -7038,11 +7315,32 @@ var CloudflaredController = class {
|
|
|
7038
7315
|
this.child = void 0;
|
|
7039
7316
|
if (this.enabled) this.enqueue(() => this.failGeneration(generation, code === 0 ? "cloudflared_stopped" : "cloudflared_exited"));
|
|
7040
7317
|
});
|
|
7318
|
+
this.armStartupTimer(generation);
|
|
7319
|
+
}
|
|
7320
|
+
armStartupTimer(generation) {
|
|
7321
|
+
if (this.startupTimer !== void 0) clearTimeout(this.startupTimer);
|
|
7041
7322
|
this.startupTimer = setTimeout(() => {
|
|
7042
|
-
this.enqueue(() => this.
|
|
7323
|
+
this.enqueue(() => this.checkStartupTimeout(generation));
|
|
7043
7324
|
}, this.options.startupTimeoutMs ?? START_TIMEOUT_MS$1);
|
|
7044
7325
|
this.startupTimer.unref();
|
|
7045
7326
|
}
|
|
7327
|
+
/**
|
|
7328
|
+
* Give a live named connector more time instead of killing it: it retries the
|
|
7329
|
+
* edge on its own and usually registers once the rebooted network is usable.
|
|
7330
|
+
* A dead connector, or an exhausted budget, fails the generation as before.
|
|
7331
|
+
*/
|
|
7332
|
+
async checkStartupTimeout(generation) {
|
|
7333
|
+
if (generation !== this.generation || !this.enabled || this.disposed) return;
|
|
7334
|
+
if (this.latest.state === "ready") return;
|
|
7335
|
+
const childAlive = this.child !== void 0 && this.child.exitCode === null;
|
|
7336
|
+
const budget = this.options.maxStartupRounds ?? NAMED_STARTUP_ROUNDS;
|
|
7337
|
+
if (this.mode === "named" && childAlive && this.startupRounds + 1 < budget) {
|
|
7338
|
+
this.startupRounds += 1;
|
|
7339
|
+
this.armStartupTimer(generation);
|
|
7340
|
+
return;
|
|
7341
|
+
}
|
|
7342
|
+
await this.failGeneration(generation, "cloudflared_start_timeout");
|
|
7343
|
+
}
|
|
7046
7344
|
consume(generation, chunk) {
|
|
7047
7345
|
if (generation !== this.generation) return;
|
|
7048
7346
|
this.buffer += chunk;
|
|
@@ -9526,21 +9824,56 @@ var CpolarController = class {
|
|
|
9526
9824
|
};
|
|
9527
9825
|
//#endregion
|
|
9528
9826
|
//#region src/cpolar-component.ts
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
}
|
|
9827
|
+
const CPOLAR_VERSION = "3.3.18";
|
|
9828
|
+
/** Pinned official cpolar release metadata for supported desktop targets. */
|
|
9829
|
+
const CPOLAR_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
|
|
9830
|
+
{
|
|
9831
|
+
version: CPOLAR_VERSION,
|
|
9832
|
+
platform: "win32",
|
|
9833
|
+
arch: "x64",
|
|
9834
|
+
downloadUrl: `https://www.cpolar.com/static/downloads/releases/${CPOLAR_VERSION}/cpolar-stable-windows-amd64-setup.zip`,
|
|
9835
|
+
downloadBytes: 7603505,
|
|
9836
|
+
downloadSha256: "fb8cf60289058ee26079f995d2eeea0b21768a742d90c93015afe96e83428830",
|
|
9837
|
+
executableName: "cpolar.exe",
|
|
9838
|
+
executableBytes: 19637680,
|
|
9839
|
+
executableSha256: "b2d865ee505e842d22ceca5493a872efa893a79b079a7a8ee2bd3aa5343a5c41",
|
|
9840
|
+
archiveKind: "msi-zip"
|
|
9841
|
+
},
|
|
9842
|
+
{
|
|
9843
|
+
version: CPOLAR_VERSION,
|
|
9844
|
+
platform: "linux",
|
|
9845
|
+
arch: "x64",
|
|
9846
|
+
downloadUrl: `https://www.cpolar.com/static/downloads/releases/${CPOLAR_VERSION}/cpolar-stable-linux-amd64.tar.gz`,
|
|
9847
|
+
downloadBytes: 7404781,
|
|
9848
|
+
downloadSha256: "5cd3320c4369928ccb509c4f5fa2ec3b86151ead77e1b77c1694aa64c43e32e7",
|
|
9849
|
+
executableName: "cpolar",
|
|
9850
|
+
executableBytes: 19328632,
|
|
9851
|
+
executableSha256: "c076e1109372a3f88031841c5989030e49b6871b54aee3260c796da9122dec05",
|
|
9852
|
+
archiveKind: "tar.gz"
|
|
9853
|
+
},
|
|
9854
|
+
{
|
|
9855
|
+
version: CPOLAR_VERSION,
|
|
9856
|
+
platform: "linux",
|
|
9857
|
+
arch: "arm64",
|
|
9858
|
+
downloadUrl: `https://www.cpolar.com/static/downloads/releases/${CPOLAR_VERSION}/cpolar-stable-linux-arm64.tar.gz`,
|
|
9859
|
+
downloadBytes: 6855666,
|
|
9860
|
+
downloadSha256: "8a61a97983f18ae5ffb4b8bac4c9b3d8e2399a6ee101844e7b0b30d7f326157c",
|
|
9861
|
+
executableName: "cpolar",
|
|
9862
|
+
executableBytes: 19017169,
|
|
9863
|
+
executableSha256: "8d76a1b7e518df45f387f107c7a428079c67ecd9b971a2915c53b947ea9b443a",
|
|
9864
|
+
archiveKind: "tar.gz"
|
|
9865
|
+
}
|
|
9866
|
+
].map((release) => [`${release.platform}-${release.arch}`, Object.freeze(release)])));
|
|
9867
|
+
/**
|
|
9868
|
+
* Canonical release metadata. New code should select from
|
|
9869
|
+
* {@link CPOLAR_COMPONENT_RELEASES} by platform and architecture; this alias
|
|
9870
|
+
* preserves the original Windows x64 entry for existing callers.
|
|
9871
|
+
*/
|
|
9872
|
+
const CPOLAR_COMPONENT_RELEASE = CPOLAR_COMPONENT_RELEASES["win32-x64"];
|
|
9873
|
+
const DOWNLOAD_PAGE = "https://www.cpolar.com/download";
|
|
9874
|
+
const SIGNUP_URL = "https://dashboard.cpolar.com/signup";
|
|
9875
|
+
const DASHBOARD_URL = "https://dashboard.cpolar.com/auth";
|
|
9876
|
+
const TERMS_URL = "https://www.cpolar.com/tos";
|
|
9544
9877
|
function inside(parent, child) {
|
|
9545
9878
|
const candidate = relative(parent, child);
|
|
9546
9879
|
return candidate !== "" && !candidate.startsWith("..") && !isAbsolute(candidate);
|
|
@@ -9568,15 +9901,15 @@ async function run(file, args) {
|
|
|
9568
9901
|
});
|
|
9569
9902
|
});
|
|
9570
9903
|
}
|
|
9571
|
-
async function defaultFetchArtifact(url, signal) {
|
|
9904
|
+
async function defaultFetchArtifact(url, signal, expectedBytes) {
|
|
9572
9905
|
return downloadPinnedArtifact({
|
|
9573
9906
|
url,
|
|
9574
|
-
expectedBytes
|
|
9907
|
+
expectedBytes,
|
|
9575
9908
|
errorPrefix: "cpolar",
|
|
9576
9909
|
signal
|
|
9577
9910
|
});
|
|
9578
9911
|
}
|
|
9579
|
-
async function
|
|
9912
|
+
async function extractWindowsMsi(archive, destination, executableName) {
|
|
9580
9913
|
if (process.platform !== "win32") throw new Error("cpolar_component_unsupported");
|
|
9581
9914
|
const unpacked = join(destination, "archive");
|
|
9582
9915
|
const administrative = join(destination, "administrative");
|
|
@@ -9602,9 +9935,29 @@ async function defaultExtractArtifact(archive, destination) {
|
|
|
9602
9935
|
"/qn",
|
|
9603
9936
|
`TARGETDIR=${administrative}`
|
|
9604
9937
|
]);
|
|
9605
|
-
const executableRelative = (await readdir(administrative, { recursive: true })).find((entry) => basename(entry).toLowerCase() ===
|
|
9938
|
+
const executableRelative = (await readdir(administrative, { recursive: true })).find((entry) => basename(entry).toLowerCase() === executableName.toLowerCase());
|
|
9939
|
+
if (executableRelative === void 0) throw new Error("cpolar_executable_missing");
|
|
9940
|
+
await copyFile(join(administrative, executableRelative), join(destination, executableName));
|
|
9941
|
+
}
|
|
9942
|
+
async function extractLinuxTarball(archive, destination, executableName) {
|
|
9943
|
+
const unpacked = join(destination, "archive");
|
|
9944
|
+
await mkdir(unpacked, {
|
|
9945
|
+
recursive: true,
|
|
9946
|
+
mode: 448
|
|
9947
|
+
});
|
|
9948
|
+
await run(process.platform === "win32" ? "tar.exe" : "tar", [
|
|
9949
|
+
"-xzf",
|
|
9950
|
+
archive,
|
|
9951
|
+
"-C",
|
|
9952
|
+
unpacked
|
|
9953
|
+
]);
|
|
9954
|
+
const executableRelative = (await readdir(unpacked, { recursive: true })).find((entry) => basename(entry).toLowerCase() === executableName.toLowerCase());
|
|
9606
9955
|
if (executableRelative === void 0) throw new Error("cpolar_executable_missing");
|
|
9607
|
-
await copyFile(join(
|
|
9956
|
+
await copyFile(join(unpacked, executableRelative), join(destination, executableName));
|
|
9957
|
+
}
|
|
9958
|
+
async function defaultExtractArtifact(archive, destination, release) {
|
|
9959
|
+
if (release.archiveKind === "tar.gz") return extractLinuxTarball(archive, destination, release.executableName);
|
|
9960
|
+
return extractWindowsMsi(archive, destination, release.executableName);
|
|
9608
9961
|
}
|
|
9609
9962
|
/** Validate a cpolar Authtoken before it crosses the durable-file boundary. */
|
|
9610
9963
|
function validateCpolarAuthtoken(value) {
|
|
@@ -9622,6 +9975,7 @@ var CpolarComponentManager = class {
|
|
|
9622
9975
|
stagingRoot;
|
|
9623
9976
|
platform;
|
|
9624
9977
|
arch;
|
|
9978
|
+
release;
|
|
9625
9979
|
fetchArtifact;
|
|
9626
9980
|
extractArtifact;
|
|
9627
9981
|
installed = false;
|
|
@@ -9633,9 +9987,10 @@ var CpolarComponentManager = class {
|
|
|
9633
9987
|
if (!isAbsolute(stateDirectory)) throw new Error("cpolar state directory must be absolute");
|
|
9634
9988
|
this.platform = options.platform ?? process.platform;
|
|
9635
9989
|
this.arch = options.arch ?? process.arch;
|
|
9990
|
+
this.release = CPOLAR_COMPONENT_RELEASES[`${this.platform}-${this.arch}`];
|
|
9636
9991
|
this.componentRoot = join(stateDirectory, "components", "cpolar");
|
|
9637
|
-
this.componentStorage = join(this.componentRoot,
|
|
9638
|
-
this.executable = join(this.componentStorage, "cpolar
|
|
9992
|
+
this.componentStorage = join(this.componentRoot, this.release?.version ?? CPOLAR_VERSION);
|
|
9993
|
+
this.executable = join(this.componentStorage, this.release?.executableName ?? "cpolar");
|
|
9639
9994
|
this.stateRoot = join(stateDirectory, "state", "cpolar");
|
|
9640
9995
|
this.configFile = join(this.stateRoot, "cpolar.yml");
|
|
9641
9996
|
this.logRoot = join(stateDirectory, "logs", "cpolar");
|
|
@@ -9647,13 +10002,18 @@ var CpolarComponentManager = class {
|
|
|
9647
10002
|
this.logRoot,
|
|
9648
10003
|
this.stagingRoot
|
|
9649
10004
|
]) if (!inside(stateDirectory, child)) throw new Error("cpolar component path escaped its state directory");
|
|
9650
|
-
|
|
9651
|
-
this.
|
|
10005
|
+
const release = this.release;
|
|
10006
|
+
this.fetchArtifact = options.fetchArtifact ?? ((url, signal) => defaultFetchArtifact(url, signal, release?.downloadBytes ?? 0));
|
|
10007
|
+
this.extractArtifact = options.extractArtifact ?? ((archive, destination) => {
|
|
10008
|
+
if (release === void 0) throw new Error("cpolar_component_unsupported");
|
|
10009
|
+
return defaultExtractArtifact(archive, destination, release);
|
|
10010
|
+
});
|
|
9652
10011
|
}
|
|
9653
10012
|
/** Inspect the managed binary and configuration without using global cpolar state. */
|
|
9654
10013
|
async initialize() {
|
|
9655
|
-
|
|
9656
|
-
|
|
10014
|
+
const release = this.release;
|
|
10015
|
+
this.installed = release !== void 0 && await regularFile(this.executable, release.executableBytes);
|
|
10016
|
+
if (this.installed && release !== void 0 && await sha256(this.executable) !== release.executableSha256) {
|
|
9657
10017
|
this.installed = false;
|
|
9658
10018
|
this.errorCode = "cpolar_component_invalid";
|
|
9659
10019
|
}
|
|
@@ -9662,18 +10022,19 @@ var CpolarComponentManager = class {
|
|
|
9662
10022
|
}
|
|
9663
10023
|
/** Return a safe status that never includes the account token. */
|
|
9664
10024
|
status() {
|
|
10025
|
+
const release = this.release ?? CPOLAR_COMPONENT_RELEASE;
|
|
9665
10026
|
return Object.freeze({
|
|
9666
|
-
supported: this.
|
|
10027
|
+
supported: this.release !== void 0,
|
|
9667
10028
|
installed: this.installed,
|
|
9668
10029
|
configured: this.configured,
|
|
9669
|
-
version:
|
|
9670
|
-
downloadBytes:
|
|
9671
|
-
installedBytes:
|
|
9672
|
-
sourceUrl:
|
|
9673
|
-
downloadPage:
|
|
9674
|
-
signupUrl:
|
|
9675
|
-
dashboardUrl:
|
|
9676
|
-
termsUrl:
|
|
10030
|
+
version: release.version,
|
|
10031
|
+
downloadBytes: release.downloadBytes,
|
|
10032
|
+
installedBytes: release.executableBytes,
|
|
10033
|
+
sourceUrl: release.downloadUrl,
|
|
10034
|
+
downloadPage: DOWNLOAD_PAGE,
|
|
10035
|
+
signupUrl: SIGNUP_URL,
|
|
10036
|
+
dashboardUrl: DASHBOARD_URL,
|
|
10037
|
+
termsUrl: TERMS_URL,
|
|
9677
10038
|
storagePath: this.componentRoot,
|
|
9678
10039
|
...this.errorCode === void 0 ? {} : { errorCode: this.errorCode }
|
|
9679
10040
|
});
|
|
@@ -9681,7 +10042,8 @@ var CpolarComponentManager = class {
|
|
|
9681
10042
|
/** Download, verify, and administratively extract cpolar after explicit confirmation. */
|
|
9682
10043
|
install() {
|
|
9683
10044
|
return this.enqueue(async () => {
|
|
9684
|
-
|
|
10045
|
+
const release = this.release;
|
|
10046
|
+
if (release === void 0) throw new Error("cpolar_component_unsupported");
|
|
9685
10047
|
await mkdir(this.stagingRoot, {
|
|
9686
10048
|
recursive: true,
|
|
9687
10049
|
mode: 448
|
|
@@ -9695,26 +10057,26 @@ var CpolarComponentManager = class {
|
|
|
9695
10057
|
timeout.unref();
|
|
9696
10058
|
let bytes;
|
|
9697
10059
|
try {
|
|
9698
|
-
bytes = await this.fetchArtifact(
|
|
10060
|
+
bytes = await this.fetchArtifact(release.downloadUrl, controller.signal);
|
|
9699
10061
|
} finally {
|
|
9700
10062
|
clearTimeout(timeout);
|
|
9701
10063
|
}
|
|
9702
|
-
if (createHash("sha256").update(bytes).digest("hex") !==
|
|
9703
|
-
const archive = join(staging, "cpolar.zip");
|
|
10064
|
+
if (createHash("sha256").update(bytes).digest("hex") !== release.downloadSha256) throw new Error("cpolar_download_hash_mismatch");
|
|
10065
|
+
const archive = join(staging, release.archiveKind === "tar.gz" ? "cpolar.tar.gz" : "cpolar.zip");
|
|
9704
10066
|
await writeFile(archive, bytes, {
|
|
9705
10067
|
flag: "wx",
|
|
9706
10068
|
mode: 384
|
|
9707
10069
|
});
|
|
9708
10070
|
await this.extractArtifact(archive, staging);
|
|
9709
|
-
const extracted = join(staging,
|
|
9710
|
-
if (!await regularFile(extracted,
|
|
10071
|
+
const extracted = join(staging, release.executableName);
|
|
10072
|
+
if (!await regularFile(extracted, release.executableBytes) || await sha256(extracted) !== release.executableSha256) throw new Error("cpolar_executable_hash_mismatch");
|
|
9711
10073
|
const candidate = join(this.componentRoot, `.install-${randomBytes(12).toString("hex")}`);
|
|
9712
10074
|
await mkdir(candidate, {
|
|
9713
10075
|
recursive: true,
|
|
9714
10076
|
mode: 448
|
|
9715
10077
|
});
|
|
9716
|
-
await copyFile(extracted, join(candidate,
|
|
9717
|
-
await chmod(join(candidate,
|
|
10078
|
+
await copyFile(extracted, join(candidate, release.executableName));
|
|
10079
|
+
await chmod(join(candidate, release.executableName), 448);
|
|
9718
10080
|
await rm(this.componentStorage, {
|
|
9719
10081
|
recursive: true,
|
|
9720
10082
|
force: true
|
|
@@ -10404,6 +10766,16 @@ function selectLanNetwork(requestedAddress, requestedInterface, table, preferred
|
|
|
10404
10766
|
if (physicalCandidates.length === 1) return physicalCandidates[0];
|
|
10405
10767
|
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
10768
|
}
|
|
10769
|
+
/**
|
|
10770
|
+
* True when the failure came from LAN selection (the configured network is
|
|
10771
|
+
* gone, nothing is up, or several are) rather than from config/TLS code.
|
|
10772
|
+
* Callers use it to degrade (warn + stay down) instead of failing boot.
|
|
10773
|
+
* Pinned to the selectLanNetwork message contract — keep in sync.
|
|
10774
|
+
*/
|
|
10775
|
+
function isNetworkSelectionError(error) {
|
|
10776
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
10777
|
+
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");
|
|
10778
|
+
}
|
|
10407
10779
|
function assertMatchingCa(certPem, keyPem) {
|
|
10408
10780
|
const certificate = new X509Certificate(certPem);
|
|
10409
10781
|
if (!certificate.ca || certificate.subject !== certificate.issuer || !certificate.verify(certificate.publicKey)) throw new Error("managed TLS CA must be a self-signed CA certificate");
|
|
@@ -10965,7 +11337,14 @@ async function apply(ctx, config) {
|
|
|
10965
11337
|
}, (error) => {
|
|
10966
11338
|
process.emitWarning(`DSH Mobile could not follow the current LAN address: ${error instanceof Error ? error.message : String(error)}`, { code: "DSH_MOBILE_NETWORK_REFRESH" });
|
|
10967
11339
|
});
|
|
10968
|
-
|
|
11340
|
+
try {
|
|
11341
|
+
await following.initialize(2e3);
|
|
11342
|
+
} catch (error) {
|
|
11343
|
+
if (!isNetworkSelectionError(error)) throw error;
|
|
11344
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
11345
|
+
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" });
|
|
11346
|
+
following.beginPolling(2e3);
|
|
11347
|
+
}
|
|
10969
11348
|
return following;
|
|
10970
11349
|
};
|
|
10971
11350
|
const lanControlFile = parseControlFile(config.controlFile);
|