dsh-mobile 0.4.7 → 0.5.1

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/lib/index.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { X509Certificate } from "node:crypto";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { ChildProcessWithoutNullStreams } from "node:child_process";
4
+ import { IncomingMessage } from "node:http";
4
5
  import { Readable } from "node:stream";
5
6
  import { Context, Service } from "@deepseek-ai/cordis";
6
7
  import { WebRoute } from "@deepseek-ai/dsh-host-webserver";
@@ -355,6 +356,8 @@ declare class JsonMobileAccessControlStore implements MobileAccessControlStore {
355
356
  private readonly file;
356
357
  private readonly initiallyEnabled;
357
358
  constructor(file: string, initiallyEnabled: boolean);
359
+ /** Avoid writing an absent already-disabled state without changing recovery of existing files. */
360
+ disableIfPresent(): Promise<void>;
358
361
  load(): Promise<MobileAccessControlState>;
359
362
  save(state: MobileAccessControlState): Promise<void>;
360
363
  }
@@ -756,7 +759,7 @@ declare class MobileAccessGateway {
756
759
  blockedUpgradePathReport(): BlockedUpgradePathEntry[];
757
760
  private handleUpgrade;
758
761
  /** Loopback-only DSH WebServer route for opening pairing and managing devices. */
759
- localAdminRoute(prefix?: string): WebRoute;
762
+ localAdminRoute(prefix?: string, authenticateDesktop?: (request: IncomingMessage) => boolean): WebRoute;
760
763
  /** Close listeners and abort all accepted work before resolving teardown. */
761
764
  close(): Promise<void>;
762
765
  private performClose;
package/lib/index.mjs CHANGED
@@ -1078,6 +1078,25 @@ var JsonMobileAccessControlStore = class {
1078
1078
  this.file = file;
1079
1079
  this.initiallyEnabled = initiallyEnabled;
1080
1080
  }
1081
+ /** Avoid writing an absent already-disabled state without changing recovery of existing files. */
1082
+ async disableIfPresent() {
1083
+ try {
1084
+ await lstat(this.file);
1085
+ } catch (error) {
1086
+ if (error.code === "ENOENT") {
1087
+ if (this.initiallyEnabled) await this.save({
1088
+ version: 1,
1089
+ enabled: false
1090
+ });
1091
+ return;
1092
+ }
1093
+ throw error;
1094
+ }
1095
+ await this.save({
1096
+ version: 1,
1097
+ enabled: false
1098
+ });
1099
+ }
1081
1100
  async load() {
1082
1101
  let stat;
1083
1102
  try {
@@ -1222,9 +1241,6 @@ var MobileAccessGatewayController = class {
1222
1241
  return run;
1223
1242
  }
1224
1243
  };
1225
- //#endregion
1226
- //#region src/local-admin-host.ts
1227
- /** Browser-safe desktop-admin Host checks. Do not import Node APIs here. */
1228
1244
  function unwrapBrackets(hostname) {
1229
1245
  return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
1230
1246
  }
@@ -1339,7 +1355,8 @@ function setSecurityHeaders(response, tls, framing = "gateway") {
1339
1355
  "worker-src 'self' blob:",
1340
1356
  ...proxied ? [PROXIED_FRAME_SRC] : []
1341
1357
  ].join("; "));
1342
- response.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=(), usb=()");
1358
+ const microphone = proxied ? "microphone=(self)" : "microphone=()";
1359
+ response.setHeader("Permissions-Policy", `camera=(), ${microphone}, geolocation=(), payment=(), usb=()`);
1343
1360
  response.setHeader("Referrer-Policy", "no-referrer");
1344
1361
  response.setHeader("X-Content-Type-Options", "nosniff");
1345
1362
  response.setHeader("X-Frame-Options", proxied ? "SAMEORIGIN" : "DENY");
@@ -1436,7 +1453,7 @@ function localAuthority(header) {
1436
1453
  }
1437
1454
  }
1438
1455
  /** Protect the inner management route from non-loopback and DNS-rebinding callers. */
1439
- function assertLocalAdminTrust(request, requireBrowserOrigin) {
1456
+ function assertLocalAdminTrust(request, requireBrowserOrigin, authenticateDesktop = () => false) {
1440
1457
  if (request.socket.remoteAddress === void 0 || !isLoopbackAddress(request.socket.remoteAddress)) throw new HttpError(403, "forbidden");
1441
1458
  const host = localAuthority(request.headers.host);
1442
1459
  if (host === void 0 || !isLocalAdminHostname(host.hostname)) throw new HttpError(403, "forbidden");
@@ -1450,7 +1467,8 @@ function assertLocalAdminTrust(request, requireBrowserOrigin) {
1450
1467
  if (error instanceof HttpError) throw error;
1451
1468
  throw new HttpError(403, "forbidden");
1452
1469
  }
1453
- if (requireBrowserOrigin && origin === void 0) throw new HttpError(403, "forbidden");
1470
+ const desktopForward = site === void 0 && request.headers["x-dsh-mobile-desktop-admin"] === "dsh-app://app";
1471
+ if (requireBrowserOrigin && origin === void 0 && !(desktopForward && authenticateDesktop())) throw new HttpError(403, "forbidden");
1454
1472
  if (requireBrowserOrigin && site !== void 0 && site !== "same-origin") throw new HttpError(403, "forbidden");
1455
1473
  }
1456
1474
  //#endregion
@@ -2647,7 +2665,7 @@ const MOBILE_LAYOUT_DEPENDENCY_PROFILES = Object.freeze([Object.freeze({
2647
2665
  ])
2648
2666
  })]);
2649
2667
  const MOBILE_CSRF_FETCH_BOOTSTRAP = `(()=>{const nativeFetch=window.fetch.bind(window);window.fetch=(input,init)=>{const source=input instanceof Request?input:undefined;const method=String(init?.method??source?.method??'GET').toUpperCase();if(method==='GET'||method==='HEAD')return nativeFetch(input,init);const raw=typeof input==='string'?input:input instanceof URL?input.href:source?.url;if(raw===undefined||new URL(raw,location.href).origin!==location.origin)return nativeFetch(input,init);const headers=new Headers(init?.headers??source?.headers);if(!headers.has(${JSON.stringify(CSRF_HEADER)})){const prefix=${JSON.stringify(`${CSRF_COOKIE}=`)};const token=document.cookie.split(';').map(value=>value.trim()).find(value=>value.startsWith(prefix))?.slice(prefix.length);if(token!==undefined)headers.set(${JSON.stringify(CSRF_HEADER)},token)}return nativeFetch(input,{...init,headers})};})();`;
2650
- const MOBILE_AUTHENTICATED_TRANSPORT_BOOTSTRAP = `(()=>{if(window.__DSH_TRANSPORT__!==undefined)throw new Error('DSH Mobile cannot replace an existing transport override');window.__DSH_TRANSPORT__={fetch:(input,init)=>window.fetch(input,init),ownsHost:true}})();`;
2668
+ const MOBILE_AUTHENTICATED_TRANSPORT_BOOTSTRAP = `(()=>{if(window.__DSH_TRANSPORT__!==undefined)throw new Error('DSH Mobile cannot replace an existing transport override');window.__DSH_TRANSPORT__={fetch:(input,init)=>window.fetch(input,init),ownsHost:true},window.__DSH_FILE_UPLOAD__={fetch:(input,init)=>window.fetch(input,init)}})();`;
2651
2669
  /** Use this page's gateway health, rather than OS internet validation, for DSH reconnects. */
2652
2670
  const MOBILE_GATEWAY_REACHABILITY_BOOTSTRAP = `(()=>{
2653
2671
  const view=globalThis;
@@ -5085,14 +5103,14 @@ var MobileAccessGateway = class {
5085
5103
  }
5086
5104
  }
5087
5105
  /** Loopback-only DSH WebServer route for opening pairing and managing devices. */
5088
- localAdminRoute(prefix = LOCAL_ADMIN_PREFIX) {
5106
+ localAdminRoute(prefix = LOCAL_ADMIN_PREFIX, authenticateDesktop) {
5089
5107
  return {
5090
5108
  kind: "prefix",
5091
5109
  path: prefix,
5092
5110
  handler: async (request, response) => {
5093
5111
  try {
5094
5112
  const target = parseRequestTarget(request.url);
5095
- assertLocalAdminTrust(request, request.method === "POST");
5113
+ assertLocalAdminTrust(request, request.method === "POST", () => authenticateDesktop?.(request) === true);
5096
5114
  if (target.search !== "") throw new HttpError(400, "bad_request");
5097
5115
  if (request.method === "GET" && target.decodedPathname === `${prefix}/status`) {
5098
5116
  sendJson(response, 200, {
@@ -9509,6 +9527,10 @@ var TaskEventHub = class {
9509
9527
  };
9510
9528
  //#endregion
9511
9529
  //#region src/diagnostics.ts
9530
+ /** Recognize the active remote-web-ui boot hook without evaluating or modifying it. */
9531
+ function hasCompetingRemoteChannelBoot(rows) {
9532
+ return rows.some((row) => row.kind === "script" && row.text?.includes("__DSH_REMOTE_CHANNEL_BOOT__") === true);
9533
+ }
9512
9534
  const REMOTE_ERROR_GUIDANCE = Object.freeze({
9513
9535
  component_missing: "重新安装完整插件包。",
9514
9536
  funnel_permission_required: "继续完成 Tailscale Funnel 授权。",
@@ -9903,6 +9925,7 @@ async function collectConnectionDiagnostics(snapshot, probes = {}) {
9903
9925
  const remoteProbe = snapshot.remote.provider !== "origin" && snapshot.remote.running && snapshot.remote.state === "ready" && snapshot.remote.origin !== void 0 ? (probes.remote ?? defaultRemoteProbe)(snapshot.remote.origin) : Promise.resolve({ state: "not-applicable" });
9904
9926
  const [firewall, remoteObservation] = await Promise.all([(probes.firewall ?? defaultFirewallProbe())(snapshot.lan.port), remoteProbe]);
9905
9927
  checks.push(check("versions", "ok", "versions-current", "版本兼容", `插件 ${DSH_MOBILE_VERSION},DSH ${snapshot.dshVersion},Android App 最低 ${MINIMUM_ANDROID_APP_VERSION}。`));
9928
+ if (snapshot.competingRemoteChannelBoot === true) checks.push(check("remote-plugin-conflict", "warning", "competing-remote-channel", "第三方远程插件", "检测到另一套远程请求通道脚本;手机页面可能进入该插件自己的配对流程。", "在电脑端关闭 dsh-remote-web-ui 的远程访问后刷新,再使用 DSH Mobile 当前二维码配对;不要混用两种配对码。"));
9906
9929
  if (snapshot.lan.configured === false) checks.push(check("network", "error", "lan-setup-required", "局域网配置", "尚未完成局域网初始化。", "返回局域网页选择网卡并完成配置。"));
9907
9930
  else if (snapshot.lan.networkError !== void 0) checks.push(check("network", "error", "network-unavailable", "局域网网卡", "已保存的网卡当前不可用。", "重新运行 dsh-mobile setup。"));
9908
9931
  else if (snapshot.lan.configuredInterface !== void 0) {
@@ -9973,7 +9996,7 @@ function buildMobileGuide(state) {
9973
9996
  const scriptLine = state.hasCustomJs ? "存在(当前生效的自定义脚本)" : "不存在(无自定义脚本)";
9974
9997
  const extensionLines = state.extensions.length === 0 ? "(无)" : state.extensions.map((entry) => `- ${entry.id}(${entry.name} v${entry.version})`).join("\n");
9975
9998
  const failureLine = state.failedExtensionCount > 0 ? `注意:${state.failedExtensionCount} 个扩展的电脑端 host 激活失败,如改动相关扩展请先检查其 host.mjs 与 extension.json。` : "";
9976
- return `${`## 手机端当前状态(改名前必读,避免覆盖已有定制)
9999
+ return `${`## 手机端当前状态(改动前必读,避免覆盖已有定制)
9977
10000
 
9978
10001
  - 定制目录:${state.directory}(所有改动只允许在这里进行)
9979
10002
  - mobile.css:${styleLine}
@@ -9999,6 +10022,8 @@ const MOBILE_CUSTOMIZATION_GUIDE_BODY = `你在为用户定制 DSH Mobile 的手
9999
10022
  - $DSH_HOME/mobile-access/mobile.js:手机端脚本,用 window.dshMobile.register(({ root }) => { ... }) 把内容挂载到 root,返回清理函数
10000
10023
  - 保存后手机端几秒内自动应用,无需重启
10001
10024
 
10025
+ 动手前先读已有的定制文件。若要调整 DSH 自带的输入框、侧栏或设置等现有界面,先确认当前 DSH 版本与手机页面模式(专用移动页面或 ?frontend=stock),只读核对运行页面的 DOM、属性和样式;本机有同版本前端源码时可参考相应组件,没有源码时可查看已安装的客户端产物,不必下载源码。优先使用扩展 API、插槽和稳定的 data-* 标记,不凭猜测依赖构建类名。只读查看不改变前述写入范围。
10026
+
10002
10027
  2. 电脑端能力 —— 手机需要读电脑文件、执行命令或访问硬件时,创建扩展:
10003
10028
  - 目录:$DSH_HOME/mobile-access/extensions/<id>/,id 用小写字母数字和连字符(如 media-remote)
10004
10029
  - extension.json:{"schemaVersion":1,"id":"<id>","name":"显示名","version":"0.1.0","description":"说明"}
@@ -10015,6 +10040,7 @@ const MOBILE_CUSTOMIZATION_GUIDE_BODY = `你在为用户定制 DSH Mobile 的手
10015
10040
 
10016
10041
  完成前请自检:
10017
10042
  - 改动涉及 mobile.js 或扩展的 mobile.js / host.mjs 时,先做语法检查再保存(如 node --check <file>),确保没有语法错误
10043
+ - 改动 DSH 现有界面后尽可能在实际手机页面核对效果;无法观察运行页面时,明确告诉用户尚未实测
10018
10044
  - 创建或修改扩展后,确认 extension.json 的 schemaVersion 为 1、id 与目录名一致、且 id 只含小写字母数字和连字符
10019
10045
  - 扩展的 host.mjs 若在完成前无法激活,先修正而不是留下损坏的扩展
10020
10046
  - 完成后检查自己实际写入了哪些文件,向用户简要说明改了什么、手机端会有什么变化`;
@@ -11851,6 +11877,87 @@ async function regularFile(file, expectedBytes) {
11851
11877
  throw error;
11852
11878
  }
11853
11879
  }
11880
+ async function installStep(code, operation) {
11881
+ try {
11882
+ return await operation();
11883
+ } catch (error) {
11884
+ if (error instanceof Error && error.message.startsWith("cpolar_")) throw error;
11885
+ throw new Error(code, { cause: error });
11886
+ }
11887
+ }
11888
+ async function pathExists(path) {
11889
+ try {
11890
+ await lstat(path);
11891
+ return true;
11892
+ } catch (error) {
11893
+ if (error.code === "ENOENT") return false;
11894
+ throw error;
11895
+ }
11896
+ }
11897
+ /** Replace only this version's verified binary, retaining the previous directory until promotion succeeds. */
11898
+ async function publishVerifiedCpolarExecutable(extracted, componentRoot, componentStorage, executableName, operations = {}) {
11899
+ if (dirname(componentStorage) !== componentRoot || !inside(componentRoot, componentStorage) || basename(executableName) !== executableName || executableName === "." || executableName === "..") throw new Error("cpolar component storage escaped its private root");
11900
+ const move = operations.move ?? rename;
11901
+ const remove = operations.remove ?? ((path) => rm(path, {
11902
+ recursive: true,
11903
+ force: true
11904
+ }));
11905
+ const candidate = join(componentRoot, `.install-${randomBytes(12).toString("hex")}`);
11906
+ const backup = join(componentRoot, `.previous-${randomBytes(12).toString("hex")}`);
11907
+ let backupContainsPrevious = false;
11908
+ let replacementInstalled = false;
11909
+ try {
11910
+ await mkdir(candidate, {
11911
+ recursive: true,
11912
+ mode: 448
11913
+ });
11914
+ await copyFile(extracted, join(candidate, executableName));
11915
+ await chmod(join(candidate, executableName), 448);
11916
+ if (await pathExists(componentStorage)) {
11917
+ await move(componentStorage, backup);
11918
+ backupContainsPrevious = true;
11919
+ }
11920
+ try {
11921
+ await move(candidate, componentStorage);
11922
+ replacementInstalled = true;
11923
+ } catch (error) {
11924
+ if (backupContainsPrevious) try {
11925
+ await move(backup, componentStorage);
11926
+ backupContainsPrevious = false;
11927
+ } catch (rollbackError) {
11928
+ process.emitWarning(`cpolar previous component remains at ${backup}; automatic rollback failed`, { code: "DSH_MOBILE_CPOLAR_ROLLBACK_FAILED" });
11929
+ throw new Error("cpolar_storage_failed", { cause: new AggregateError([error, rollbackError]) });
11930
+ }
11931
+ throw error;
11932
+ }
11933
+ } finally {
11934
+ try {
11935
+ await remove(candidate);
11936
+ } catch {
11937
+ process.emitWarning(`cpolar temporary component remains at ${candidate}; cleanup failed`, { code: "DSH_MOBILE_CPOLAR_CLEANUP_FAILED" });
11938
+ }
11939
+ if (replacementInstalled && backupContainsPrevious) try {
11940
+ await remove(backup);
11941
+ } catch {
11942
+ process.emitWarning(`cpolar previous component remains at ${backup}; cleanup failed`, { code: "DSH_MOBILE_CPOLAR_CLEANUP_FAILED" });
11943
+ }
11944
+ }
11945
+ }
11946
+ /** Remove only this install's staging directory without replacing a preceding failure. */
11947
+ async function cleanupCpolarInstallStaging(staging, installFailed, remove = (path) => rm(path, {
11948
+ recursive: true,
11949
+ force: true
11950
+ })) {
11951
+ try {
11952
+ await remove(staging);
11953
+ } catch (error) {
11954
+ if (installFailed) {
11955
+ process.emitWarning(`cpolar staging directory remains at ${staging}; cleanup failed`, { code: "DSH_MOBILE_CPOLAR_CLEANUP_FAILED" });
11956
+ return;
11957
+ }
11958
+ throw new Error("cpolar_storage_failed", { cause: error });
11959
+ }
11960
+ }
11854
11961
  async function run(file, args) {
11855
11962
  await new Promise((resolveRun, reject) => {
11856
11963
  execFile(file, [...args], {
@@ -12005,11 +12112,14 @@ var CpolarComponentManager = class {
12005
12112
  return this.enqueue(async () => {
12006
12113
  const release = this.release;
12007
12114
  if (release === void 0) throw new Error("cpolar_component_unsupported");
12008
- await mkdir(this.stagingRoot, {
12009
- recursive: true,
12010
- mode: 448
12115
+ const staging = await installStep("cpolar_storage_failed", async () => {
12116
+ await mkdir(this.stagingRoot, {
12117
+ recursive: true,
12118
+ mode: 448
12119
+ });
12120
+ return mkdtemp(join(this.stagingRoot, "install-"));
12011
12121
  });
12012
- const staging = await mkdtemp(join(this.stagingRoot, "install-"));
12122
+ let installFailed = false;
12013
12123
  try {
12014
12124
  const controller = new AbortController();
12015
12125
  const timeout = setTimeout(() => {
@@ -12018,38 +12128,35 @@ var CpolarComponentManager = class {
12018
12128
  timeout.unref();
12019
12129
  let bytes;
12020
12130
  try {
12021
- bytes = await this.fetchArtifact(release.downloadUrl, controller.signal);
12131
+ bytes = await installStep("cpolar_download_failed", () => this.fetchArtifact(release.downloadUrl, controller.signal));
12132
+ } catch (error) {
12133
+ if (controller.signal.aborted) throw new Error("cpolar_download_timeout", { cause: error });
12134
+ throw error;
12022
12135
  } finally {
12023
12136
  clearTimeout(timeout);
12024
12137
  }
12025
12138
  if (createHash("sha256").update(bytes).digest("hex") !== release.downloadSha256) throw new Error("cpolar_download_hash_mismatch");
12026
12139
  const archive = join(staging, release.archiveKind === "tar.gz" ? "cpolar.tar.gz" : "cpolar.zip");
12027
- await writeFile(archive, bytes, {
12140
+ await installStep("cpolar_storage_failed", () => writeFile(archive, bytes, {
12028
12141
  flag: "wx",
12029
12142
  mode: 384
12030
- });
12031
- await this.extractArtifact(archive, staging);
12143
+ }));
12144
+ await installStep("cpolar_extract_failed", () => this.extractArtifact(archive, staging));
12032
12145
  const extracted = join(staging, release.executableName);
12033
- if (!await regularFile(extracted, release.executableBytes) || await sha256(extracted) !== release.executableSha256) throw new Error("cpolar_executable_hash_mismatch");
12034
- const candidate = join(this.componentRoot, `.install-${randomBytes(12).toString("hex")}`);
12035
- await mkdir(candidate, {
12036
- recursive: true,
12037
- mode: 448
12038
- });
12039
- await copyFile(extracted, join(candidate, release.executableName));
12040
- await chmod(join(candidate, release.executableName), 448);
12041
- await rm(this.componentStorage, {
12042
- recursive: true,
12043
- force: true
12044
- });
12045
- await rename(candidate, this.componentStorage);
12146
+ if (!await installStep("cpolar_extract_failed", async () => await regularFile(extracted, release.executableBytes) && await sha256(extracted) === release.executableSha256)) throw new Error("cpolar_executable_hash_mismatch");
12147
+ await installStep("cpolar_storage_failed", () => publishVerifiedCpolarExecutable(extracted, this.componentRoot, this.componentStorage, release.executableName));
12046
12148
  this.installed = true;
12047
12149
  this.errorCode = void 0;
12150
+ } catch (error) {
12151
+ installFailed = true;
12152
+ try {
12153
+ this.installed = await regularFile(this.executable, release.executableBytes) && await sha256(this.executable) === release.executableSha256;
12154
+ } catch (_inspectionError) {
12155
+ this.installed = false;
12156
+ }
12157
+ throw error;
12048
12158
  } finally {
12049
- await rm(staging, {
12050
- recursive: true,
12051
- force: true
12052
- });
12159
+ await cleanupCpolarInstallStaging(staging, installFailed);
12053
12160
  }
12054
12161
  });
12055
12162
  }
@@ -13223,6 +13330,7 @@ async function apply(ctx, config) {
13223
13330
  } : void 0;
13224
13331
  return collectConnectionDiagnostics({
13225
13332
  dshVersion,
13333
+ competingRemoteChannelBoot: hasCompetingRemoteChannelBoot(ctx.webServer.collectIndexInjections?.() ?? []),
13226
13334
  lan: {
13227
13335
  configured: loaded.kind !== "unconfigured" || preparedLanSetup !== void 0,
13228
13336
  running: lanController.isRunning(),
@@ -13246,13 +13354,17 @@ async function apply(ctx, config) {
13246
13354
  }
13247
13355
  }, pinnedFrpProbe === void 0 ? {} : { remote: pinnedFrpProbe });
13248
13356
  };
13357
+ const authenticateDesktopAdmin = (request) => {
13358
+ const connection = ctx.connection;
13359
+ return typeof connection?.requestRejection === "function" && connection.requestRejection(request) === void 0;
13360
+ };
13249
13361
  const adminRoute = {
13250
13362
  kind: "prefix",
13251
13363
  path: LOCAL_ADMIN_PREFIX,
13252
13364
  handler: async (request, response) => {
13253
13365
  try {
13254
13366
  const target = parseRequestTarget(request.url);
13255
- assertLocalAdminTrust(request, request.method === "POST");
13367
+ assertLocalAdminTrust(request, request.method === "POST", () => authenticateDesktopAdmin(request));
13256
13368
  if (target.search !== "") throw new HttpError(400, "bad_request");
13257
13369
  const lanControl = target.decodedPathname === `/api/mobile-access/control` || target.decodedPathname === `/api/mobile-access/lan/control`;
13258
13370
  if (request.method === "GET" && lanControl) {
@@ -13341,7 +13453,15 @@ async function apply(ctx, config) {
13341
13453
  }
13342
13454
  if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/cpolar/component/install`) {
13343
13455
  if ((await readJsonObject(request, 4096)).confirm !== true) throw new HttpError(400, "bad_request");
13344
- await remoteProviders.mutate(async () => cpolarComponent.install());
13456
+ logger.info("cpolar component install started");
13457
+ try {
13458
+ await remoteProviders.mutate(async () => cpolarComponent.install());
13459
+ logger.info("cpolar component install completed");
13460
+ } catch (error) {
13461
+ const cause = error instanceof Error && error.cause instanceof Error ? error.cause : void 0;
13462
+ logger.error("cpolar component install failed: %s%s", error instanceof Error ? error.stack ?? error.message : String(error), cause === void 0 ? "" : `; cause: ${cause.message}`);
13463
+ throw error;
13464
+ }
13345
13465
  sendJson(response, 200, remotePayload(), false);
13346
13466
  return;
13347
13467
  }
@@ -13584,18 +13704,18 @@ async function apply(ctx, config) {
13584
13704
  if (target.decodedPathname.startsWith(`/api/mobile-access/remote/`)) {
13585
13705
  const active = remoteController().gateway();
13586
13706
  if (active === void 0) throw new HttpError(409, "gateway_stopped");
13587
- await active.localAdminRoute(`${LOCAL_ADMIN_PREFIX}/remote`).handler(request, response);
13707
+ await active.localAdminRoute(`${LOCAL_ADMIN_PREFIX}/remote`, authenticateDesktopAdmin).handler(request, response);
13588
13708
  return;
13589
13709
  }
13590
13710
  if (target.decodedPathname.startsWith(`/api/mobile-access/lan/`)) {
13591
13711
  const active = lanGateway;
13592
13712
  if (active === void 0) throw new HttpError(409, "gateway_stopped");
13593
- await active.localAdminRoute(`${LOCAL_ADMIN_PREFIX}/lan`).handler(request, response);
13713
+ await active.localAdminRoute(`${LOCAL_ADMIN_PREFIX}/lan`, authenticateDesktopAdmin).handler(request, response);
13594
13714
  return;
13595
13715
  }
13596
13716
  const active = lanGateway;
13597
13717
  if (active === void 0) throw new HttpError(409, "gateway_stopped");
13598
- await active.localAdminRoute().handler(request, response);
13718
+ await active.localAdminRoute(LOCAL_ADMIN_PREFIX, authenticateDesktopAdmin).handler(request, response);
13599
13719
  } catch (error) {
13600
13720
  const mapped = mapAdminError(error);
13601
13721
  if (response.headersSent) response.destroy();
@@ -13607,7 +13727,7 @@ async function apply(ctx, config) {
13607
13727
  const unregister = ctx.webServer.register(adminRoute);
13608
13728
  const disposeMobileCommand = ctx.commands.register({
13609
13729
  name: "mobile",
13610
- description: "按需求修改 DSH Mobile 的手机端界面或添加电脑端能力",
13730
+ description: "定制手机界面或添加电脑能力",
13611
13731
  input: { hint: "<要做什么>" },
13612
13732
  handler: async ({ agent, rawInput }) => {
13613
13733
  const task = rawInput.trim();
@@ -13653,10 +13773,7 @@ async function apply(ctx, config) {
13653
13773
  frp: frpStore,
13654
13774
  origin: originStore
13655
13775
  };
13656
- await Promise.all(Object.keys(stores).filter((provider) => provider !== remoteProviders.selected).map((provider) => stores[provider].save({
13657
- version: 1,
13658
- enabled: false
13659
- })));
13776
+ await Promise.all(Object.keys(stores).filter((provider) => provider !== remoteProviders.selected).map((provider) => stores[provider].disableIfPresent()));
13660
13777
  for (const provider of REMOTE_PROVIDERS) await remoteControllers[provider].initialize();
13661
13778
  const lanDiscovery = lanGateway?.discoveryStatus();
13662
13779
  if (lanDiscovery !== void 0 && !lanDiscovery.broadcast) logger.warn("broadcast discovery is unavailable (%s); mDNS is still published, so pair manually or free the UDP port", lanDiscovery.errorCode ?? "unknown_cause");