dsh-mobile 0.3.13 → 0.3.15
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 +24 -0
- package/README.en.md +10 -9
- package/README.md +10 -9
- package/SECURITY.md +1 -1
- package/docs/DSH_0.1.5_LAN.md +27 -0
- package/lib/client.js +109 -18
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +13 -0
- package/lib/index.mjs +225 -25
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-layout.js +119 -10
- package/lib/mobile-layout.js.map +1 -1
- package/package.json +14 -12
package/lib/index.d.mts
CHANGED
|
@@ -645,7 +645,20 @@ declare class MobileAccessGateway {
|
|
|
645
645
|
private proxyMobileIndex;
|
|
646
646
|
private rememberMobileBootBatch;
|
|
647
647
|
private serveMobileBootBatch;
|
|
648
|
+
private startMobileBootBatchAssembly;
|
|
648
649
|
private assembleMobileBootBatch;
|
|
650
|
+
/**
|
|
651
|
+
* Read one upstream bundle, retrying transient connection failures.
|
|
652
|
+
*
|
|
653
|
+
* Assembling a batch fans out over every client entry, and the upstream
|
|
654
|
+
* resets a fraction of those connections before sending a byte
|
|
655
|
+
* (`read ECONNRESET`, recv=0) — randomly, on any entry, at any concurrency.
|
|
656
|
+
* A single such reset used to fail the whole batch with 502
|
|
657
|
+
* `upstream_unavailable`, surfacing in the browser as "bundle script
|
|
658
|
+
* /mobile-access/mobile-boot/<hash>.js failed to load". Retrying recovers
|
|
659
|
+
* every observed reset; a genuine upstream error still fails.
|
|
660
|
+
*/
|
|
661
|
+
private readUpstreamClientBundleWithRetry;
|
|
649
662
|
private readUpstreamClientBundle;
|
|
650
663
|
private allocateRequest;
|
|
651
664
|
private proxyHttp;
|
package/lib/index.mjs
CHANGED
|
@@ -1061,7 +1061,8 @@ const AUTH_PREFIX = "/mobile-access";
|
|
|
1061
1061
|
const WS_PATHS = /* @__PURE__ */ new Set([
|
|
1062
1062
|
"/api/events.mux",
|
|
1063
1063
|
"/api/events.host",
|
|
1064
|
-
"/api/remote.mux"
|
|
1064
|
+
"/api/remote.mux",
|
|
1065
|
+
"/sidebar/ws/terminal"
|
|
1065
1066
|
]);
|
|
1066
1067
|
/** Terse request failure safe to expose without internal diagnostics. */
|
|
1067
1068
|
var HttpError = class extends Error {
|
|
@@ -2108,6 +2109,21 @@ const MOBILE_BOOT_BATCH_PREFIX = `${AUTH_PREFIX}/mobile-boot/`;
|
|
|
2108
2109
|
const MAX_MOBILE_BOOT_BATCH_BYTES = 33554432;
|
|
2109
2110
|
const MAX_MOBILE_BOOT_ENTRY_BYTES = 8388608;
|
|
2110
2111
|
const MAX_MOBILE_BOOT_BATCHES = 8;
|
|
2112
|
+
const MOBILE_BOOT_UPSTREAM_ATTEMPTS = 4;
|
|
2113
|
+
const MOBILE_BOOT_RETRY_DELAY_MS = 150;
|
|
2114
|
+
const TRANSIENT_UPSTREAM_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
2115
|
+
"EAI_AGAIN",
|
|
2116
|
+
"ECONNABORTED",
|
|
2117
|
+
"ECONNREFUSED",
|
|
2118
|
+
"ECONNRESET",
|
|
2119
|
+
"EPIPE",
|
|
2120
|
+
"EHOSTUNREACH",
|
|
2121
|
+
"ENETDOWN",
|
|
2122
|
+
"ENETRESET",
|
|
2123
|
+
"ENETUNREACH",
|
|
2124
|
+
"ETIMEDOUT",
|
|
2125
|
+
"ERR_STREAM_PREMATURE_CLOSE"
|
|
2126
|
+
]);
|
|
2111
2127
|
const UPSTREAM_AUTH_REFRESH_MARGIN_MS = 6e4;
|
|
2112
2128
|
const UPSTREAM_COOKIE_PAIR = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+=[\x21-\x3A\x3C-\x7E]*$/u;
|
|
2113
2129
|
const CUSTOM_STYLE_FALLBACK = "/* Add mobile overrides in the DSH home mobile-access/mobile.css file. */\n";
|
|
@@ -2575,6 +2591,51 @@ function mapError(error) {
|
|
|
2575
2591
|
if (error instanceof MobileExtensionError) return new HttpError(error.status, error.code);
|
|
2576
2592
|
return new HttpError(500, "internal_error");
|
|
2577
2593
|
}
|
|
2594
|
+
function requestAbortedError() {
|
|
2595
|
+
const error = /* @__PURE__ */ new Error("request aborted");
|
|
2596
|
+
error.name = "AbortError";
|
|
2597
|
+
return error;
|
|
2598
|
+
}
|
|
2599
|
+
function isTransientUpstreamError(error) {
|
|
2600
|
+
if (!(error instanceof Error)) return false;
|
|
2601
|
+
const code = error.code;
|
|
2602
|
+
return typeof code === "string" && TRANSIENT_UPSTREAM_ERROR_CODES.has(code);
|
|
2603
|
+
}
|
|
2604
|
+
function upstreamTimeoutError() {
|
|
2605
|
+
const error = /* @__PURE__ */ new Error("upstream timeout");
|
|
2606
|
+
error.code = "ETIMEDOUT";
|
|
2607
|
+
return error;
|
|
2608
|
+
}
|
|
2609
|
+
function waitForAbortableDelay(delayMs, signal) {
|
|
2610
|
+
if (signal.aborted) return Promise.reject(requestAbortedError());
|
|
2611
|
+
return new Promise((resolve, reject) => {
|
|
2612
|
+
const aborted = () => {
|
|
2613
|
+
clearTimeout(timer);
|
|
2614
|
+
reject(requestAbortedError());
|
|
2615
|
+
};
|
|
2616
|
+
const timer = setTimeout(() => {
|
|
2617
|
+
signal.removeEventListener("abort", aborted);
|
|
2618
|
+
resolve();
|
|
2619
|
+
}, delayMs);
|
|
2620
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
function waitForRequestTask(task, signal) {
|
|
2624
|
+
if (signal.aborted) return Promise.reject(requestAbortedError());
|
|
2625
|
+
return new Promise((resolve, reject) => {
|
|
2626
|
+
const aborted = () => {
|
|
2627
|
+
reject(requestAbortedError());
|
|
2628
|
+
};
|
|
2629
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
2630
|
+
task.then((value) => {
|
|
2631
|
+
signal.removeEventListener("abort", aborted);
|
|
2632
|
+
resolve(value);
|
|
2633
|
+
}, (error) => {
|
|
2634
|
+
signal.removeEventListener("abort", aborted);
|
|
2635
|
+
reject(error);
|
|
2636
|
+
});
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2578
2639
|
function discoveryDeviceName() {
|
|
2579
2640
|
const value = hostname().trim().replaceAll(/[\u0000-\u001f\u007f]/gu, "");
|
|
2580
2641
|
return (value === "" ? "DeepSeek Harness" : value).slice(0, 63);
|
|
@@ -3574,6 +3635,7 @@ var MobileAccessGateway = class {
|
|
|
3574
3635
|
while (this.mobileBootBatches.size > MAX_MOBILE_BOOT_BATCHES) {
|
|
3575
3636
|
const oldest = this.mobileBootBatches.keys().next().value;
|
|
3576
3637
|
if (oldest === void 0) break;
|
|
3638
|
+
this.mobileBootBatches.get(oldest)?.assembly?.controller.abort();
|
|
3577
3639
|
this.mobileBootBatches.delete(oldest);
|
|
3578
3640
|
}
|
|
3579
3641
|
}
|
|
@@ -3582,17 +3644,17 @@ var MobileAccessGateway = class {
|
|
|
3582
3644
|
const stored = this.mobileBootBatches.get(key);
|
|
3583
3645
|
if (stored === void 0) throw new HttpError(404, "not_found");
|
|
3584
3646
|
const operation = this.allocateRequest(authorization, response, {});
|
|
3647
|
+
response.once("close", operation.abort);
|
|
3585
3648
|
try {
|
|
3586
3649
|
const layoutStat = await stat(this.config.mobileLayoutFile);
|
|
3587
3650
|
if (stored.body === void 0 || stored.etag === void 0 || stored.layoutMtimeMs !== layoutStat.mtimeMs) {
|
|
3588
|
-
|
|
3589
|
-
stored.
|
|
3590
|
-
delete stored.gzipBody;
|
|
3591
|
-
stored.etag = createHash("sha256").update(body).digest("hex");
|
|
3592
|
-
stored.layoutMtimeMs = layoutStat.mtimeMs;
|
|
3651
|
+
operation.signal.throwIfAborted();
|
|
3652
|
+
await waitForRequestTask((stored.assembly ?? this.startMobileBootBatchAssembly(stored, layoutStat.mtimeMs)).task, operation.signal);
|
|
3593
3653
|
}
|
|
3594
3654
|
const compressed = acceptsGzip(request.headers["accept-encoding"]);
|
|
3595
|
-
const
|
|
3655
|
+
const assembled = stored.body;
|
|
3656
|
+
if (assembled === void 0) throw new HttpError(502, "upstream_unavailable");
|
|
3657
|
+
const body = compressed ? stored.gzipBody ??= await gzipBuffer(assembled) : assembled;
|
|
3596
3658
|
const etag = compressed ? `${stored.etag}-gzip` : stored.etag;
|
|
3597
3659
|
const headers = {
|
|
3598
3660
|
"Content-Type": "text/javascript; charset=utf-8",
|
|
@@ -3620,9 +3682,32 @@ var MobileAccessGateway = class {
|
|
|
3620
3682
|
if (error.code === "ENOENT") throw new HttpError(503, "mobile_frontend_unavailable");
|
|
3621
3683
|
throw error;
|
|
3622
3684
|
} finally {
|
|
3685
|
+
response.removeListener("close", operation.abort);
|
|
3623
3686
|
operation.release();
|
|
3624
3687
|
}
|
|
3625
3688
|
}
|
|
3689
|
+
startMobileBootBatchAssembly(stored, layoutMtimeMs) {
|
|
3690
|
+
const controller = new AbortController();
|
|
3691
|
+
const task = (async () => {
|
|
3692
|
+
const body = await this.assembleMobileBootBatch(stored.plan, controller.signal);
|
|
3693
|
+
stored.body = body;
|
|
3694
|
+
delete stored.gzipBody;
|
|
3695
|
+
stored.etag = createHash("sha256").update(body).digest("hex");
|
|
3696
|
+
stored.layoutMtimeMs = layoutMtimeMs;
|
|
3697
|
+
return body;
|
|
3698
|
+
})();
|
|
3699
|
+
const assembly = Object.freeze({
|
|
3700
|
+
controller,
|
|
3701
|
+
task
|
|
3702
|
+
});
|
|
3703
|
+
stored.assembly = assembly;
|
|
3704
|
+
task.then(() => {
|
|
3705
|
+
if (stored.assembly === assembly) delete stored.assembly;
|
|
3706
|
+
}, () => {
|
|
3707
|
+
if (stored.assembly === assembly) delete stored.assembly;
|
|
3708
|
+
});
|
|
3709
|
+
return assembly;
|
|
3710
|
+
}
|
|
3626
3711
|
async assembleMobileBootBatch(plan, signal) {
|
|
3627
3712
|
const bodies = new Array(plan.entries.length);
|
|
3628
3713
|
let cursor = 0;
|
|
@@ -3630,15 +3715,41 @@ var MobileAccessGateway = class {
|
|
|
3630
3715
|
while (cursor < plan.entries.length) {
|
|
3631
3716
|
const index = cursor++;
|
|
3632
3717
|
const entry = plan.entries[index];
|
|
3633
|
-
bodies[index] = entry.id === MOBILE_LAYOUT_MODULE ? await readFile(this.config.mobileLayoutFile, { signal }) : await this.
|
|
3718
|
+
bodies[index] = entry.id === MOBILE_LAYOUT_MODULE ? await readFile(this.config.mobileLayoutFile, { signal }) : await this.readUpstreamClientBundleWithRetry(entry.url, signal);
|
|
3634
3719
|
if (bodies[index].byteLength > MAX_MOBILE_BOOT_ENTRY_BYTES) throw new HttpError(502, "upstream_unavailable");
|
|
3635
3720
|
}
|
|
3636
3721
|
};
|
|
3637
|
-
await Promise.all(Array.from({ length: Math.min(
|
|
3722
|
+
await Promise.all(Array.from({ length: Math.min(3, plan.entries.length) }, worker));
|
|
3638
3723
|
if (bodies.reduce((bytes, body) => bytes + body.byteLength + 2, 0) > MAX_MOBILE_BOOT_BATCH_BYTES) throw new HttpError(502, "upstream_unavailable");
|
|
3639
3724
|
return Buffer.concat(bodies.flatMap((body) => [body, Buffer.from("\n;\n")]));
|
|
3640
3725
|
}
|
|
3726
|
+
/**
|
|
3727
|
+
* Read one upstream bundle, retrying transient connection failures.
|
|
3728
|
+
*
|
|
3729
|
+
* Assembling a batch fans out over every client entry, and the upstream
|
|
3730
|
+
* resets a fraction of those connections before sending a byte
|
|
3731
|
+
* (`read ECONNRESET`, recv=0) — randomly, on any entry, at any concurrency.
|
|
3732
|
+
* A single such reset used to fail the whole batch with 502
|
|
3733
|
+
* `upstream_unavailable`, surfacing in the browser as "bundle script
|
|
3734
|
+
* /mobile-access/mobile-boot/<hash>.js failed to load". Retrying recovers
|
|
3735
|
+
* every observed reset; a genuine upstream error still fails.
|
|
3736
|
+
*/
|
|
3737
|
+
async readUpstreamClientBundleWithRetry(source, signal) {
|
|
3738
|
+
for (let attempt = 1; attempt <= MOBILE_BOOT_UPSTREAM_ATTEMPTS; attempt++) {
|
|
3739
|
+
signal.throwIfAborted();
|
|
3740
|
+
try {
|
|
3741
|
+
return await this.readUpstreamClientBundle(source, signal);
|
|
3742
|
+
} catch (error) {
|
|
3743
|
+
if (signal.aborted) throw error;
|
|
3744
|
+
if (!isTransientUpstreamError(error)) throw error;
|
|
3745
|
+
if (attempt === MOBILE_BOOT_UPSTREAM_ATTEMPTS) throw new HttpError(502, "upstream_unavailable");
|
|
3746
|
+
await waitForAbortableDelay(MOBILE_BOOT_RETRY_DELAY_MS * attempt, signal);
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
throw new HttpError(502, "upstream_unavailable");
|
|
3750
|
+
}
|
|
3641
3751
|
async readUpstreamClientBundle(source, signal) {
|
|
3752
|
+
signal.throwIfAborted();
|
|
3642
3753
|
if (!source.startsWith("/plugins/") || source.includes("#")) throw new HttpError(502, "upstream_unavailable");
|
|
3643
3754
|
const target = new URL(source, this.config.upstreamOrigin);
|
|
3644
3755
|
if (target.origin !== this.config.upstreamOrigin.origin) throw new HttpError(502, "upstream_unavailable");
|
|
@@ -3649,6 +3760,7 @@ var MobileAccessGateway = class {
|
|
|
3649
3760
|
signal.addEventListener("abort", aborted, { once: true });
|
|
3650
3761
|
try {
|
|
3651
3762
|
const upstreamCookie = await this.upstreamCookieHeader();
|
|
3763
|
+
signal.throwIfAborted();
|
|
3652
3764
|
const proxied = await new Promise((resolve, reject) => {
|
|
3653
3765
|
upstreamRequest = request({
|
|
3654
3766
|
protocol: "http:",
|
|
@@ -3665,7 +3777,7 @@ var MobileAccessGateway = class {
|
|
|
3665
3777
|
agent: false
|
|
3666
3778
|
});
|
|
3667
3779
|
upstreamRequest.setTimeout(this.config.upstreamTimeoutMs, () => {
|
|
3668
|
-
upstreamRequest?.destroy(
|
|
3780
|
+
upstreamRequest?.destroy(upstreamTimeoutError());
|
|
3669
3781
|
});
|
|
3670
3782
|
upstreamRequest.once("response", resolve);
|
|
3671
3783
|
upstreamRequest.once("error", reject);
|
|
@@ -3683,6 +3795,7 @@ var MobileAccessGateway = class {
|
|
|
3683
3795
|
return Buffer.concat(chunks);
|
|
3684
3796
|
} catch (error) {
|
|
3685
3797
|
if (error instanceof HttpError) throw error;
|
|
3798
|
+
if (signal.aborted || isTransientUpstreamError(error)) throw error;
|
|
3686
3799
|
throw new HttpError(502, "upstream_unavailable");
|
|
3687
3800
|
} finally {
|
|
3688
3801
|
signal.removeEventListener("abort", aborted);
|
|
@@ -3708,6 +3821,7 @@ var MobileAccessGateway = class {
|
|
|
3708
3821
|
return {
|
|
3709
3822
|
id,
|
|
3710
3823
|
signal: controller.signal,
|
|
3824
|
+
abort,
|
|
3711
3825
|
release: () => {
|
|
3712
3826
|
const entry = this.activeRequests.get(id);
|
|
3713
3827
|
if (entry !== void 0) clearTimeout(entry.timer);
|
|
@@ -4105,6 +4219,7 @@ var MobileAccessGateway = class {
|
|
|
4105
4219
|
this.upstreamAuthRequest = void 0;
|
|
4106
4220
|
this.removeSessionListener();
|
|
4107
4221
|
const accessClose = this.access.close();
|
|
4222
|
+
for (const stored of this.mobileBootBatches.values()) stored.assembly?.controller.abort();
|
|
4108
4223
|
for (const request of this.activeRequests.values()) request.abort();
|
|
4109
4224
|
for (const websocket of this.activeWebSockets.values()) {
|
|
4110
4225
|
websocket.client.destroy();
|
|
@@ -7340,6 +7455,18 @@ async function reserveLoopbackPort() {
|
|
|
7340
7455
|
}
|
|
7341
7456
|
};
|
|
7342
7457
|
}
|
|
7458
|
+
function spawnCpolarProcess(executable, args, environment) {
|
|
7459
|
+
return spawn(executable, [...args], {
|
|
7460
|
+
env: environment,
|
|
7461
|
+
shell: false,
|
|
7462
|
+
stdio: [
|
|
7463
|
+
"pipe",
|
|
7464
|
+
"pipe",
|
|
7465
|
+
"pipe"
|
|
7466
|
+
],
|
|
7467
|
+
windowsHide: true
|
|
7468
|
+
});
|
|
7469
|
+
}
|
|
7343
7470
|
function withoutProxyEnvironment(environment) {
|
|
7344
7471
|
const blocked = /* @__PURE__ */ new Set([
|
|
7345
7472
|
"HTTP_PROXY",
|
|
@@ -7526,16 +7653,7 @@ var CpolarController = class {
|
|
|
7526
7653
|
"-log-level=INFO",
|
|
7527
7654
|
String(reservation.port)
|
|
7528
7655
|
];
|
|
7529
|
-
const child =
|
|
7530
|
-
env: withoutProxyEnvironment(process.env),
|
|
7531
|
-
shell: false,
|
|
7532
|
-
stdio: [
|
|
7533
|
-
"pipe",
|
|
7534
|
-
"pipe",
|
|
7535
|
-
"pipe"
|
|
7536
|
-
],
|
|
7537
|
-
windowsHide: true
|
|
7538
|
-
});
|
|
7656
|
+
const child = (this.options.spawnProcess ?? spawnCpolarProcess)(this.options.executable, args, withoutProxyEnvironment(process.env));
|
|
7539
7657
|
this.child = child;
|
|
7540
7658
|
child.stdout.setEncoding("utf8");
|
|
7541
7659
|
child.stderr.setEncoding("utf8");
|
|
@@ -7581,7 +7699,13 @@ var CpolarController = class {
|
|
|
7581
7699
|
}
|
|
7582
7700
|
}
|
|
7583
7701
|
async attachGateway(generation, origin) {
|
|
7584
|
-
if (generation !== this.generation || !this.enabled || this.
|
|
7702
|
+
if (generation !== this.generation || !this.enabled || this.disposed) return;
|
|
7703
|
+
const current = this.gatewayValue;
|
|
7704
|
+
if (current !== void 0) {
|
|
7705
|
+
if (current.address().origin === origin) return;
|
|
7706
|
+
await this.rotateGateway(generation, origin, current);
|
|
7707
|
+
return;
|
|
7708
|
+
}
|
|
7585
7709
|
const reservation = this.reservation;
|
|
7586
7710
|
if (reservation === void 0) return;
|
|
7587
7711
|
this.publish({
|
|
@@ -7598,7 +7722,7 @@ var CpolarController = class {
|
|
|
7598
7722
|
await this.failGeneration(generation, "gateway_start_failed");
|
|
7599
7723
|
return;
|
|
7600
7724
|
}
|
|
7601
|
-
if (generation !== this.generation || !this.enabled) {
|
|
7725
|
+
if (generation !== this.generation || !this.enabled || this.disposed || this.child === void 0) {
|
|
7602
7726
|
await gateway.close();
|
|
7603
7727
|
return;
|
|
7604
7728
|
}
|
|
@@ -7611,6 +7735,40 @@ var CpolarController = class {
|
|
|
7611
7735
|
origin
|
|
7612
7736
|
});
|
|
7613
7737
|
}
|
|
7738
|
+
/** Replace the gateway authority when cpolar rotates a temporary public origin. */
|
|
7739
|
+
async rotateGateway(generation, origin, current) {
|
|
7740
|
+
const listenPort = current.address().port;
|
|
7741
|
+
if (this.gatewayValue === current) this.gatewayValue = void 0;
|
|
7742
|
+
this.publish({
|
|
7743
|
+
enabled: true,
|
|
7744
|
+
state: "connecting",
|
|
7745
|
+
origin
|
|
7746
|
+
});
|
|
7747
|
+
try {
|
|
7748
|
+
await current.close();
|
|
7749
|
+
} catch {
|
|
7750
|
+
await this.failGeneration(generation, "gateway_start_failed");
|
|
7751
|
+
return;
|
|
7752
|
+
}
|
|
7753
|
+
if (generation !== this.generation || !this.enabled || this.disposed || this.child === void 0) return;
|
|
7754
|
+
let replacement;
|
|
7755
|
+
try {
|
|
7756
|
+
replacement = await this.options.createGateway(origin, listenPort);
|
|
7757
|
+
} catch {
|
|
7758
|
+
await this.failGeneration(generation, "gateway_start_failed");
|
|
7759
|
+
return;
|
|
7760
|
+
}
|
|
7761
|
+
if (generation !== this.generation || !this.enabled || this.disposed || this.child === void 0) {
|
|
7762
|
+
await replacement.close();
|
|
7763
|
+
return;
|
|
7764
|
+
}
|
|
7765
|
+
this.gatewayValue = replacement;
|
|
7766
|
+
this.publish({
|
|
7767
|
+
enabled: true,
|
|
7768
|
+
state: "ready",
|
|
7769
|
+
origin
|
|
7770
|
+
});
|
|
7771
|
+
}
|
|
7614
7772
|
async failGeneration(generation, code) {
|
|
7615
7773
|
if (generation !== this.generation) return;
|
|
7616
7774
|
await this.stopProcessAndGateway();
|
|
@@ -7913,7 +8071,9 @@ var CpolarComponentManager = class {
|
|
|
7913
8071
|
const PACKAGE_NAME = "dsh-mobile";
|
|
7914
8072
|
const NPM_LATEST_URL = "https://registry.npmjs.org/dsh-mobile/latest";
|
|
7915
8073
|
const GITHUB_LATEST_URL = "https://github.com/saya-ch/dsh-mobile/releases/latest";
|
|
8074
|
+
const GITHUB_API_LATEST_URL = "https://api.github.com/repos/saya-ch/dsh-mobile/releases/latest";
|
|
7916
8075
|
const GITHUB_RELEASES_URL = "https://github.com/saya-ch/dsh-mobile/releases";
|
|
8076
|
+
const RELEASE_NOTES_MAX_CHARS = 4e3;
|
|
7917
8077
|
const STATUS_CACHE_MS = 6e5;
|
|
7918
8078
|
const REQUEST_TIMEOUT_MS = 8e3;
|
|
7919
8079
|
const UPDATE_TIMEOUT_MS = 12e4;
|
|
@@ -8053,6 +8213,21 @@ async function fetchAndroidVersion(fetcher) {
|
|
|
8053
8213
|
});
|
|
8054
8214
|
return githubReleaseVersion(response.headers.get("location"), response.url);
|
|
8055
8215
|
}
|
|
8216
|
+
/** Best-effort body of the latest GitHub release, trimmed to a bounded size. */
|
|
8217
|
+
async function fetchReleaseNotes(fetcher) {
|
|
8218
|
+
const response = await fetcher(GITHUB_API_LATEST_URL, {
|
|
8219
|
+
headers: {
|
|
8220
|
+
accept: "application/vnd.github+json",
|
|
8221
|
+
"user-agent": "dsh-mobile-release-check"
|
|
8222
|
+
},
|
|
8223
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
8224
|
+
});
|
|
8225
|
+
if (!response.ok) return void 0;
|
|
8226
|
+
const payload = await response.json();
|
|
8227
|
+
if (typeof payload.body !== "string") return void 0;
|
|
8228
|
+
const notes = payload.body.trim();
|
|
8229
|
+
return notes === "" ? void 0 : notes.slice(0, RELEASE_NOTES_MAX_CHARS);
|
|
8230
|
+
}
|
|
8056
8231
|
async function readProfileInstalledVersion(profileDirectory) {
|
|
8057
8232
|
try {
|
|
8058
8233
|
const manifestPath = createRequire(join(profileDirectory, "package.json")).resolve(`${PACKAGE_NAME}/package.json`);
|
|
@@ -8245,9 +8420,14 @@ var PluginReleaseManager = class {
|
|
|
8245
8420
|
async status(force = false) {
|
|
8246
8421
|
if (!force && this.cache !== void 0 && this.cache.expiresAt > this.now()) return this.cache.status;
|
|
8247
8422
|
const updateSupported = isRegistryPluginSpec(this.profileDirectory === void 0 ? void 0 : await profileDependencySpec(this.profileDirectory));
|
|
8248
|
-
const [npmResult, androidResult] = await Promise.allSettled([
|
|
8423
|
+
const [npmResult, androidResult, notesResult] = await Promise.allSettled([
|
|
8424
|
+
fetchNpmVersion(this.fetcher),
|
|
8425
|
+
fetchAndroidVersion(this.fetcher),
|
|
8426
|
+
fetchReleaseNotes(this.fetcher)
|
|
8427
|
+
]);
|
|
8249
8428
|
const latestVersion = npmResult.status === "fulfilled" ? npmResult.value : void 0;
|
|
8250
8429
|
const androidVersion = androidResult.status === "fulfilled" ? androidResult.value : void 0;
|
|
8430
|
+
const releaseNotes = notesResult.status === "fulfilled" ? notesResult.value : void 0;
|
|
8251
8431
|
const comparison = latestVersion === void 0 ? void 0 : comparePluginVersions(latestVersion, this.installedVersion);
|
|
8252
8432
|
const status = Object.freeze({
|
|
8253
8433
|
installedVersion: this.installedVersion,
|
|
@@ -8255,7 +8435,8 @@ var PluginReleaseManager = class {
|
|
|
8255
8435
|
updateAvailable: updateSupported && comparison === 1,
|
|
8256
8436
|
updateSupported,
|
|
8257
8437
|
...androidVersion === void 0 ? {} : { androidVersion },
|
|
8258
|
-
androidDownloadUrl: androidReleaseDownloadUrl(androidVersion)
|
|
8438
|
+
androidDownloadUrl: androidReleaseDownloadUrl(androidVersion),
|
|
8439
|
+
...releaseNotes === void 0 ? {} : { releaseNotes }
|
|
8259
8440
|
});
|
|
8260
8441
|
this.cache = {
|
|
8261
8442
|
expiresAt: this.now() + STATUS_CACHE_MS,
|
|
@@ -8559,9 +8740,28 @@ async function settleCleanupSteps(steps) {
|
|
|
8559
8740
|
if (errors.length === 1 && errors[0] instanceof Error) throw errors[0];
|
|
8560
8741
|
if (errors.length > 0) throw new AggregateError(errors, "DSH Mobile cleanup failed");
|
|
8561
8742
|
}
|
|
8743
|
+
/**
|
|
8744
|
+
* Resolve the DSH launch-token URL the gateway exchanges for its upstream cookie.
|
|
8745
|
+
*
|
|
8746
|
+
* A layer that disables DSH browser authentication — dsh-lan-access with
|
|
8747
|
+
* `noAuth: true` replaces `authenticatedUrl` with one returning the bare origin
|
|
8748
|
+
* — produces a URL with no query string, so it cannot carry a launch token. That
|
|
8749
|
+
* is a legitimate "this upstream needs no browser auth" signal: report no URL and
|
|
8750
|
+
* the gateway proxies without a cookie instead of failing every route with
|
|
8751
|
+
* `upstream_unavailable`. A token-bearing URL keeps the existing exchange, and a
|
|
8752
|
+
* connection service that is absent or returns a malformed URL keeps failing
|
|
8753
|
+
* closed. Only parsing is guarded: a connection service that throws still fails
|
|
8754
|
+
* plugin activation loudly rather than silently proxying without authentication.
|
|
8755
|
+
*/
|
|
8562
8756
|
function upstreamAuthenticatedUrl(ctx, upstreamOrigin) {
|
|
8563
8757
|
const connection = ctx.connection;
|
|
8564
|
-
|
|
8758
|
+
if (typeof connection?.authenticatedUrl !== "function") return void 0;
|
|
8759
|
+
const authenticatedUrl = connection.authenticatedUrl(upstreamOrigin.origin);
|
|
8760
|
+
try {
|
|
8761
|
+
return new URL(authenticatedUrl).search === "" ? void 0 : authenticatedUrl;
|
|
8762
|
+
} catch {
|
|
8763
|
+
return;
|
|
8764
|
+
}
|
|
8565
8765
|
}
|
|
8566
8766
|
function installedDshVersion() {
|
|
8567
8767
|
try {
|