dsh-mobile 0.3.14 → 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 +19 -0
- package/README.en.md +9 -8
- package/README.md +9 -8
- package/SECURITY.md +1 -1
- package/docs/DSH_0.1.5_LAN.md +27 -0
- package/lib/client.js +41 -8
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +13 -0
- package/lib/index.mjs +200 -23
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-layout.js +116 -11
- 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();
|
|
@@ -8582,9 +8740,28 @@ async function settleCleanupSteps(steps) {
|
|
|
8582
8740
|
if (errors.length === 1 && errors[0] instanceof Error) throw errors[0];
|
|
8583
8741
|
if (errors.length > 0) throw new AggregateError(errors, "DSH Mobile cleanup failed");
|
|
8584
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
|
+
*/
|
|
8585
8756
|
function upstreamAuthenticatedUrl(ctx, upstreamOrigin) {
|
|
8586
8757
|
const connection = ctx.connection;
|
|
8587
|
-
|
|
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
|
+
}
|
|
8588
8765
|
}
|
|
8589
8766
|
function installedDshVersion() {
|
|
8590
8767
|
try {
|