dsh-mobile 0.6.6 → 0.6.7
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 +10 -0
- package/CONTRIBUTORS.md +8 -0
- package/README.en.md +5 -3
- package/README.md +5 -3
- package/docs/CPOLAR_SUBDOMAIN.en.md +81 -0
- package/docs/CPOLAR_SUBDOMAIN.md +81 -0
- package/docs/README.md +1 -0
- package/docs/SKIN_COMPATIBILITY.en.md +1 -1
- package/docs/SKIN_COMPATIBILITY.md +1 -1
- package/lib/cli.js +2 -0
- package/lib/cli.js.map +1 -1
- package/lib/client.js +240 -25
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +13 -2
- package/lib/index.mjs +89 -59
- package/lib/index.mjs.map +1 -1
- package/package.json +6 -1
- package/packages/question-fixes/package.json +1 -1
package/lib/index.d.mts
CHANGED
|
@@ -333,6 +333,15 @@ interface PluginConfig {
|
|
|
333
333
|
keyFile?: string;
|
|
334
334
|
caFile?: string;
|
|
335
335
|
};
|
|
336
|
+
/**
|
|
337
|
+
* Reserved cpolar subdomain passed through as `cpolar http -subdomain`.
|
|
338
|
+
*
|
|
339
|
+
* A compatible account reservation and region are required. Unset or blank values
|
|
340
|
+
* keep provider-selected allocation; credentials may still expire or be revoked.
|
|
341
|
+
*/
|
|
342
|
+
cpolarSubdomain?: string;
|
|
343
|
+
/** cpolar region for `-region`; use the region chosen when reserving the subdomain. */
|
|
344
|
+
cpolarRegion?: string;
|
|
336
345
|
pairingTtlMs?: number;
|
|
337
346
|
deviceTtlMs?: number;
|
|
338
347
|
sessionTtlMs?: number;
|
|
@@ -664,8 +673,8 @@ declare class ExtensionWorkerHost {
|
|
|
664
673
|
private cancelStream;
|
|
665
674
|
}
|
|
666
675
|
//#endregion
|
|
667
|
-
//#region src/
|
|
668
|
-
/** Maximum sizes enforced
|
|
676
|
+
//#region src/extension-limits.d.ts
|
|
677
|
+
/** Maximum sizes enforced when reading local extension files. */
|
|
669
678
|
export declare const EXTENSION_LIMITS: Readonly<{
|
|
670
679
|
manifest: number;
|
|
671
680
|
script: number;
|
|
@@ -675,6 +684,8 @@ export declare const EXTENSION_LIMITS: Readonly<{
|
|
|
675
684
|
assetBytes: number;
|
|
676
685
|
assetDepth: 8;
|
|
677
686
|
}>;
|
|
687
|
+
//#endregion
|
|
688
|
+
//#region src/extensions.d.ts
|
|
678
689
|
/** A controlled business failure returned by an extension action or route. */
|
|
679
690
|
export declare class MobileExtensionError extends Error {
|
|
680
691
|
readonly code: string;
|
package/lib/index.mjs
CHANGED
|
@@ -956,6 +956,8 @@ const Config = z.object({
|
|
|
956
956
|
keyFile: z.string(),
|
|
957
957
|
caFile: z.string()
|
|
958
958
|
}),
|
|
959
|
+
cpolarSubdomain: z.string(),
|
|
960
|
+
cpolarRegion: z.string(),
|
|
959
961
|
pairingTtlMs: z.natural(),
|
|
960
962
|
deviceTtlMs: z.natural(),
|
|
961
963
|
sessionTtlMs: z.natural(),
|
|
@@ -1991,6 +1993,13 @@ function attributeValue(tag, name) {
|
|
|
1991
1993
|
function commentRanges(html) {
|
|
1992
1994
|
return [...html.matchAll(/<!--[\s\S]*?-->/gu)].map((match) => [match.index, match.index + match[0].length]);
|
|
1993
1995
|
}
|
|
1996
|
+
/** Mark an authenticated Gateway document without changing its layout, transport or module selection. */
|
|
1997
|
+
function ensureMobileGatewayMarker(html) {
|
|
1998
|
+
const comments = commentRanges(html);
|
|
1999
|
+
const first = [...html.matchAll(/<script\b[^>]*>/giu)].find((match) => !comments.some(([start, end]) => match.index >= start && match.index < end));
|
|
2000
|
+
if (first === void 0) return html;
|
|
2001
|
+
return `${html.slice(0, first.index)}<meta name="dsh-mobile-gateway" content="authenticated">${html.slice(first.index)}`;
|
|
2002
|
+
}
|
|
1994
2003
|
/**
|
|
1995
2004
|
* Load the compatibility bundle synchronously, before the first DSH script.
|
|
1996
2005
|
*
|
|
@@ -2010,6 +2019,16 @@ function ensureMobileCompatibility(html) {
|
|
|
2010
2019
|
const script = `<script src="${MOBILE_COMPAT_PATH}"${attribute}><\/script>`;
|
|
2011
2020
|
return `${html.slice(0, first.index)}${script}${html.slice(first.index)}`;
|
|
2012
2021
|
}
|
|
2022
|
+
/** Maximum sizes enforced when reading local extension files. */
|
|
2023
|
+
const EXTENSION_LIMITS = Object.freeze({
|
|
2024
|
+
manifest: 65536,
|
|
2025
|
+
script: 1048576,
|
|
2026
|
+
css: 524288,
|
|
2027
|
+
asset: 8388608,
|
|
2028
|
+
assetFiles: 256,
|
|
2029
|
+
assetBytes: 33554432,
|
|
2030
|
+
assetDepth: 8
|
|
2031
|
+
});
|
|
2013
2032
|
//#endregion
|
|
2014
2033
|
//#region src/http-cache.ts
|
|
2015
2034
|
/** Entity-tag handling for authenticated GET and HEAD cache validation. */
|
|
@@ -3066,16 +3085,6 @@ var WorkerStreamBridge = class extends Readable {
|
|
|
3066
3085
|
};
|
|
3067
3086
|
//#endregion
|
|
3068
3087
|
//#region src/extensions.ts
|
|
3069
|
-
/** Maximum sizes enforced at the local-extension filesystem boundary. */
|
|
3070
|
-
const EXTENSION_LIMITS = Object.freeze({
|
|
3071
|
-
manifest: 65536,
|
|
3072
|
-
script: 1048576,
|
|
3073
|
-
css: 524288,
|
|
3074
|
-
asset: 8388608,
|
|
3075
|
-
assetFiles: 256,
|
|
3076
|
-
assetBytes: 33554432,
|
|
3077
|
-
assetDepth: 8
|
|
3078
|
-
});
|
|
3079
3088
|
/** A misbehaving host activation must not wedge the local watcher forever. */
|
|
3080
3089
|
const HOST_ACTIVATION_TIMEOUT_MS = 5e3;
|
|
3081
3090
|
/** The previous Host outlives the hidden-page refresh interval and one timed refresh. */
|
|
@@ -4811,7 +4820,7 @@ function rewriteMobileIndexWithBatch(html, options = {}) {
|
|
|
4811
4820
|
const replacement = `${MOBILE_GATEWAY_REACHABILITY_BOOTSTRAP}${remoteSettings ? MOBILE_AUTHENTICATED_TRANSPORT_BOOTSTRAP : ""}${mobileCsrfFetchBootstrap(options.csrfCookie ?? "dsh_ma_csrf")}window.__DSH_MOBILE_FRONTEND__="dedicated";${plan.assignment}${JSON.stringify(plan.parsed)};`;
|
|
4812
4821
|
const rewritten = `${html.slice(0, plan.replaceStart)}${replacement}${html.slice(plan.replaceEnd)}`;
|
|
4813
4822
|
return Object.freeze({
|
|
4814
|
-
html: ensureMobileViewport(ensureMobileCompatibility(rewriteMobileApplicationPreloads(rewritten, preloadReplacements))),
|
|
4823
|
+
html: ensureMobileGatewayMarker(ensureMobileViewport(ensureMobileCompatibility(rewriteMobileApplicationPreloads(rewritten, preloadReplacements)))),
|
|
4815
4824
|
batches
|
|
4816
4825
|
});
|
|
4817
4826
|
}
|
|
@@ -5986,8 +5995,9 @@ var MobileAccessGateway = class {
|
|
|
5986
5995
|
const stockFrontend = new URL(target.raw, this.address().origin).searchParams.get("frontend") === "stock";
|
|
5987
5996
|
const acceptsHtml = request.headers.accept?.split(",").some((value) => value.trim().split(";", 1)[0] === "text/html") ?? false;
|
|
5988
5997
|
const frontendDocument = target.decodedPathname === "/" || target.decodedPathname === "/index.html";
|
|
5989
|
-
if (request.method === "GET" && frontendDocument && acceptsHtml
|
|
5990
|
-
|
|
5998
|
+
if (request.method === "GET" && frontendDocument && acceptsHtml) {
|
|
5999
|
+
if (stockFrontend && target.decodedPathname === "/") request.url = "/";
|
|
6000
|
+
await this.proxyMobileIndex(request, response, authorization, stockFrontend);
|
|
5991
6001
|
return;
|
|
5992
6002
|
}
|
|
5993
6003
|
if (stockFrontend && target.decodedPathname === "/") request.url = "/";
|
|
@@ -6378,6 +6388,8 @@ var MobileAccessGateway = class {
|
|
|
6378
6388
|
signal.throwIfAborted();
|
|
6379
6389
|
if (upstreamCookie !== void 0) headers.cookie = upstreamCookie;
|
|
6380
6390
|
headers["accept-encoding"] = "identity";
|
|
6391
|
+
delete headers["if-none-match"];
|
|
6392
|
+
delete headers["if-modified-since"];
|
|
6381
6393
|
const proxied = await new Promise((resolve, reject) => {
|
|
6382
6394
|
upstreamRequest = request({
|
|
6383
6395
|
protocol: "http:",
|
|
@@ -6417,7 +6429,7 @@ var MobileAccessGateway = class {
|
|
|
6417
6429
|
upstreamRequest?.destroy();
|
|
6418
6430
|
}
|
|
6419
6431
|
}
|
|
6420
|
-
async proxyMobileIndex(request, response, authorization) {
|
|
6432
|
+
async proxyMobileIndex(request, response, authorization, stockFrontend = false) {
|
|
6421
6433
|
const holder = {};
|
|
6422
6434
|
const operation = this.allocateRequest(authorization, response, holder);
|
|
6423
6435
|
const onClose = () => {
|
|
@@ -6427,50 +6439,53 @@ var MobileAccessGateway = class {
|
|
|
6427
6439
|
response.once("close", onClose);
|
|
6428
6440
|
try {
|
|
6429
6441
|
const index = await this.fetchAuthenticatedUpstreamIndex(operation.signal, request);
|
|
6430
|
-
const preference = await this.clientModulePreferences.read(authorization.deviceId);
|
|
6431
|
-
const excludedClientModules = preference.excludedClientModules;
|
|
6432
6442
|
let body;
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
const
|
|
6436
|
-
const
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
const
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6443
|
+
if (stockFrontend) body = Buffer.from(ensureMobileGatewayMarker(index.html));
|
|
6444
|
+
else {
|
|
6445
|
+
const preference = await this.clientModulePreferences.read(authorization.deviceId);
|
|
6446
|
+
const excludedClientModules = preference.excludedClientModules;
|
|
6447
|
+
try {
|
|
6448
|
+
const html = index.html;
|
|
6449
|
+
const plan = parseMobileBootPlan(html, excludedClientModules);
|
|
6450
|
+
const options = plan.rewrittenBatchEntries === void 0 ? { excludedClientModules } : {
|
|
6451
|
+
...await this.resolveMobileBootSizes(plan.rewrittenBatchEntries.flatMap((batch) => batch.entries), operation.signal),
|
|
6452
|
+
excludedClientModules
|
|
6453
|
+
};
|
|
6454
|
+
const cookies = browserAuthCookies(request.headers.cookie);
|
|
6455
|
+
const csrfCookie = this.tlsEnabled && cookies.has("__Host-dsh_ma_session") ? HOST_CSRF_COOKIE : CSRF_COOKIE;
|
|
6456
|
+
const rewritten = rewriteMobileIndexWithBatch(html, {
|
|
6457
|
+
...options,
|
|
6458
|
+
csrfCookie
|
|
6459
|
+
});
|
|
6460
|
+
for (const batch of rewritten.batches) this.rememberMobileBootBatch(batch);
|
|
6461
|
+
body = Buffer.from(rewritten.html);
|
|
6462
|
+
} catch (error) {
|
|
6463
|
+
if (error instanceof ExcludedClientModulesError) {
|
|
6464
|
+
const detail = error.message.replace(/[\r\n]/gu, " ").slice(0, 512);
|
|
6465
|
+
if (detail !== this.lastModuleExclusionError) {
|
|
6466
|
+
this.lastModuleExclusionError = detail;
|
|
6467
|
+
process.emitWarning(detail, { code: "DSH_MOBILE_MODULE_EXCLUSION_INVALID" });
|
|
6468
|
+
}
|
|
6469
|
+
if (preference.source === "device" || preference.source === "computer") {
|
|
6470
|
+
operation.signal.throwIfAborted();
|
|
6471
|
+
const locale = resolveAuthPageLocale(headerValue(request.headers, "accept-language"));
|
|
6472
|
+
const recovery = Buffer.from(renderClientModuleRecoveryPage(locale));
|
|
6473
|
+
setSecurityHeaders(response, this.tlsEnabled);
|
|
6474
|
+
response.writeHead(200, {
|
|
6475
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
6476
|
+
"Content-Length": recovery.byteLength
|
|
6477
|
+
});
|
|
6478
|
+
response.end(recovery);
|
|
6479
|
+
return;
|
|
6480
|
+
}
|
|
6481
|
+
sendJson(response, 409, {
|
|
6482
|
+
error: "excluded_client_modules_invalid",
|
|
6483
|
+
detail
|
|
6484
|
+
}, this.tlsEnabled);
|
|
6465
6485
|
return;
|
|
6466
6486
|
}
|
|
6467
|
-
|
|
6468
|
-
error: "excluded_client_modules_invalid",
|
|
6469
|
-
detail
|
|
6470
|
-
}, this.tlsEnabled);
|
|
6471
|
-
return;
|
|
6487
|
+
throw new HttpError(502, "upstream_unavailable");
|
|
6472
6488
|
}
|
|
6473
|
-
throw new HttpError(502, "upstream_unavailable");
|
|
6474
6489
|
}
|
|
6475
6490
|
operation.signal.throwIfAborted();
|
|
6476
6491
|
const headers = sanitizeResponseHeaders(index.headers, this.config.upstreamOrigin);
|
|
@@ -13281,11 +13296,11 @@ async function collectConnectionDiagnostics(snapshot, probes = {}) {
|
|
|
13281
13296
|
//#region src/mobile-guide.ts
|
|
13282
13297
|
/** Compose the guide with the current customization state injected. */
|
|
13283
13298
|
function buildMobileGuide(state) {
|
|
13284
|
-
const styleLine = state.hasCustomCss ? "
|
|
13285
|
-
const scriptLine = state.hasCustomJs ? "
|
|
13299
|
+
const styleLine = state.hasCustomCss ? "存在(已检测到文件,需在已配对页面验证加载)" : "不存在(使用内置默认样式)";
|
|
13300
|
+
const scriptLine = state.hasCustomJs ? "存在(已检测到文件,需在已配对页面验证加载)" : "不存在(无自定义脚本)";
|
|
13286
13301
|
const extensionLines = state.extensions.length === 0 ? "(无)" : state.extensions.map((entry) => `- ${entry.id}(${entry.name} v${entry.version})${entry.executionMode === void 0 ? "" : ` · ${entry.executionMode}${entry.executionState === "unavailable" ? " · 当前不可用,需要显式重启扩展" : ""}`}`).join("\n");
|
|
13287
13302
|
const failureLine = state.failedExtensionCount > 0 ? `注意:${state.failedExtensionCount} 个扩展的电脑端 host 激活失败,如改动相关扩展请先检查其 host.mjs 与 extension.json。` : "";
|
|
13288
|
-
return `${`##
|
|
13303
|
+
return `${`## 本机定制文件与扩展状态(改动前必读,避免覆盖已有定制)
|
|
13289
13304
|
|
|
13290
13305
|
- 定制目录:${state.directory}(所有改动只允许在这里进行)
|
|
13291
13306
|
- mobile.css:${styleLine}
|
|
@@ -13309,7 +13324,9 @@ const MOBILE_CUSTOMIZATION_GUIDE_BODY = `你在为用户定制 DSH Mobile 的手
|
|
|
13309
13324
|
1. 界面与交互 —— 只改外观和交互,不需要碰电脑的文件或程序:
|
|
13310
13325
|
- $DSH_HOME/mobile-access/mobile.css:手机端样式
|
|
13311
13326
|
- $DSH_HOME/mobile-access/mobile.js:手机端脚本,用 window.dshMobile.register(({ root }) => { ... }) 把内容挂载到 root,返回清理函数
|
|
13312
|
-
-
|
|
13327
|
+
- 保存后已配对的 DSH Mobile 页面会自动检查并应用,无需重启;文件存在不表示某个页面已成功加载
|
|
13328
|
+
|
|
13329
|
+
定制资源通过 DSH Mobile Gateway 的移动配对会话认证。普通 DSH 网页的登录凭据不等于移动配对凭据;直接打开普通 DSH 地址或仅用手机视口预览,不能据此验证 mobile.css / mobile.js。若扩展 manifest 返回 401,页面不会继续加载定制样式与脚本;先确认是通过“移动访问”配对入口进入,并检查实际请求与页面中的定制样式,不要盲改 CSS 或放松认证。核对 Cookie 时只确认是否存在,不读取或公开其值。
|
|
13313
13330
|
|
|
13314
13331
|
动手前先读已有的定制文件。若要调整 DSH 自带的输入框、侧栏或设置等现有界面,先确认当前 DSH 版本与手机页面模式(专用移动页面或 ?frontend=stock),只读核对运行页面的 DOM、属性和样式;本机有同版本前端源码时可参考相应组件,没有源码时可查看已安装的客户端产物,不必下载源码。优先使用扩展 API、插槽和稳定的 data-* 标记,不凭猜测依赖构建类名。只读查看不改变前述写入范围。
|
|
13315
13332
|
|
|
@@ -14824,6 +14841,7 @@ const CPOLAR_HOST_SUFFIXES = Object.freeze([
|
|
|
14824
14841
|
".cpolar.top",
|
|
14825
14842
|
".cpolar.com"
|
|
14826
14843
|
]);
|
|
14844
|
+
const CPOLAR_SUBDOMAIN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$/u;
|
|
14827
14845
|
function publicStatus(status) {
|
|
14828
14846
|
return Object.freeze({
|
|
14829
14847
|
enabled: status.enabled,
|
|
@@ -14918,7 +14936,16 @@ var CpolarController = class {
|
|
|
14918
14936
|
constructor(options) {
|
|
14919
14937
|
this.options = options;
|
|
14920
14938
|
if (!isAbsolute(options.executable) || !isAbsolute(options.configFile)) throw new Error("cpolar paths must be absolute");
|
|
14921
|
-
|
|
14939
|
+
const { subdomain: rawSubdomain, region: rawRegion, ...resources } = options;
|
|
14940
|
+
const subdomain = rawSubdomain?.trim() || void 0;
|
|
14941
|
+
const region = rawRegion?.trim() || void 0;
|
|
14942
|
+
if (region !== void 0 && !/^[a-z][a-z0-9_]{0,31}$/u.test(region)) throw new Error("cpolar region is invalid");
|
|
14943
|
+
if (subdomain !== void 0 && !CPOLAR_SUBDOMAIN_PATTERN.test(subdomain)) throw new Error("cpolar subdomain is invalid");
|
|
14944
|
+
this.options = {
|
|
14945
|
+
...resources,
|
|
14946
|
+
...subdomain === void 0 ? {} : { subdomain },
|
|
14947
|
+
...region === void 0 ? {} : { region }
|
|
14948
|
+
};
|
|
14922
14949
|
}
|
|
14923
14950
|
/** Restore the remembered cpolar switch independently from LAN and Funnel state. */
|
|
14924
14951
|
async initialize(initialState) {
|
|
@@ -15068,6 +15095,7 @@ var CpolarController = class {
|
|
|
15068
15095
|
const args = [
|
|
15069
15096
|
"http",
|
|
15070
15097
|
`-config=${resolve(this.options.configFile)}`,
|
|
15098
|
+
...this.options.subdomain === void 0 ? [] : [`-subdomain=${this.options.subdomain}`],
|
|
15071
15099
|
...this.options.region === void 0 ? [] : [`-region=${this.options.region}`],
|
|
15072
15100
|
"-inspect-addr=false",
|
|
15073
15101
|
"-redirect-https=true",
|
|
@@ -17271,6 +17299,8 @@ async function apply(ctx, config) {
|
|
|
17271
17299
|
store: cpolarStore,
|
|
17272
17300
|
executable: cpolarComponent.executable,
|
|
17273
17301
|
configFile: cpolarComponent.configFile,
|
|
17302
|
+
...config.cpolarSubdomain === void 0 ? {} : { subdomain: config.cpolarSubdomain },
|
|
17303
|
+
...config.cpolarRegion === void 0 ? {} : { region: config.cpolarRegion },
|
|
17274
17304
|
createGateway: createRemoteGateway
|
|
17275
17305
|
}),
|
|
17276
17306
|
cloudflared: new CloudflaredController({
|