dsh-mobile 0.8.1 → 0.8.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/CONTRIBUTORS.md +30 -2
  3. package/README.en.md +7 -5
  4. package/README.md +7 -5
  5. package/SECURITY.md +1 -1
  6. package/THIRD_PARTY_NOTICES.md +1 -1
  7. package/apps/mobile/README.md +8 -4
  8. package/apps/mobile/README.zh-CN.md +8 -4
  9. package/apps/mobile/release.json +3 -3
  10. package/docs/ATTACH_EXISTING_FRPS.en.md +2 -0
  11. package/docs/ATTACH_EXISTING_FRPS.md +2 -0
  12. package/docs/CLIENT_MODULES.en.md +6 -2
  13. package/docs/CLIENT_MODULES.md +6 -2
  14. package/docs/CONNECTION_DIAGNOSTICS.en.md +56 -0
  15. package/docs/CONNECTION_DIAGNOSTICS.md +56 -0
  16. package/docs/IROH.en.md +7 -1
  17. package/docs/IROH.md +7 -1
  18. package/docs/MOBILE_PLUGIN_ADAPTATION.en.md +1 -1
  19. package/docs/MOBILE_PLUGIN_ADAPTATION.md +1 -1
  20. package/docs/README.md +1 -0
  21. package/docs/SKIN_COMPATIBILITY.en.md +1 -1
  22. package/docs/SKIN_COMPATIBILITY.md +1 -1
  23. package/docs/SLOW_CONNECTIONS.en.md +6 -2
  24. package/docs/SLOW_CONNECTIONS.md +6 -2
  25. package/lib/cli.js +6 -12
  26. package/lib/cli.js.map +1 -1
  27. package/lib/client.js +772 -248
  28. package/lib/client.js.map +1 -1
  29. package/lib/extension-worker-runtime.mjs +4 -8
  30. package/lib/extension-worker-runtime.mjs.map +1 -1
  31. package/lib/index.d.mts +19 -89
  32. package/lib/index.mjs +397 -273
  33. package/lib/index.mjs.map +1 -1
  34. package/lib/mobile-compat.js +2 -2
  35. package/lib/mobile-compat.js.map +1 -1
  36. package/lib/mobile-layout.js +116 -33
  37. package/lib/mobile-layout.js.map +1 -1
  38. package/package.json +9 -7
  39. package/packages/question-fixes/lib/client.js +1 -2
  40. package/packages/question-fixes/lib/index.mjs +1 -2
  41. package/packages/question-fixes/package.json +1 -1
package/lib/index.mjs CHANGED
@@ -28,7 +28,7 @@ import { lookup } from "node:dns/promises";
28
28
  import { constants } from "node:fs";
29
29
  import { z as z$1 } from "zod";
30
30
  import webPush from "web-push";
31
- //#region src/access.ts
31
+ // src/access.ts
32
32
  /** Stable error categories converted to deliberately terse HTTP responses. */
33
33
  var AccessError = class extends Error {
34
34
  status;
@@ -466,8 +466,7 @@ var AccessController = class {
466
466
  });
467
467
  }
468
468
  };
469
- //#endregion
470
- //#region src/ip.ts
469
+ // src/ip.ts
471
470
  /**
472
471
  * Browser-safe IP literal classification.
473
472
  *
@@ -548,8 +547,7 @@ function isIP$1(value) {
548
547
  if (leftGroups < 0 || rightGroups < 0) return 0;
549
548
  return leftGroups + rightGroups >= 8 ? 0 : 6;
550
549
  }
551
- //#endregion
552
- //#region src/network.ts
550
+ // src/network.ts
553
551
  function parseIpv4$1(address) {
554
552
  const parts = address.split(".");
555
553
  if (parts.length !== 4) throw new Error(`invalid IPv4 address ${JSON.stringify(address)}`);
@@ -826,8 +824,7 @@ async function probeTcpReachable(host, port, timeoutMs = 1500) {
826
824
  });
827
825
  });
828
826
  }
829
- //#endregion
830
- //#region src/websocket-paths.ts
827
+ // src/websocket-paths.ts
831
828
  /** Cap the admin-approved extra WebSocket upgrade paths (exact pathnames). */
832
829
  const MAX_EXTRA_WEBSOCKET_PATHS = 16;
833
830
  const MAX_WEBSOCKET_PATH_LENGTH = 256;
@@ -932,8 +929,7 @@ var WebSocketPathStore = class {
932
929
  return this.list();
933
930
  }
934
931
  };
935
- //#endregion
936
- //#region src/extension-worker-config.ts
932
+ // src/extension-worker-config.ts
937
933
  function integer$1(value, name, fallback, minimum, maximum) {
938
934
  const resolved = value ?? fallback;
939
935
  if (typeof resolved !== "number" || !Number.isSafeInteger(resolved) || resolved < minimum || resolved > maximum) throw new Error(`hostExecution.${name} must be an integer from ${minimum} through ${maximum}`);
@@ -971,8 +967,7 @@ function resolveHostExecution(raw) {
971
967
  logMessageBytes: integer$1(value.logMessageBytes, "logMessageBytes", 4096, 128, 65536)
972
968
  });
973
969
  }
974
- //#endregion
975
- //#region src/config.ts
970
+ // src/config.ts
976
971
  /** Loader-facing defaults; {@link parseGatewayConfig} enforces cross-field security rules. */
977
972
  const Config = z.object({
978
973
  hostExecution: z.object({
@@ -1206,8 +1201,7 @@ function parseGatewayConfig(raw) {
1206
1201
  maxRateLimitKeys: integer(value.maxRateLimitKeys, "maxRateLimitKeys", 256, 1, 4096)
1207
1202
  });
1208
1203
  }
1209
- //#endregion
1210
- //#region src/exec-file.ts
1204
+ // src/exec-file.ts
1211
1205
  /** Capture both output streams even when a desktop host wraps execFile without Node's promisify metadata. */
1212
1206
  function execFileText(file, args, options = {}) {
1213
1207
  return new Promise((resolve, reject) => {
@@ -1231,8 +1225,7 @@ function execFileText(file, args, options = {}) {
1231
1225
  });
1232
1226
  });
1233
1227
  }
1234
- //#endregion
1235
- //#region src/private-file.ts
1228
+ // src/private-file.ts
1236
1229
  let userSidTask;
1237
1230
  function commandFailureDetails(error) {
1238
1231
  if (error === null || typeof error !== "object") return {};
@@ -1371,8 +1364,7 @@ async function restrictPrivateDirectory(directory) {
1371
1364
  else if (info.isFile()) await restrictPrivateFile(child);
1372
1365
  }
1373
1366
  }
1374
- //#endregion
1375
- //#region src/client-module-preferences.ts
1367
+ // src/client-module-preferences.ts
1376
1368
  const MAX_RECORD_BYTES = 1048576;
1377
1369
  /** Validate untrusted JSON selections; graph membership is checked by the Gateway. */
1378
1370
  function parseExcludedClientModules(value) {
@@ -1545,8 +1537,7 @@ var ClientModulePreferenceStore = class {
1545
1537
  }
1546
1538
  }
1547
1539
  };
1548
- //#endregion
1549
- //#region src/control.ts
1540
+ // src/control.ts
1550
1541
  /** A configuration change failed; recovered distinguishes a restored runtime from an unknown configuration. */
1551
1542
  var MobileAccessReconfigurationError = class extends Error {
1552
1543
  recovered;
@@ -1868,8 +1859,7 @@ function isLocalAdminHostname(hostname) {
1868
1859
  if (octets === void 0) return false;
1869
1860
  return isLoopbackIpv4(octets) || isPrivateIpv4(octets) || isLinkLocalIpv4(octets);
1870
1861
  }
1871
- //#endregion
1872
- //#region src/http-security.ts
1862
+ // src/http-security.ts
1873
1863
  const DEVICE_COOKIE = "dsh_ma_device";
1874
1864
  const SESSION_COOKIE = "dsh_ma_session";
1875
1865
  const CSRF_COOKIE = "dsh_ma_csrf";
@@ -2053,8 +2043,7 @@ function assertLocalAdminTrust(request, requireBrowserOrigin, authenticateDeskto
2053
2043
  if (requireBrowserOrigin && site !== void 0 && site !== "same-origin") throw new HttpError(403, "forbidden");
2054
2044
  if (!loopback && !authenticateDesktop()) throw new HttpError(401, "admin_authentication_required");
2055
2045
  }
2056
- //#endregion
2057
- //#region src/browser-auth-cookies.ts
2046
+ // src/browser-auth-cookies.ts
2058
2047
  const HOST_SESSION_COOKIE = "__Host-dsh_ma_session";
2059
2048
  const HOST_CSRF_COOKIE = "__Host-dsh_ma_csrf";
2060
2049
  /** Non-secret browser identity; the persistent device credential keeps its narrow renewal path. */
@@ -2106,8 +2095,7 @@ function authenticatedCookie(values, authenticate, accepts = () => true) {
2106
2095
  if (selected === void 0) throw new HttpError(401, "authentication_failed");
2107
2096
  return selected;
2108
2097
  }
2109
- //#endregion
2110
- //#region src/auth-failure-log.ts
2098
+ // src/auth-failure-log.ts
2111
2099
  const LOG_INTERVAL_MS$3 = 3e4;
2112
2100
  const MAX_LOGGED_ORIGIN_LENGTH = 512;
2113
2101
  function requestPathCategory(path) {
@@ -2222,12 +2210,17 @@ var AuthFailureRecorder = class {
2222
2210
  }
2223
2211
  }
2224
2212
  };
2225
- //#endregion
2226
- //#region src/http-failure-log.ts
2213
+ // src/http-failure-log.ts
2227
2214
  function safeParserCode(code) {
2228
2215
  switch (code) {
2229
2216
  case "ERR_HTTP_REQUEST_TIMEOUT":
2230
2217
  case "HPE_HEADER_OVERFLOW":
2218
+ case "HPE_INVALID_METHOD":
2219
+ case "HPE_INVALID_CONSTANT":
2220
+ case "HPE_INVALID_URL":
2221
+ case "HPE_INVALID_VERSION":
2222
+ case "HPE_LF_EXPECTED":
2223
+ case "HPE_PAUSED_H2_UPGRADE":
2231
2224
  case "HPE_INVALID_CHUNK_SIZE":
2232
2225
  case "HPE_INVALID_CONTENT_LENGTH":
2233
2226
  case "HPE_INVALID_EOF_STATE":
@@ -2262,7 +2255,7 @@ function classifyHttpParserFailure(nodeCode) {
2262
2255
  }
2263
2256
  const LOG_INTERVAL_MS$2 = 3e4;
2264
2257
  /**
2265
- * Per-Gateway diagnostics with three fixed sampling slots and no timers or request-selected keys.
2258
+ * Per-Gateway diagnostics with four fixed sampling slots and no timers or request-selected keys.
2266
2259
  * Each kind records once immediately, then at most once per 30 seconds with suppressed counts.
2267
2260
  * A sink failure cannot change a response, retry a write or expose the diagnostic sink's error.
2268
2261
  */
@@ -2275,6 +2268,10 @@ var HttpFailureRecorder = class {
2275
2268
  nextLogAt: 0,
2276
2269
  suppressed: 0
2277
2270
  },
2271
+ transport: {
2272
+ nextLogAt: 0,
2273
+ suppressed: 0
2274
+ },
2278
2275
  target: {
2279
2276
  nextLogAt: 0,
2280
2277
  suppressed: 0
@@ -2302,6 +2299,26 @@ var HttpFailureRecorder = class {
2302
2299
  });
2303
2300
  return failure;
2304
2301
  }
2302
+ /** Classify a client socket error without assigning an HTTP status to a transport failure. */
2303
+ recordClientError(nodeCode) {
2304
+ switch (nodeCode) {
2305
+ case "ECONNRESET":
2306
+ case "EPIPE":
2307
+ case "ETIMEDOUT":
2308
+ case "ERR_STREAM_PREMATURE_CLOSE": {
2309
+ const failure = {
2310
+ kind: "transport",
2311
+ code: nodeCode
2312
+ };
2313
+ this.record(failure);
2314
+ return failure;
2315
+ }
2316
+ default: return {
2317
+ kind: "parser",
2318
+ ...this.recordParser(nodeCode)
2319
+ };
2320
+ }
2321
+ }
2305
2322
  /** Sample an invalid request target without retaining its raw or decoded URL. */
2306
2323
  recordTarget() {
2307
2324
  this.record({
@@ -2345,8 +2362,7 @@ var HttpFailureRecorder = class {
2345
2362
  }
2346
2363
  }
2347
2364
  };
2348
- //#endregion
2349
- //#region src/api-latency-log.ts
2365
+ // src/api-latency-log.ts
2350
2366
  /**
2351
2367
  * Bounded API latency samples for diagnosing slow mobile requests.
2352
2368
  *
@@ -2453,8 +2469,7 @@ var ApiLatencyRecorder = class {
2453
2469
  }
2454
2470
  }
2455
2471
  };
2456
- //#endregion
2457
- //#region src/upstream-auth-log.ts
2472
+ // src/upstream-auth-log.ts
2458
2473
  const LOG_INTERVAL_MS = 3e4;
2459
2474
  /** Two fixed sampling slots keep a first failure visible even after a successful exchange. */
2460
2475
  var UpstreamAuthRecorder = class {
@@ -2519,8 +2534,7 @@ var UpstreamAuthRecorder = class {
2519
2534
  }
2520
2535
  }
2521
2536
  };
2522
- //#endregion
2523
- //#region src/client-module-recovery.ts
2537
+ // src/client-module-recovery.ts
2524
2538
  const CLIENT_MODULE_RECOVERY_COPY = {
2525
2539
  zh: {
2526
2540
  title: "移动页面模块需要调整",
@@ -2601,8 +2615,7 @@ function renderClientModuleRecoveryScript(locale) {
2601
2615
  })
2602
2616
  })()`;
2603
2617
  }
2604
- //#endregion
2605
- //#region src/mobile-compat-bootstrap.ts
2618
+ // src/mobile-compat-bootstrap.ts
2606
2619
  const MOBILE_COMPAT_PATH = `${AUTH_PREFIX}/compat.js`;
2607
2620
  /**
2608
2621
  * Name the compatibility bundle by content.
@@ -2678,8 +2691,7 @@ const EXTENSION_LIMITS = Object.freeze({
2678
2691
  assetBytes: 33554432,
2679
2692
  assetDepth: 8
2680
2693
  });
2681
- //#endregion
2682
- //#region src/http-cache.ts
2694
+ // src/http-cache.ts
2683
2695
  /** Entity-tag handling for authenticated GET and HEAD cache validation. */
2684
2696
  const ENTITY_TAG = /^[\t ]*(?:W\/)?("[\x21\x23-\x7e\x80-\xff]*")[\t ]*$/u;
2685
2697
  const ENTITY_TAG_LIST_ITEM = /(?:W\/)?"[\x21\x23-\x7e\x80-\xff]*"/gu;
@@ -2716,8 +2728,7 @@ function weakenEntityTag(etag) {
2716
2728
  const opaque = etag === void 0 ? void 0 : ENTITY_TAG.exec(etag)?.[1];
2717
2729
  return opaque === void 0 ? void 0 : `W/${opaque}`;
2718
2730
  }
2719
- //#endregion
2720
- //#region src/mobile-graph-events.ts
2731
+ // src/mobile-graph-events.ts
2721
2732
  /** Bounded SSE framing for the dedicated Mobile client graph stream. */
2722
2733
  /** Maximum retained UTF-8 frame/output bytes, with CRLF normalized to LF. */
2723
2734
  const MOBILE_GRAPH_EVENT_MAX_BYTES = 4194304;
@@ -2906,8 +2917,7 @@ var MobileGraphEventTransform = class extends Transform {
2906
2917
  function createMobileGraphEventTransform(project) {
2907
2918
  return new MobileGraphEventTransform(project);
2908
2919
  }
2909
- //#endregion
2910
- //#region src/websocket-compression.ts
2920
+ // src/websocket-compression.ts
2911
2921
  function payloadBytes(data) {
2912
2922
  if (Array.isArray(data)) return data.reduce((bytes, part) => bytes + part.byteLength, 0);
2913
2923
  return data.byteLength;
@@ -3127,15 +3137,13 @@ function bridgeCompressedWebSocket(request, client, upstream, head, target, upst
3127
3137
  }
3128
3138
  });
3129
3139
  }
3130
- //#endregion
3131
- //#region src/version.ts
3140
+ // src/version.ts
3132
3141
  const manifest = createRequire(import.meta.url)("../package.json");
3133
3142
  /** Version of the installed DSH Mobile plugin package. */
3134
3143
  const DSH_MOBILE_VERSION = typeof manifest.version === "string" ? manifest.version : "unknown";
3135
3144
  /** Oldest Android App release supported by this plugin generation. */
3136
3145
  const MINIMUM_ANDROID_APP_VERSION = "0.2.2";
3137
- //#endregion
3138
- //#region src/computer-images.ts
3146
+ // src/computer-images.ts
3139
3147
  const MAX_ENTRIES = 500;
3140
3148
  const MAX_IMAGE_BYTES = 20971520;
3141
3149
  const IMAGE_TYPES = Object.freeze({
@@ -3336,8 +3344,7 @@ async function readComputerImage(path, signal) {
3336
3344
  throw new HttpError(404, "file_unavailable");
3337
3345
  }
3338
3346
  }
3339
- //#endregion
3340
- //#region src/extension-worker.ts
3347
+ // src/extension-worker.ts
3341
3348
  /** Supervise opt-in local hosts without executing extension code on the DSH thread. */
3342
3349
  /** JSON bytes serialized and size-checked inside the worker. */
3343
3350
  var PreparedJsonResult = class {
@@ -3990,8 +3997,7 @@ var WorkerStreamBridge = class extends Readable {
3990
3997
  return super.emit(event, ...args);
3991
3998
  }
3992
3999
  };
3993
- //#endregion
3994
- //#region src/extensions.ts
4000
+ // src/extensions.ts
3995
4001
  /** A misbehaving host activation must not wedge the local watcher forever. */
3996
4002
  const HOST_ACTIVATION_TIMEOUT_MS = 5e3;
3997
4003
  /** The previous Host outlives the hidden-page refresh interval and one timed refresh. */
@@ -5064,8 +5070,7 @@ function workerLoggerAdapter(context) {
5064
5070
  function createMobileAccessService(ctx) {
5065
5071
  return new MobileAccessService(ctx);
5066
5072
  }
5067
- //#endregion
5068
- //#region src/auth-pages.ts
5073
+ // src/auth-pages.ts
5069
5074
  const COPY = {
5070
5075
  en: {
5071
5076
  lang: "en",
@@ -5293,8 +5298,7 @@ function renderLoginScript(locale) {
5293
5298
  })()
5294
5299
  `;
5295
5300
  }
5296
- //#endregion
5297
- //#region src/browser-ui-locale.ts
5301
+ // src/browser-ui-locale.ts
5298
5302
  const UI_LOCALE_COOKIE = "dsh_ma_ui_locale";
5299
5303
  /** Read presentation data only; malformed or ambiguous cookies keep browser negotiation. */
5300
5304
  function readBrowserUiLocale(cookieHeader) {
@@ -5313,8 +5317,7 @@ function readBrowserUiLocale(cookieHeader) {
5313
5317
  function resolveBrowserUiLocale(cookieHeader, acceptLanguage) {
5314
5318
  return readBrowserUiLocale(cookieHeader) ?? resolveAuthPageLocale(acceptLanguage);
5315
5319
  }
5316
- //#endregion
5317
- //#region src/mobile-push/assets.ts
5320
+ // src/mobile-push/assets.ts
5318
5321
  const ICON_FILES = Object.freeze({
5319
5322
  "/mobile-access/push/icon-180.png": "icon-180.png",
5320
5323
  "/mobile-access/push/icon-192.png": "icon-192.png",
@@ -5366,16 +5369,14 @@ const loadFromPackage = createMobilePushAssetLoader(packageRoot);
5366
5369
  function mobilePushAsset(path, root = packageRoot) {
5367
5370
  return root === packageRoot ? loadFromPackage(path) : createMobilePushAssetLoader(root)(path);
5368
5371
  }
5369
- //#endregion
5370
- //#region src/mobile-push/installation-id.ts
5372
+ // src/mobile-push/installation-id.ts
5371
5373
  /** Browser-safe installation-id validation shared by the browser controller and the Gateway. */
5372
5374
  const INSTALLATION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
5373
5375
  /** True only for a UUID-shaped installation id with an allowed version and variant nibble. */
5374
5376
  function isPushInstallationId(value) {
5375
5377
  return typeof value === "string" && INSTALLATION_ID.test(value);
5376
5378
  }
5377
- //#endregion
5378
- //#region src/gateway.ts
5379
+ // src/gateway.ts
5379
5380
  const MAX_CONTROL_BODY_BYTES = 16384;
5380
5381
  const MAX_HEADER_BYTES = 16384;
5381
5382
  const DISCOVERY_QUERY = Buffer.from("DSH_MOBILE_DISCOVER_V1", "ascii");
@@ -5548,6 +5549,7 @@ const RUNTIME_MODULE = "@deepseek-ai/dsh-client-runtime";
5548
5549
  const RENDERER_MODULE = "@deepseek-ai/dsh-client-ui-renderer";
5549
5550
  const SIDEBAR_MODULE = "@deepseek-ai/dsh-client-ui-sidebar";
5550
5551
  const SETTINGS_MODULE = "@deepseek-ai/dsh-client-ui-settings";
5552
+ const SETTINGS_GENERAL_MODULE = "@deepseek-ai/dsh-client-ui-settings-general";
5551
5553
  const API_GATEWAY_MODULE = "@deepseek-ai/dsh-api-gateway";
5552
5554
  const API_REMOTES_MODULE = "@deepseek-ai/dsh-api-remotes";
5553
5555
  const MOBILE_BOOT_CORE_MODULES = /* @__PURE__ */ new Set([
@@ -5558,6 +5560,7 @@ const MOBILE_BOOT_CORE_MODULES = /* @__PURE__ */ new Set([
5558
5560
  RENDERER_MODULE,
5559
5561
  SIDEBAR_MODULE,
5560
5562
  SETTINGS_MODULE,
5563
+ SETTINGS_GENERAL_MODULE,
5561
5564
  API_GATEWAY_MODULE,
5562
5565
  API_REMOTES_MODULE
5563
5566
  ]);
@@ -5678,8 +5681,8 @@ var ExcludedClientModulesError = class extends Error {
5678
5681
  /** A graph-selection conflict with a bounded diagnostic safe for authenticated callers. */
5679
5682
  var ClientModuleConflictError = class extends HttpError {
5680
5683
  detail;
5681
- constructor(detail) {
5682
- super(409, "excluded_client_modules_invalid");
5684
+ constructor(detail, code = "excluded_client_modules_invalid") {
5685
+ super(409, code);
5683
5686
  this.detail = detail.replace(/[\u0000-\u001f\u007f]/gu, " ").replace(/(?:https?:\/\/|[a-z]:[\\/])\S+/giu, "[redacted]").slice(0, 512);
5684
5687
  }
5685
5688
  };
@@ -5714,6 +5717,25 @@ function describeMobileClientModules(html) {
5714
5717
  dependencies: Object.freeze(clientModuleDependencies(entry).filter((id) => ids.has(id)))
5715
5718
  })));
5716
5719
  }
5720
+ /**
5721
+ * Canonical fingerprint of an installed catalog.
5722
+ *
5723
+ * A settings page computes "required only" client-side. If a plugin was added,
5724
+ * removed or became required while that page stayed open, the computed set is
5725
+ * based on a catalog that no longer exists. The fingerprint travels with the
5726
+ * read and back with the write, so a stale page is told to reread instead of
5727
+ * saving a selection made against an older graph.
5728
+ * @param entries - Catalog entries in any order.
5729
+ * @returns Stable 16-character hexadecimal revision.
5730
+ */
5731
+ function clientModuleCatalogRevision(entries) {
5732
+ const canonical = entries.map((entry) => [
5733
+ entry.id,
5734
+ entry.required,
5735
+ [...new Set(entry.dependencies)].sort()
5736
+ ]).sort((left, right) => left[0] < right[0] ? -1 : left[0] > right[0] ? 1 : 0);
5737
+ return createHash("sha256").update(JSON.stringify(canonical)).digest("hex").slice(0, 16);
5738
+ }
5717
5739
  /** Validate graph edges before removing application rows from both manifest views. */
5718
5740
  function excludeMobileBootEntries(entries, batches, excludedIds) {
5719
5741
  if (excludedIds.length === 0) return;
@@ -6609,7 +6631,11 @@ var MobileAccessGateway = class {
6609
6631
  });
6610
6632
  server.on("clientError", (error, socket) => {
6611
6633
  const nodeCode = "code" in error && typeof error.code === "string" ? error.code : void 0;
6612
- const failure = this.httpFailures.recordParser(nodeCode);
6634
+ const failure = this.httpFailures.recordClientError(nodeCode);
6635
+ if (failure.kind === "transport" || !socket.writable) {
6636
+ socket.destroy();
6637
+ return;
6638
+ }
6613
6639
  rejectUpgrade(socket, failure.status, failure.code);
6614
6640
  });
6615
6641
  await new Promise((resolve, reject) => {
@@ -7339,18 +7365,19 @@ var MobileAccessGateway = class {
7339
7365
  try {
7340
7366
  let kind = "describe";
7341
7367
  let excluded;
7368
+ let revision;
7342
7369
  if (request.method === "POST") {
7343
7370
  const body = await readJsonObject(request, this.config.maxBodyBytes);
7344
7371
  const keys = Object.keys(body);
7345
- if (keys.length !== 1) throw new HttpError(400, "bad_request");
7346
- if (keys[0] === "reset" && body.reset === true) kind = "reset";
7347
- else if (keys[0] === "measure" && body.measure === true) kind = "measure";
7348
- else if (keys[0] === "excludedClientModules") {
7372
+ if (keys.length === 1 && keys[0] === "reset" && body.reset === true) kind = "reset";
7373
+ else if (keys.length === 1 && keys[0] === "measure" && body.measure === true) kind = "measure";
7374
+ else if (keys.every((key) => key === "excludedClientModules" || key === "revision") && Object.prototype.hasOwnProperty.call(body, "excludedClientModules") && (body.revision === void 0 || typeof body.revision === "string")) {
7349
7375
  kind = "configure";
7350
7376
  excluded = body.excludedClientModules;
7377
+ revision = body.revision;
7351
7378
  } else throw new HttpError(400, "bad_request");
7352
7379
  }
7353
- const view = await this.clientModulePreferenceOperation(kind, excluded, authorization.deviceId, operation.signal);
7380
+ const view = await this.clientModulePreferenceOperation(kind, excluded, authorization.deviceId, operation.signal, revision);
7354
7381
  operation.signal.throwIfAborted();
7355
7382
  sendJson(response, 200, view, this.tlsEnabled);
7356
7383
  } catch (error) {
@@ -7835,10 +7862,11 @@ var MobileAccessGateway = class {
7835
7862
  * @param excluded - Untrusted module IDs to validate against the current boot graph.
7836
7863
  * @param deviceId - Authenticated device identity; omitted for computer defaults.
7837
7864
  * @param callerSignal - Cancels fetching and validation, not an atomic write already started.
7865
+ * @param revision - Catalog fingerprint the selection was computed against; a mismatch is rejected.
7838
7866
  * @returns The catalog and saved selection without reloading clients.
7839
7867
  */
7840
- configureClientModules(excluded, deviceId, callerSignal) {
7841
- return this.clientModulePreferenceOperation("configure", excluded, deviceId, callerSignal);
7868
+ configureClientModules(excluded, deviceId, callerSignal, revision) {
7869
+ return this.clientModulePreferenceOperation("configure", excluded, deviceId, callerSignal, revision);
7842
7870
  }
7843
7871
  /** Reset one override without reloading connected clients.
7844
7872
  * @param deviceId - Authenticated device identity; omitted to reset computer defaults.
@@ -8024,7 +8052,7 @@ var MobileAccessGateway = class {
8024
8052
  await upstreamClosed;
8025
8053
  }
8026
8054
  }
8027
- async clientModulePreferenceOperation(kind, excluded, deviceId, callerSignal) {
8055
+ async clientModulePreferenceOperation(kind, excluded, deviceId, callerSignal, revision) {
8028
8056
  if (this.closing || !this.started) throw new HttpError(503, "unavailable");
8029
8057
  if (kind === "measure" && this.clientModuleMeasurementActive) throw new HttpError(429, "busy");
8030
8058
  if (kind === "measure") this.clientModuleMeasurementActive = true;
@@ -8043,8 +8071,10 @@ var MobileAccessGateway = class {
8043
8071
  const validate = async (selection) => {
8044
8072
  html = (await this.fetchAuthenticatedUpstreamIndex(controller.signal)).html;
8045
8073
  try {
8074
+ if (revision !== void 0 && revision !== clientModuleCatalogRevision(describeMobileClientModules(html))) throw new ClientModuleConflictError("the module catalog changed since it was read; reread it and confirm the selection again", "client_module_catalog_changed");
8046
8075
  parseMobileBootPlan(html, selection);
8047
8076
  } catch (error) {
8077
+ if (error instanceof ClientModuleConflictError) throw error;
8048
8078
  if (error instanceof ExcludedClientModulesError) throw new ClientModuleConflictError(error.message);
8049
8079
  throw new HttpError(502, "upstream_unavailable");
8050
8080
  }
@@ -8072,8 +8102,10 @@ var MobileAccessGateway = class {
8072
8102
  } catch (error) {
8073
8103
  if (!measurementExpired || callerSignal?.aborted === true || this.closing) throw error;
8074
8104
  }
8105
+ const entries = this.describeMobileModulesWithSizes(html);
8075
8106
  return Object.freeze({
8076
- entries: this.describeMobileModulesWithSizes(html),
8107
+ entries,
8108
+ revision: clientModuleCatalogRevision(entries),
8077
8109
  ...selection,
8078
8110
  ...plan.batches === void 0 ? {
8079
8111
  excludedClientModules: Object.freeze([]),
@@ -9232,8 +9264,7 @@ var MobileAccessGateway = class {
9232
9264
  };
9233
9265
  }
9234
9266
  };
9235
- //#endregion
9236
- //#region src/storage.ts
9267
+ // src/storage.ts
9237
9268
  function assertInteger(value, name) {
9238
9269
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error(`device state ${name} must be a non-negative integer`);
9239
9270
  }
@@ -9360,8 +9391,7 @@ var MemoryDeviceStore = class {
9360
9391
  return structuredClone(this.snapshot);
9361
9392
  }
9362
9393
  };
9363
- //#endregion
9364
- //#region src/managed-files.ts
9394
+ // src/managed-files.ts
9365
9395
  /** Refuse link-shaped ancestors inside an owned state root before accessing a child. */
9366
9396
  async function assertManagedParents(root, target, prefix) {
9367
9397
  const parent = resolve(root);
@@ -9528,8 +9558,7 @@ async function replaceManagedDirectory(root, target, candidate, prefix, operatio
9528
9558
  }
9529
9559
  }
9530
9560
  }
9531
- //#endregion
9532
- //#region src/component-download.ts
9561
+ // src/component-download.ts
9533
9562
  /**
9534
9563
  * Hosts a pinned GitHub release download may redirect to.
9535
9564
  *
@@ -9640,8 +9669,7 @@ async function downloadPinnedArtifact(request) {
9640
9669
  }
9641
9670
  throw lastError;
9642
9671
  }
9643
- //#endregion
9644
- //#region src/frp-component.ts
9672
+ // src/frp-component.ts
9645
9673
  const FRP_VERSION$1 = "0.70.1";
9646
9674
  const MAX_ARCHIVE_ENTRIES$1 = 128;
9647
9675
  const MAX_ARCHIVE_LIST_BYTES$1 = 262144;
@@ -9925,8 +9953,7 @@ var FrpComponentManager = class {
9925
9953
  return task.then(() => this.status());
9926
9954
  }
9927
9955
  };
9928
- //#endregion
9929
- //#region src/windows-msi-extract.ts
9956
+ // src/windows-msi-extract.ts
9930
9957
  /** A failed read-only MSI extraction; causes deliberately exclude raw command arguments. */
9931
9958
  var WindowsMsiExtractionError = class extends Error {
9932
9959
  stage;
@@ -10410,8 +10437,7 @@ async function extractWindowsMsiBinary(msi, destination, executableName, expecte
10410
10437
  await copyFile(binary, join(destination, executableName));
10411
10438
  });
10412
10439
  }
10413
- //#endregion
10414
- //#region src/cpolar-component.ts
10440
+ // src/cpolar-component.ts
10415
10441
  const CPOLAR_VERSION = "3.3.18";
10416
10442
  /** Pinned official cpolar release metadata for supported desktop targets. */
10417
10443
  const CPOLAR_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
@@ -10779,8 +10805,7 @@ var CpolarComponentManager = class {
10779
10805
  return task.then(() => this.status());
10780
10806
  }
10781
10807
  };
10782
- //#endregion
10783
- //#region src/frp-template.ts
10808
+ // src/frp-template.ts
10784
10809
  /** Loopback-only HTTP vhost port used between Caddy and frps. */
10785
10810
  const FRP_VHOST_HTTP_PORT = 7080;
10786
10811
  /** Caddy snippet owned entirely by DSH Mobile; the main Caddyfile only imports it. */
@@ -10909,8 +10934,7 @@ function createRestrictedFrpServerTemplate(serverPort, token, publicOrigin, opti
10909
10934
  if (publicIpv4Address(publicHost)) lines.push(manualIpCertificateGuide(publicHost), "");
10910
10935
  return lines.join("\n");
10911
10936
  }
10912
- //#endregion
10913
- //#region src/frp-config.ts
10937
+ // src/frp-config.ts
10914
10938
  const MAX_SETTINGS_BYTES$1 = 8192;
10915
10939
  /** Public entry port used by the self-signed TCP passthrough (never 443 by requirement). */
10916
10940
  const FRP_DEFAULT_PUBLIC_PORT = 33080;
@@ -10930,7 +10954,8 @@ const FRP_SETTINGS_KEYS = /* @__PURE__ */ new Set([
10930
10954
  "mode",
10931
10955
  "entryTls",
10932
10956
  "vhostHttpPort",
10933
- "publicPort"
10957
+ "publicPort",
10958
+ "user"
10934
10959
  ]);
10935
10960
  function hostname$2(value) {
10936
10961
  if (value.length > 253 || !value.includes(".")) return false;
@@ -10953,6 +10978,11 @@ function validateFrpToken(value) {
10953
10978
  if (typeof value !== "string" || value.length < 16 || value.length > 512 || /[\s\u0000-\u001f\u007f]/u.test(value)) throw new Error("frp_token_invalid");
10954
10979
  return value;
10955
10980
  }
10981
+ /** Validate the optional frpc `user` field; it must stay one safe TOML token. */
10982
+ function validateFrpUser(value) {
10983
+ if (typeof value !== "string" || value.length < 1 || value.length > 128 || !/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error("frp_user_invalid");
10984
+ return value;
10985
+ }
10956
10986
  /** Validate the public HTTPS origin used by Caddy and Android pairing. */
10957
10987
  function validateFrpPublicOrigin(value) {
10958
10988
  if (typeof value !== "string" || value.length > 512) throw new Error("frp_public_origin_invalid");
@@ -11043,6 +11073,7 @@ function parseFrpSettings(value) {
11043
11073
  const entryTls = record.entryTls === void 0 ? void 0 : validateFrpEntryTls(record.entryTls);
11044
11074
  const vhostHttpPort = record.vhostHttpPort === void 0 ? void 0 : validateFrpVhostHttpPort(record.vhostHttpPort);
11045
11075
  const publicPort = record.publicPort === void 0 ? void 0 : validateFrpPublicPort(record.publicPort);
11076
+ const user = record.user === void 0 || record.user === "" ? void 0 : validateFrpUser(record.user);
11046
11077
  const resolvedMode = mode ?? "deploy";
11047
11078
  const resolvedEntryTls = entryTls ?? "public-ip-cert";
11048
11079
  if (resolvedEntryTls === "self-signed" && resolvedMode !== "attach") throw new Error("frp_entry_tls_invalid");
@@ -11058,7 +11089,8 @@ function parseFrpSettings(value) {
11058
11089
  ...mode === void 0 ? {} : { mode },
11059
11090
  ...entryTls === void 0 ? {} : { entryTls },
11060
11091
  ...vhostHttpPort === void 0 ? {} : { vhostHttpPort },
11061
- ...publicPort === void 0 ? {} : { publicPort }
11092
+ ...publicPort === void 0 ? {} : { publicPort },
11093
+ ...user === void 0 ? {} : { user }
11062
11094
  });
11063
11095
  }
11064
11096
  /**
@@ -11077,6 +11109,7 @@ function mergeSavedFrpSettings(partial, saved) {
11077
11109
  const entryTls = pick("entryTls");
11078
11110
  const vhostHttpPort = pick("vhostHttpPort");
11079
11111
  const publicPort = pick("publicPort");
11112
+ const user = partial.user === void 0 ? saved?.user : partial.user;
11080
11113
  const merged = {
11081
11114
  serverAddress: partial.serverAddress === "" || partial.serverAddress === void 0 ? saved?.serverAddress : partial.serverAddress,
11082
11115
  serverPort: typeof partial.serverPort === "number" && Number.isSafeInteger(partial.serverPort) && partial.serverPort >= 1 ? partial.serverPort : saved?.serverPort,
@@ -11085,7 +11118,8 @@ function mergeSavedFrpSettings(partial, saved) {
11085
11118
  ...mode === "" || mode === void 0 ? {} : { mode },
11086
11119
  ...entryTls === "" || entryTls === void 0 ? {} : { entryTls },
11087
11120
  ...vhostHttpPort === "" || vhostHttpPort === void 0 ? {} : { vhostHttpPort },
11088
- ...publicPort === "" || publicPort === void 0 ? {} : { publicPort }
11121
+ ...publicPort === "" || publicPort === void 0 ? {} : { publicPort },
11122
+ ...user === "" || user === void 0 ? {} : { user }
11089
11123
  };
11090
11124
  if (merged.serverAddress === void 0 && merged.serverPort === void 0 && merged.token === void 0 && merged.publicOrigin === void 0) throw new Error("frp_config_missing");
11091
11125
  return parseFrpSettings(merged);
@@ -11155,6 +11189,7 @@ function createFrpcToml(settings, localPort, proxyName = FRP_DEFAULT_PROXY_NAME)
11155
11189
  `serverPort = ${String(settings.serverPort)}`,
11156
11190
  "auth.method = \"token\"",
11157
11191
  `auth.token = ${tomlString(settings.token)}`,
11192
+ ...settings.user === void 0 ? [] : [`user = ${tomlString(settings.user)}`],
11158
11193
  "transport.tls.enable = true",
11159
11194
  "",
11160
11195
  "[[proxies]]",
@@ -11267,7 +11302,8 @@ var FrpConfigStore = class {
11267
11302
  ...this.errorCode === void 0 ? {} : { errorCode: this.errorCode },
11268
11303
  ...mode === void 0 || mode === "deploy" ? {} : { mode },
11269
11304
  ...entryTls === void 0 || entryTls === "public-ip-cert" ? {} : { entryTls },
11270
- ...settings?.publicPort === void 0 ? {} : { publicPort: settings.publicPort }
11305
+ ...settings?.publicPort === void 0 ? {} : { publicPort: settings.publicPort },
11306
+ ...settings?.user === void 0 ? {} : { user: settings.user }
11271
11307
  });
11272
11308
  }
11273
11309
  /** Return private settings only to the provider lifecycle. */
@@ -11298,8 +11334,7 @@ var FrpConfigStore = class {
11298
11334
  return this.status();
11299
11335
  }
11300
11336
  };
11301
- //#endregion
11302
- //#region src/frp-attach.ts
11337
+ // src/frp-attach.ts
11303
11338
  /** Default proof path used to confirm the public entry answered from this computer. */
11304
11339
  const FRP_ATTACH_DISCOVERY_PATH = "/mobile-access/discovery";
11305
11340
  /** Declaration printed above the VPS half of an attach template. */
@@ -11496,8 +11531,7 @@ function createFrpAttachTemplateParts(settings, options = {}) {
11496
11531
  function createFrpAttachTemplate(settings, options = {}) {
11497
11532
  return createFrpAttachTemplateParts(settings, options).text;
11498
11533
  }
11499
- //#endregion
11500
- //#region src/frp-attach-plan.ts
11534
+ // src/frp-attach-plan.ts
11501
11535
  const DEFAULT_PLAN_CONFIG_FILE = "<DSH Mobile 私有目录>/remote/frp/config/frpc.toml";
11502
11536
  function planConfigFile(options) {
11503
11537
  const file = options.configFile;
@@ -11635,8 +11669,7 @@ function createFrpAttachPlan(settings, options = {}) {
11635
11669
  ])
11636
11670
  });
11637
11671
  }
11638
- //#endregion
11639
- //#region src/cert-renewal.ts
11672
+ // src/cert-renewal.ts
11640
11673
  /** Days left below which the entry certificate is reported as expiring. */
11641
11674
  const CERT_EXPIRING_DAYS = 2;
11642
11675
  const DAY_MS = 864e5;
@@ -11742,8 +11775,7 @@ async function probeOriginCertificate(host, port, timeoutMs = 5e3) {
11742
11775
  });
11743
11776
  });
11744
11777
  }
11745
- //#endregion
11746
- //#region src/managed-setup.ts
11778
+ // src/managed-setup.ts
11747
11779
  const VIRTUAL_INTERFACE_MARKERS = [
11748
11780
  "bridge",
11749
11781
  "docker",
@@ -12241,8 +12273,7 @@ async function materializeManagedSetup(setup, table) {
12241
12273
  }
12242
12274
  };
12243
12275
  }
12244
- //#endregion
12245
- //#region src/frp-ingress.ts
12276
+ // src/frp-ingress.ts
12246
12277
  /** Private ingress directory, always a sibling of the remote device file. */
12247
12278
  function frpIngressPaths(stateFile) {
12248
12279
  const directory = join(dirname(stateFile), "ingress");
@@ -12462,8 +12493,7 @@ async function purgeFrpIngressCertificates(stateFile, ownedRoot = dirname(stateF
12462
12493
  throw error;
12463
12494
  }
12464
12495
  }
12465
- //#endregion
12466
- //#region src/remote.ts
12496
+ // src/remote.ts
12467
12497
  const REMOTE_PROVIDERS = Object.freeze([
12468
12498
  "tailscale",
12469
12499
  "cpolar",
@@ -12699,8 +12729,7 @@ function configuredRemoteProvider(environment) {
12699
12729
  if (value !== "tailscale" && value !== "cpolar" && value !== "cloudflared" && value !== "frp" && value !== "origin" && value !== "iroh") throw new Error("DSH_MOBILE_REMOTE_PROVIDER must be tailscale, cpolar, cloudflared, frp, origin, or iroh");
12700
12730
  return value;
12701
12731
  }
12702
- //#endregion
12703
- //#region src/frp.ts
12732
+ // src/frp.ts
12704
12733
  const START_TIMEOUT_MS$2 = 45e3;
12705
12734
  const DISCOVERY_REQUEST_TIMEOUT_MS = 5e3;
12706
12735
  const DISCOVERY_RETRY_MS = 1e3;
@@ -13310,8 +13339,7 @@ var FrpController = class {
13310
13339
  ], "FRP resource cleanup failed");
13311
13340
  }
13312
13341
  };
13313
- //#endregion
13314
- //#region src/origin-proxy-config.ts
13342
+ // src/origin-proxy-config.ts
13315
13343
  const DEFAULT_ORIGIN_LISTEN_PORT = 3444;
13316
13344
  const MAX_SETTINGS_BYTES = 8192;
13317
13345
  /**
@@ -13481,8 +13509,7 @@ var OriginConfigStore = class {
13481
13509
  return this.status();
13482
13510
  }
13483
13511
  };
13484
- //#endregion
13485
- //#region src/origin-proxy.ts
13512
+ // src/origin-proxy.ts
13486
13513
  /** Owns an authenticated HTTP gateway, with no tunnel process or public network probes. */
13487
13514
  var OriginController = class {
13488
13515
  options;
@@ -13632,13 +13659,12 @@ var OriginController = class {
13632
13659
  });
13633
13660
  }
13634
13661
  };
13635
- //#endregion
13636
- //#region src/cloudflared-component.ts
13662
+ // src/cloudflared-component.ts
13637
13663
  /** Upper bound for `tar -t` output; a pinned release listing is a few lines. */
13638
13664
  const MAX_ARCHIVE_LIST_BYTES = 1048576;
13639
13665
  const MAX_ARCHIVE_ENTRIES = 256;
13640
13666
  const DOWNLOAD_TIMEOUT_MS = 3e5;
13641
- const CLOUDFLARED_VERSION = "2026.9.1";
13667
+ const CLOUDFLARED_VERSION = "2026.10.7";
13642
13668
  /** Pinned official cloudflared release metadata for supported desktop targets. */
13643
13669
  const CLOUDFLARED_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
13644
13670
  {
@@ -13646,44 +13672,44 @@ const CLOUDFLARED_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
13646
13672
  platform: "win32",
13647
13673
  arch: "x64",
13648
13674
  downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-windows-amd64.exe`,
13649
- downloadBytes: 54976432,
13650
- downloadSha256: "2837888cc0f5d58f15b6dc478376de90b4d3ba5241c7947455d1e0a0df429712",
13675
+ downloadBytes: 55390512,
13676
+ downloadSha256: "2493dbc168923bb3e59e5e7716f71deced92992eea9acd6e9d74f84ac7751343",
13651
13677
  executableName: "cloudflared.exe",
13652
- executableBytes: 54976432,
13653
- executableSha256: "2837888cc0f5d58f15b6dc478376de90b4d3ba5241c7947455d1e0a0df429712"
13678
+ executableBytes: 55390512,
13679
+ executableSha256: "2493dbc168923bb3e59e5e7716f71deced92992eea9acd6e9d74f84ac7751343"
13654
13680
  },
13655
13681
  {
13656
13682
  version: CLOUDFLARED_VERSION,
13657
13683
  platform: "linux",
13658
13684
  arch: "x64",
13659
13685
  downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64`,
13660
- downloadBytes: 39838488,
13661
- downloadSha256: "03f1f25d1cc93b9ad6c60569d44060bc4f17ed97075760ed8cfca4b12dcd68cc",
13686
+ downloadBytes: 40274445,
13687
+ downloadSha256: "1f99fb518b83dec2ba8d870a7cc991d82a77e6196295a3d2754ba17ab9f91872",
13662
13688
  executableName: "cloudflared",
13663
- executableBytes: 39838488,
13664
- executableSha256: "03f1f25d1cc93b9ad6c60569d44060bc4f17ed97075760ed8cfca4b12dcd68cc"
13689
+ executableBytes: 40274445,
13690
+ executableSha256: "1f99fb518b83dec2ba8d870a7cc991d82a77e6196295a3d2754ba17ab9f91872"
13665
13691
  },
13666
13692
  {
13667
13693
  version: CLOUDFLARED_VERSION,
13668
13694
  platform: "linux",
13669
13695
  arch: "arm64",
13670
13696
  downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64`,
13671
- downloadBytes: 37466252,
13672
- downloadSha256: "3d97437c71848bd8df68041e12436b484a661d95073ea1937f01a845ce88faa3",
13697
+ downloadBytes: 37667293,
13698
+ downloadSha256: "3df43aedc9ff291d1735b5cb576cb97e0b157488796db2e32b28aecabb445775",
13673
13699
  executableName: "cloudflared",
13674
- executableBytes: 37466252,
13675
- executableSha256: "3d97437c71848bd8df68041e12436b484a661d95073ea1937f01a845ce88faa3"
13700
+ executableBytes: 37667293,
13701
+ executableSha256: "3df43aedc9ff291d1735b5cb576cb97e0b157488796db2e32b28aecabb445775"
13676
13702
  },
13677
13703
  {
13678
13704
  version: CLOUDFLARED_VERSION,
13679
13705
  platform: "darwin",
13680
13706
  arch: "x64",
13681
13707
  downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-darwin-amd64.tgz`,
13682
- downloadBytes: 21118723,
13683
- downloadSha256: "ff0d3b51d5ff70eceef89d6b32145fee985018a2174596a5dbe405e2766e2ac4",
13708
+ downloadBytes: 21834416,
13709
+ downloadSha256: "02f3edfd87684dd5b705607688e4eb4b323f6e777b81e7b3a85af556aa356848",
13684
13710
  executableName: "cloudflared",
13685
- executableBytes: 41731488,
13686
- executableSha256: "1ea07ae775b03236bd6be18ca1848d6bdc4af2f4f3bce398823b5a36e5761b75",
13711
+ executableBytes: 42430512,
13712
+ executableSha256: "5c55e77da7012cebca19170357e196176c48c9647cc2ead404702fa125ddbd86",
13687
13713
  archive: {
13688
13714
  format: "tgz",
13689
13715
  member: "cloudflared"
@@ -13694,11 +13720,11 @@ const CLOUDFLARED_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
13694
13720
  platform: "darwin",
13695
13721
  arch: "arm64",
13696
13722
  downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-darwin-arm64.tgz`,
13697
- downloadBytes: 19217478,
13698
- downloadSha256: "c27ab8fd0aa489449e3d201eb02f957ef460a13b613662928b1b23394bf1bcfe",
13723
+ downloadBytes: 19847161,
13724
+ downloadSha256: "3586ccf397e98e5f437e60e7c307ca6ac463e110580503af8bb8e6f9ebf831bd",
13699
13725
  executableName: "cloudflared",
13700
- executableBytes: 38893072,
13701
- executableSha256: "9a0b19f67dc7a3011bc6b972c7ce06a5fcea8784ac6bd599ffa382ea4aeb5a6e",
13726
+ executableBytes: 39487168,
13727
+ executableSha256: "2136431d5eb467bf447d2ee514b713e192719b143aec1d9e09bf7dc290d5bb12",
13702
13728
  archive: {
13703
13729
  format: "tgz",
13704
13730
  member: "cloudflared"
@@ -13958,8 +13984,7 @@ var CloudflaredComponentManager = class {
13958
13984
  return task.then(() => this.status());
13959
13985
  }
13960
13986
  };
13961
- //#endregion
13962
- //#region src/cloudflared.ts
13987
+ // src/cloudflared.ts
13963
13988
  const MAX_LOG_BUFFER_BYTES$1 = 65536;
13964
13989
  /**
13965
13990
  * A quick tunnel has to be allocated and registered with the Cloudflare edge
@@ -14477,8 +14502,7 @@ var CloudflaredController = class {
14477
14502
  ], "cloudflared resource cleanup failed");
14478
14503
  }
14479
14504
  };
14480
- //#endregion
14481
- //#region src/cloudflared-tunnel.ts
14505
+ // src/cloudflared-tunnel.ts
14482
14506
  const MAX_CONFIG_BYTES$1 = 8192;
14483
14507
  const QUICK_TUNNEL_SUFFIX = ".trycloudflare.com";
14484
14508
  /** Cloudflare's own tunnel-routing domain; it can never be a public hostname. */
@@ -14684,8 +14708,7 @@ var CloudflaredTunnelStore = class {
14684
14708
  return this.status();
14685
14709
  }
14686
14710
  };
14687
- //#endregion
14688
- //#region src/iroh-files.ts
14711
+ // src/iroh-files.ts
14689
14712
  async function renameIrohPath(root, source, destination) {
14690
14713
  await renameManagedPath(root, source, destination, "iroh");
14691
14714
  }
@@ -14695,8 +14718,7 @@ async function ensureIrohDirectory(root, directory) {
14695
14718
  async function removeIrohTree(root, target) {
14696
14719
  await removeManagedTree(root, target, "iroh");
14697
14720
  }
14698
- //#endregion
14699
- //#region src/iroh-manifest.ts
14721
+ // src/iroh-manifest.ts
14700
14722
  /** Checksum-gated resolution of the iroh transport host binary.
14701
14723
  *
14702
14724
  * The sidecar is built and published by the independent `dsh-mobile-iroh`
@@ -14800,8 +14822,7 @@ async function resolveIrohSidecarAt(companionRoot, platform, arch) {
14800
14822
  if (process.platform !== "win32" && platform !== "win32" && (before.mode & 73) === 0) throw new Error("iroh_native_not_executable");
14801
14823
  return path;
14802
14824
  }
14803
- //#endregion
14804
- //#region src/iroh-component.ts
14825
+ // src/iroh-component.ts
14805
14826
  /** Managed view of the iroh transport host sidecar.
14806
14827
  *
14807
14828
  * Explicit installation verifies and promotes a pinned sidecar release into
@@ -15115,8 +15136,7 @@ var IrohComponentManager = class {
15115
15136
  return this.cached;
15116
15137
  }
15117
15138
  };
15118
- //#endregion
15119
- //#region src/iroh-sidecar.ts
15139
+ // src/iroh-sidecar.ts
15120
15140
  /** Versioned parent-to-sidecar protocol for the iroh transport host.
15121
15141
  *
15122
15142
  * Ported from the independent `dsh-mobile-iroh` companion's sidecar
@@ -15246,8 +15266,7 @@ function decodeIrohEvent(line) {
15246
15266
  }
15247
15267
  return Object.freeze(event);
15248
15268
  }
15249
- //#endregion
15250
- //#region src/iroh-origin.ts
15269
+ // src/iroh-origin.ts
15251
15270
  /** Identity-derived browser origins for the raw TLS-over-iroh carrier.
15252
15271
  *
15253
15272
  * Ported from the independent `dsh-mobile-iroh` companion's origin contract.
@@ -15278,8 +15297,7 @@ function validateIrohOrigin(value, endpointId, localPort = DEFAULT_IROH_LOCAL_PO
15278
15297
  if (value !== expected) throw new Error("iroh origin does not match the selected endpoint and local port");
15279
15298
  return expected;
15280
15299
  }
15281
- //#endregion
15282
- //#region src/iroh-pairing.ts
15300
+ // src/iroh-pairing.ts
15283
15301
  /** Short-lived iroh pairing descriptors for QR transport.
15284
15302
  *
15285
15303
  * Byte-compatible with the independent `dsh-mobile-iroh` companion's
@@ -15372,6 +15390,14 @@ function validatePairingDescriptor(value, options = {}) {
15372
15390
  function encodePairingDescriptor(value, options = {}) {
15373
15391
  return JSON.stringify(validatePairingDescriptor(value, options));
15374
15392
  }
15393
+ /** Draw the canonical pairing payload with its four-module quiet zone inside the SVG. */
15394
+ function renderIrohPairingQr(encoded) {
15395
+ return QRCode.toString(encoded, {
15396
+ type: "svg",
15397
+ margin: 4,
15398
+ errorCorrectionLevel: "M"
15399
+ });
15400
+ }
15375
15401
  /** Decode untrusted QR text, validating its expiry and public CA before use. */
15376
15402
  function decodePairingDescriptor(encoded, options = {}) {
15377
15403
  if (typeof encoded !== "string" || Buffer.byteLength(encoded, "utf8") > 2200) reject("pairing_descriptor_too_large");
@@ -15387,8 +15413,7 @@ function decodePairingDescriptor(encoded, options = {}) {
15387
15413
  }
15388
15414
  /** Phone-side listener port advertised in QR codes minted by this computer. */
15389
15415
  const PAIRING_DESCRIPTOR_LOCAL_PORT = DEFAULT_IROH_LOCAL_PORT;
15390
- //#endregion
15391
- //#region src/iroh.ts
15416
+ // src/iroh.ts
15392
15417
  /** Experimental iroh remote transport as a first-class remote provider.
15393
15418
  *
15394
15419
  * The sidecar is distributed by this plugin itself: an explicit install
@@ -15939,8 +15964,7 @@ var IrohController = class {
15939
15964
  });
15940
15965
  }
15941
15966
  };
15942
- //#endregion
15943
- //#region src/iroh-tls.ts
15967
+ // src/iroh-tls.ts
15944
15968
  /** Self-signed TLS identity for the experimental iroh gateway.
15945
15969
  *
15946
15970
  * Reuses this repository's managed CA path (`managed-setup.ts`) instead of
@@ -16107,8 +16131,7 @@ function irohGatewayConfig(template, endpointId, stateFile, tls, listenPort = 0)
16107
16131
  pairingTtlMs: 3e5
16108
16132
  });
16109
16133
  }
16110
- //#endregion
16111
- //#region src/task-events.ts
16134
+ // src/task-events.ts
16112
16135
  const TASK_EVENT_DEBOUNCE_MS = 1e3;
16113
16136
  function turnNumber(value) {
16114
16137
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
@@ -16185,8 +16208,99 @@ var TaskEventHub = class {
16185
16208
  return this.sinks.size;
16186
16209
  }
16187
16210
  };
16188
- //#endregion
16189
- //#region src/diagnostics.ts
16211
+ // src/diagnostic-network-context.ts
16212
+ function addressKind(address) {
16213
+ if (isIP(address) === 4) {
16214
+ const [first, second] = address.split(".").map(Number);
16215
+ if (first === 100 && second >= 64 && second <= 127) return "tailnet";
16216
+ if (first === 198 && (second === 18 || second === 19)) return "fake-ip";
16217
+ if (first === 0 || first === 10 || first === 127 || first >= 224 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168) return "private";
16218
+ return "public";
16219
+ }
16220
+ if (isIP(address) === 6) {
16221
+ const lower = address.toLowerCase();
16222
+ if (lower.startsWith("::ffff:") && isIP(lower.slice(7)) === 4) return addressKind(lower.slice(7));
16223
+ if (lower.startsWith("fd7a:115c:a1e0:")) return "tailnet";
16224
+ if (lower === "::" || lower === "::1" || /^(?:f[cd]|fe[89ab]|ff)/u.test(lower)) return "private";
16225
+ return "public";
16226
+ }
16227
+ return "unknown";
16228
+ }
16229
+ /**
16230
+ * Collapse system resolver results into non-identifying categories, without retaining addresses.
16231
+ * @param addresses - Addresses supplied by the system resolver.
16232
+ * @returns Address category and observed IP families; mixed answers are not treated as tailnet-only.
16233
+ */
16234
+ function classifyDiagnosticDns(addresses) {
16235
+ const kinds = new Set(addresses.map((entry) => addressKind(entry.address)));
16236
+ const families = new Set(addresses.map((entry) => isIP(entry.address)).filter((value) => value !== 0));
16237
+ return {
16238
+ kind: kinds.size === 0 ? "unknown" : kinds.size === 1 ? [...kinds][0] : "mixed",
16239
+ family: families.size === 0 ? "unknown" : families.size > 1 ? "dual" : families.has(4) ? "ipv4" : "ipv6"
16240
+ };
16241
+ }
16242
+ /**
16243
+ * Read bounded system-DNS context; never consult an external DNS service or modify settings.
16244
+ * @param hostname - The configured endpoint's hostname.
16245
+ * @param budgetMs - Maximum wait; OS lookup completion itself cannot be cancelled.
16246
+ * @param resolve - System lookup or an instance-local controlled fixture.
16247
+ * @returns Unknown on failure/deadline, not a conclusion that DNS or the public service is down.
16248
+ */
16249
+ async function probeDiagnosticDns(hostname, budgetMs, resolve = (host) => lookup(host, { all: true })) {
16250
+ let timer;
16251
+ try {
16252
+ return classifyDiagnosticDns(await Promise.race([resolve(hostname), new Promise((_resolve, reject) => {
16253
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("dns_context_timeout")), Math.max(1, budgetMs));
16254
+ timer.unref();
16255
+ })]));
16256
+ } catch {
16257
+ return {
16258
+ kind: "unknown",
16259
+ family: "unknown"
16260
+ };
16261
+ } finally {
16262
+ clearTimeout(timer);
16263
+ }
16264
+ }
16265
+ /**
16266
+ * Classify fixed transport machine codes without reflecting messages, URLs or nested errors.
16267
+ * @param error - A failed endpoint request.
16268
+ * @returns The last observed transport stage, or unknown rather than a guessed cause.
16269
+ */
16270
+ function diagnosticFailureStage(error) {
16271
+ let current = error;
16272
+ for (let depth = 0; depth < 4 && current !== null && typeof current === "object"; depth++) {
16273
+ const code = "code" in current && typeof current.code === "string" ? current.code : "";
16274
+ if ("name" in current && current.name === "TimeoutError") return "timeout";
16275
+ if (["ENOTFOUND", "EAI_AGAIN"].includes(code)) return "dns";
16276
+ if ([
16277
+ "ECONNREFUSED",
16278
+ "ECONNRESET",
16279
+ "EPIPE",
16280
+ "ENETUNREACH",
16281
+ "EHOSTUNREACH",
16282
+ "UND_ERR_SOCKET"
16283
+ ].includes(code)) return "connect";
16284
+ if ([
16285
+ "ETIMEDOUT",
16286
+ "UND_ERR_CONNECT_TIMEOUT",
16287
+ "UND_ERR_HEADERS_TIMEOUT",
16288
+ "UND_ERR_BODY_TIMEOUT"
16289
+ ].includes(code)) return "timeout";
16290
+ if ([
16291
+ "ERR_TLS_CERT_ALTNAME_INVALID",
16292
+ "CERT_HAS_EXPIRED",
16293
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
16294
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
16295
+ "SELF_SIGNED_CERT_IN_CHAIN",
16296
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
16297
+ "ERR_SSL_WRONG_VERSION_NUMBER"
16298
+ ].includes(code)) return "tls";
16299
+ current = "cause" in current ? current.cause : void 0;
16300
+ }
16301
+ return "unknown";
16302
+ }
16303
+ // src/diagnostics.ts
16190
16304
  /** Recognize the active remote-web-ui boot hook without evaluating or modifying it. */
16191
16305
  function hasCompetingRemoteChannelBoot(rows) {
16192
16306
  return rows.some((row) => row.kind === "script" && row.text?.includes("__DSH_REMOTE_CHANNEL_BOOT__") === true);
@@ -16245,6 +16359,7 @@ function check(id, status, reason, label, detail, action, facts) {
16245
16359
  id,
16246
16360
  status,
16247
16361
  reason,
16362
+ scope: id === "versions" ? "configuration" : id === "phone-network" ? "phone" : "computer",
16248
16363
  ...facts === void 0 ? {} : { facts: Object.freeze(facts) },
16249
16364
  label,
16250
16365
  detail,
@@ -16489,28 +16604,6 @@ async function relayProbeThroughProxy(target, proxy, budgetMs = remoteDiagnostic
16489
16604
  hopRequest.end();
16490
16605
  });
16491
16606
  }
16492
- /** Bounded supplementary DNS context for a failed direct attempt. */
16493
- async function unreachableWithDnsContext(hostname, budgetMs) {
16494
- let fakeIp = false;
16495
- let timer;
16496
- try {
16497
- fakeIp = (await Promise.race([lookup(hostname, { all: true }), new Promise((_resolve, reject) => {
16498
- timer = setTimeout(() => {
16499
- reject(/* @__PURE__ */ new Error("dns_context_timeout"));
16500
- }, Math.max(1, budgetMs));
16501
- timer.unref();
16502
- })])).some(({ address }) => {
16503
- const [first, second] = address.split(".").map(Number);
16504
- return first === 198 && (second === 18 || second === 19);
16505
- });
16506
- } catch {} finally {
16507
- clearTimeout(timer);
16508
- }
16509
- return {
16510
- state: "unreachable",
16511
- ...fakeIp ? { fakeIp: true } : {}
16512
- };
16513
- }
16514
16607
  async function fetchRemoteHealth(target, budgetMs) {
16515
16608
  const started = performance.now();
16516
16609
  try {
@@ -16520,19 +16613,28 @@ async function fetchRemoteHealth(target, budgetMs) {
16520
16613
  signal: AbortSignal.timeout(Math.max(1, budgetMs - 500))
16521
16614
  });
16522
16615
  const latencyMs = Math.max(0, Math.round(performance.now() - started));
16523
- if (response.status === 429) return {
16524
- state: "rate-limited",
16525
- latencyMs
16526
- };
16527
- return response.ok ? {
16616
+ if (response.status === 429) {
16617
+ await response.body?.cancel();
16618
+ return {
16619
+ state: "rate-limited",
16620
+ latencyMs
16621
+ };
16622
+ }
16623
+ const observation = response.ok ? {
16528
16624
  state: "ready",
16529
16625
  latencyMs
16530
16626
  } : {
16531
16627
  state: "unreachable",
16532
- latencyMs
16628
+ latencyMs,
16629
+ failureStage: "http"
16630
+ };
16631
+ await response.body?.cancel();
16632
+ return observation;
16633
+ } catch (error) {
16634
+ return {
16635
+ state: "unreachable",
16636
+ failureStage: diagnosticFailureStage(error)
16533
16637
  };
16534
- } catch {
16535
- return unreachableWithDnsContext(target.hostname, Math.max(1, budgetMs - (performance.now() - started)));
16536
16638
  }
16537
16639
  }
16538
16640
  /**
@@ -16555,7 +16657,14 @@ async function probeRemoteHealth(origin, options = {}) {
16555
16657
  return { state: "not-applicable" };
16556
16658
  }
16557
16659
  const budgetMs = remoteDiagnosticTimeoutMs(origin);
16558
- const observation = await (options.direct ?? fetchRemoteHealth)(target, budgetMs);
16660
+ const direct = options.direct ?? fetchRemoteHealth;
16661
+ const [observed, dns] = await Promise.all([direct(target, budgetMs), options.direct !== void 0 && options.lookup === void 0 ? void 0 : probeDiagnosticDns(target.hostname, Math.min(750, budgetMs), options.lookup)]);
16662
+ const context = dns === void 0 ? {} : { dns };
16663
+ const observation = {
16664
+ ...observed,
16665
+ ...context,
16666
+ ...dns?.kind === "fake-ip" ? { fakeIp: true } : {}
16667
+ };
16559
16668
  if (observation.state !== "unreachable") return observation;
16560
16669
  const relay = configuredProxyFor(origin, options.env ?? process.env);
16561
16670
  if (relay === void 0) return observation;
@@ -16565,12 +16674,14 @@ async function probeRemoteHealth(origin, options = {}) {
16565
16674
  if (status === 429) return {
16566
16675
  state: "rate-limited",
16567
16676
  latencyMs,
16568
- viaProxy: true
16677
+ viaProxy: true,
16678
+ ...context
16569
16679
  };
16570
16680
  return status >= 200 && status < 300 ? {
16571
16681
  state: "ready",
16572
16682
  latencyMs,
16573
- viaProxy: true
16683
+ viaProxy: true,
16684
+ ...context
16574
16685
  } : observation;
16575
16686
  } catch {
16576
16687
  return observation;
@@ -16580,28 +16691,38 @@ async function defaultRemoteProbe(origin) {
16580
16691
  return probeRemoteHealth(origin);
16581
16692
  }
16582
16693
  function reportLine(entry) {
16583
- return `[${entry.status.toUpperCase()}] ${entry.label}: ${entry.detail}${entry.action === void 0 ? "" : ` ${entry.action}`}`;
16694
+ return `[${entry.status.toUpperCase()}][${entry.scope}] ${entry.label}: ${entry.detail}${entry.action === void 0 ? "" : ` ${entry.action}`}`;
16584
16695
  }
16585
16696
  /** Run bounded read-only checks and return a report safe to paste into an issue. */
16586
16697
  async function collectConnectionDiagnostics(snapshot, probes = {}) {
16587
16698
  const checks = [];
16588
16699
  const remoteProbe = snapshot.remote.provider !== "origin" && snapshot.remote.provider !== "iroh" && snapshot.remote.running && snapshot.remote.state === "ready" && snapshot.remote.origin !== void 0 ? (probes.remote ?? defaultRemoteProbe)(snapshot.remote.origin) : Promise.resolve({ state: "not-applicable" });
16589
- const [firewall, remoteObservation] = await Promise.all([(probes.firewall ?? defaultFirewallProbe())(snapshot.lan.port), remoteProbe]);
16590
- checks.push(check("versions", "ok", "versions-current", "版本兼容", `插件 ${DSH_MOBILE_VERSION},DSH ${snapshot.dshVersion},Android App 最低 ${MINIMUM_ANDROID_APP_VERSION}。`));
16700
+ const [firewall, remoteObservation] = await Promise.all([snapshot.lan.running ? (probes.firewall ?? defaultFirewallProbe())(snapshot.lan.port) : Promise.resolve({ state: "not-applicable" }), remoteProbe]);
16701
+ checks.push(check("versions", "ok", "versions-current", "版本信息", `插件 ${DSH_MOBILE_VERSION},DSH ${snapshot.dshVersion},Android App 最低 ${MINIMUM_ANDROID_APP_VERSION}。`));
16591
16702
  if (snapshot.competingRemoteChannelBoot === true) checks.push(check("remote-plugin-conflict", "warning", "competing-remote-channel", "第三方远程插件", "检测到另一套远程请求通道脚本;手机页面可能进入该插件自己的配对流程。", "在电脑端关闭 dsh-remote-web-ui 的远程访问后刷新,再使用 DSH Mobile 当前二维码配对;不要混用两种配对码。"));
16592
- if (snapshot.lan.configured === false) checks.push(check("network", "error", "lan-setup-required", "局域网配置", "尚未完成局域网初始化。", "返回局域网页选择网卡并完成配置。"));
16593
- else if (snapshot.lan.networkError !== void 0) checks.push(check("network", "error", "network-unavailable", "局域网网卡", "已保存的网卡当前不可用。", "重新运行 dsh-mobile setup。"));
16703
+ if (snapshot.indexInspectionUnavailable === true) checks.push(check("plugin-inspection", "warning", "plugin-inspection-unavailable", "页面脚本检查", "本次未能检查活动页面脚本;不能据此认定不存在其他远程通道。", "其余网络项目独立检查;保留复现环境并核对相关插件。"));
16704
+ if (snapshot.lan.configured === false) checks.push(check("network", snapshot.lan.running ? "error" : "info", "lan-setup-required", "局域网配置", "尚未完成局域网初始化。", "需要局域网连接时,返回局域网页选择网卡并完成配置。"));
16705
+ else if (snapshot.lan.networkError !== void 0) checks.push(check("network", snapshot.lan.running ? "error" : "info", "network-unavailable", "局域网网卡", "已保存的网卡当前不可用。", snapshot.lan.running ? "在局域网页重新确认网卡配置。" : "局域网未启用;此配置不用于判断远程入口。"));
16594
16706
  else if (snapshot.lan.configuredInterface !== void 0) {
16595
16707
  const interfaceName = snapshot.lan.interfaceName ?? snapshot.lan.configuredInterface;
16596
16708
  checks.push(check("network", "ok", "network-interface", "局域网网卡", `正在跟随 ${interfaceName}。`, void 0, { interfaceName }));
16597
16709
  } else checks.push(check("network", "info", "network-fixed", "局域网网卡", "当前使用固定网络配置。"));
16598
16710
  if (snapshot.lan.running && snapshot.lan.origin !== void 0) {
16599
16711
  const endpointSuffix = maskLanOrigin(snapshot.lan.origin);
16600
- checks.push(check("lan", "ok", "lan-ready", "局域网网关", `已监听 ${endpointSuffix},配对入口可用。`, void 0, { endpointSuffix }));
16712
+ checks.push(check("lan", "ok", "lan-ready", "局域网网关", `电脑网关已监听 ${endpointSuffix};尚未验证手机能否到达。`, void 0, { endpointSuffix }));
16601
16713
  } else checks.push(check("lan", "info", "lan-off", "局域网网关", "当前未开启。", "需要手机直连时开启局域网访问。"));
16602
- if (firewall.state === "ready") checks.push(check("firewall", "ok", "firewall-ready", "Windows 防火墙", "局域网 TCP 与发现规则已启用。"));
16603
- else if (firewall.state === "missing") checks.push(check("firewall", "warning", "firewall-missing", "Windows 防火墙", "未找到完整的局域网放行规则。", "以管理员身份重新运行 dsh-mobile setup。"));
16714
+ if (!snapshot.lan.running) checks.push(check("firewall", "info", "firewall-inactive", "Windows 防火墙", "局域网未启用,本次未检查局域网防火墙规则。", "不据此判断远程入口;需要局域网时再启用并检查。"));
16715
+ else if (firewall.state === "ready") checks.push(check("firewall", "ok", "firewall-ready", "Windows 防火墙", "局域网 TCP 与发现规则已启用。"));
16716
+ else if (firewall.state === "missing") checks.push(check("firewall", snapshot.lan.running ? "warning" : "info", "firewall-missing", "Windows 防火墙", "未找到完整的局域网放行规则。", snapshot.lan.running ? "确认需要局域网连接,再检查相应规则;不要关闭整个防火墙。" : "局域网未启用,不据此判断远程入口失败。"));
16604
16717
  else if (firewall.state === "unknown") checks.push(check("firewall", "info", "firewall-unknown", "Windows 防火墙", "系统未允许插件读取防火墙状态。", "若手机找不到电脑,以管理员身份重新运行 setup。"));
16718
+ if (snapshot.remote.running && snapshot.remote.state === "ready") checks.push(check("remote-service", "ok", "remote-service-ready", "本机通道状态", "提供方报告本机组件已就绪;不代表公网或手机路径已验证。", void 0, { provider: snapshot.remote.provider }));
16719
+ if (remoteObservation.dns !== void 0) {
16720
+ const kind = remoteObservation.dns.kind;
16721
+ checks.push(check("remote-dns", kind === "fake-ip" ? "warning" : "info", `dns-${kind}`, "系统 DNS", `电脑系统解析分类:${kind};IP:${remoteObservation.dns.family}。未查询公共 DNS,地址不写入报告。`, void 0, {
16722
+ dnsKind: kind,
16723
+ dnsFamily: remoteObservation.dns.family
16724
+ }));
16725
+ }
16605
16726
  if (!snapshot.remote.running || snapshot.remote.state === "off") checks.push(check("remote", "info", "remote-off", "远程通道", "当前未启用。", void 0, { provider: snapshot.remote.provider }));
16606
16727
  else if (snapshot.remote.provider === "origin" && snapshot.remote.state === "ready") checks.push(check("remote", "info", "remote-origin-ready", "远程通道", "私网 HTTP 后端已就绪;未检测公网 HTTPS、证书或 WebSocket 连通性。", "在手机上经反向代理验证连接;代理须保留原始 Host(含端口)并支持 WebSocket。", { provider: "origin" }));
16607
16728
  else if (snapshot.remote.provider === "iroh" && snapshot.remote.state === "ready") checks.push(check("remote", "info", "remote-iroh-ready", "远程通道", "iroh 通道已就绪;端点身份为回环 TLS,不从电脑端探测手机可达性。", "在手机上用配对二维码验证连接;二维码过期或通道报错时重新生成并重连。", { provider: "iroh" }));
@@ -16613,10 +16734,14 @@ async function collectConnectionDiagnostics(snapshot, probes = {}) {
16613
16734
  ...remoteObservation.latencyMs === void 0 ? {} : { latencyMs: remoteObservation.latencyMs },
16614
16735
  ...remoteObservation.viaProxy === true ? { viaProxy: true } : {}
16615
16736
  };
16616
- if (remoteObservation.state === "ready") checks.push(check("remote", remoteObservation.viaProxy === true ? "warning" : "ok", "remote-ready", "远程通道", remoteObservation.viaProxy === true ? `电脑经代理可访问 ${endpointSuffix},但尚未验证手机能否连接。` : `${snapshot.remote.provider} 公共地址 ${endpointSuffix} 可达,往返约 ${String(remoteObservation.latencyMs ?? 0)} ms。`, remoteObservation.viaProxy === true ? "请在手机上测试远程连接。" : void 0, facts));
16737
+ if (remoteObservation.state === "ready") checks.push(check("remote", remoteObservation.viaProxy === true ? "warning" : "ok", "remote-ready", "远程通道", remoteObservation.viaProxy === true ? `电脑经代理可访问 ${endpointSuffix},但尚未验证手机能否连接。` : `电脑当前出口可访问 ${snapshot.remote.provider} 入口 ${endpointSuffix},往返约 ${String(remoteObservation.latencyMs ?? 0)} ms;公网及手机路径未独立验证。`, remoteObservation.viaProxy === true ? "请在手机上测试远程连接。" : void 0, facts));
16617
16738
  else if (remoteObservation.state === "rate-limited") checks.push(check("remote", "warning", "remote-rate-limited", "远程通道", remoteObservation.viaProxy === true ? "电脑经代理访问时观察到限流,尚未验证手机能否连接。" : "公共地址可达,但本次检查观察到服务限流。", "稍后重试;旧会话会按需加载以减少流量。", facts));
16618
- else if (snapshot.remote.provider === "tailscale" && remoteObservation.fakeIp === true) checks.push(check("remote", "error", "remote-fake-ip", "远程通道", "Tailscale 地址被当前 VPN 或 DNS 代理接管,但 TLS 链路未建立。", "切换 VPN 节点或代理模式;仍失败时改用 cpolar。", facts));
16619
- else checks.push(check("remote", "error", "remote-unreachable", "远程通道", "提供方显示已就绪,但公共地址暂不可达。", "点击“重新连接”;仍失败时检查提供方状态。", facts));
16739
+ else if (snapshot.remote.provider === "tailscale" && remoteObservation.dns?.kind === "tailnet" && (remoteObservation.failureStage === "connect" || remoteObservation.failureStage === "timeout")) checks.push(check("remote", "warning", "remote-tailnet-shadowed", "电脑出口探测", "本机系统 DNS 返回 tailnet 地址,当前 Funnel-only 监听不接受此私网路径;本次失败不能证明公网 Funnel 故障。", "用 tailnet 外的网络单独验证;如自愿临时调整设备 DNS,注意其对其他私网服务的影响并在对照后恢复。", facts));
16740
+ else if (snapshot.remote.provider === "tailscale" && (remoteObservation.fakeIp === true || remoteObservation.dns?.kind === "fake-ip") && remoteObservation.failureStage !== "tls" && remoteObservation.failureStage !== "http") checks.push(check("remote", "error", "remote-fake-ip", "远程通道", "本机系统 DNS 返回 Fake-IP 网段,本次出口探测未成功;这不是公网故障的证明。", "核对 DNS 与代理路由,用其他网络对照,不要关闭证书验证。", facts));
16741
+ else {
16742
+ const reason = remoteObservation.failureStage === "dns" ? "remote-dns-failed" : remoteObservation.failureStage === "connect" ? "remote-connect-failed" : remoteObservation.failureStage === "tls" ? "remote-tls-failed" : remoteObservation.failureStage === "timeout" ? "remote-probe-timeout" : remoteObservation.failureStage === "http" ? "remote-http-failed" : "remote-unreachable";
16743
+ checks.push(check("remote", "error", reason, "电脑出口探测", "电脑当前出口的探测未通过;不代表其他网络或手机一定失败。", "按失败阶段核对 DNS、路由、证书或代理,并用手机当前网络对照。", facts));
16744
+ }
16620
16745
  } else if (snapshot.remote.state === "starting" || snapshot.remote.state === "connecting" || snapshot.remote.state === "needs-login") {
16621
16746
  const needsLogin = snapshot.remote.state === "needs-login";
16622
16747
  checks.push(check("remote", "warning", needsLogin ? "remote-needs-login" : "remote-connecting", "远程通道", needsLogin ? "等待完成 Tailscale 登录。" : "仍在建立连接。", needsLogin ? "返回远程页继续登录。" : "等待片刻后重新检查。", { provider: snapshot.remote.provider }));
@@ -16627,15 +16752,16 @@ async function collectConnectionDiagnostics(snapshot, probes = {}) {
16627
16752
  controllerCode
16628
16753
  }));
16629
16754
  }
16630
- checks.push(check("phone-network", "info", "phone-network-unknown", "手机网络", "电脑无法判断路由器是否隔离了手机。", "局域网仍失败时,确认手机与电脑在同一网络,并关闭访客网络或 AP 隔离。"));
16755
+ checks.push(check("phone-network", "info", "phone-network-unknown", "手机网络", "电脑没有验证手机网络、认证页面、资源加载或 WebSocket。", "请用手机当前网络实际连接,记录失败阶段;不要提供配对凭据或直接关闭安全隔离措施。"));
16631
16756
  const overall = checks.some((entry) => entry.status === "error") ? "error" : checks.some((entry) => entry.status === "warning") ? "attention" : "ok";
16632
- const summary = overall === "ok" ? "连接基础检查正常。" : overall === "attention" ? "发现需要留意的项目。" : "发现会影响连接的问题。";
16757
+ const summary = overall === "ok" ? "电脑端基础检查完成;手机、认证页面、资源与 WebSocket 尚未验证。" : overall === "attention" ? "电脑端检查有待确认项目;请区分测量路径与实际手机连接。" : "电脑端检查有失败项目;不据此推断所有网络均不可达。";
16633
16758
  const report = [
16634
16759
  "DSH Mobile 诊断报告",
16635
16760
  `生成时间: ${(/* @__PURE__ */ new Date()).toISOString()}`,
16636
16761
  `版本: plugin=${DSH_MOBILE_VERSION}; dsh=${snapshot.dshVersion}; min-app=${MINIMUM_ANDROID_APP_VERSION}`,
16637
16762
  `LAN: ${snapshot.lan.running ? "on" : "off"}; endpoint=${maskLanOrigin(snapshot.lan.origin)}`,
16638
16763
  `Remote: provider=${snapshot.remote.provider}; state=${snapshot.remote.state}; endpoint=${remoteSuffix(snapshot.remote.origin)}`,
16764
+ "Coverage: computer-side only; phone, authenticated page, resources and WebSocket remain unverified.",
16639
16765
  ...checks.map(reportLine)
16640
16766
  ].join("\n");
16641
16767
  return Object.freeze({
@@ -16649,11 +16775,19 @@ async function collectConnectionDiagnostics(snapshot, probes = {}) {
16649
16775
  }),
16650
16776
  summary,
16651
16777
  checks: Object.freeze(checks),
16652
- report
16778
+ report,
16779
+ coverage: Object.freeze({
16780
+ measuredFrom: "computer",
16781
+ unverified: Object.freeze([
16782
+ "phone",
16783
+ "authenticated-page",
16784
+ "resources",
16785
+ "websocket"
16786
+ ])
16787
+ })
16653
16788
  });
16654
16789
  }
16655
- //#endregion
16656
- //#region src/mobile-guide.ts
16790
+ // src/mobile-guide.ts
16657
16791
  /** Compose the guide with the current customization state injected. */
16658
16792
  function buildMobileGuide(state) {
16659
16793
  const styleLine = state.hasCustomCss ? "存在(已检测到文件,需在已配对页面验证加载)" : "不存在(使用内置默认样式)";
@@ -16718,8 +16852,7 @@ const MOBILE_CUSTOMIZATION_GUIDE_BODY = `你在为用户定制 DSH Mobile 的手
16718
16852
  - 创建或修改扩展后,确认 extension.json 的 schemaVersion 为 1、id 与目录名一致、且 id 只含小写字母数字和连字符
16719
16853
  - 扩展的 host.mjs 若在完成前无法激活,先修正而不是留下损坏的扩展
16720
16854
  - 完成后检查自己实际写入了哪些文件,向用户简要说明改了什么、手机端会有什么变化`;
16721
- //#endregion
16722
- //#region src/vps-deploy.ts
16855
+ // src/vps-deploy.ts
16723
16856
  const FRP_VERSION = "0.70.1";
16724
16857
  const SSH_TIMEOUT_MS = 3e5;
16725
16858
  const LINUX_ARTIFACTS = Object.freeze({
@@ -17690,8 +17823,7 @@ async function uninstallVps(serverAddress, uninstall, input, options = {}) {
17690
17823
  checks
17691
17824
  });
17692
17825
  }
17693
- //#endregion
17694
- //#region src/lan-rebind.ts
17826
+ // src/lan-rebind.ts
17695
17827
  /** An unreadable setup is rejected before stopping the listener or replacing any file. */
17696
17828
  var ManagedLanConfigurationError = class extends Error {
17697
17829
  constructor(cause) {
@@ -17728,8 +17860,7 @@ async function rebindManagedLan(options) {
17728
17860
  running: result.running
17729
17861
  };
17730
17862
  }
17731
- //#endregion
17732
- //#region src/funnel.ts
17863
+ // src/funnel.ts
17733
17864
  const MAX_PROTOCOL_LINE_BYTES = 16384;
17734
17865
  const FUNNEL_START_TIMEOUT_MS = 45e3;
17735
17866
  function publicStatus$1(status) {
@@ -18243,8 +18374,7 @@ function funnelExecutable(importMetaUrl, environment = process.env) {
18243
18374
  const file = `dsh-mobile-funnel-${process.platform}-${process.arch}${suffix}`;
18244
18375
  return resolve(fileURLToPath(new URL(`../bin/${file}`, importMetaUrl)));
18245
18376
  }
18246
- //#endregion
18247
- //#region src/cpolar.ts
18377
+ // src/cpolar.ts
18248
18378
  const MAX_LOG_BUFFER_BYTES = 65536;
18249
18379
  const START_TIMEOUT_MS = 45e3;
18250
18380
  const CPOLAR_HOST_SUFFIXES = Object.freeze([
@@ -18660,8 +18790,7 @@ var CpolarController = class {
18660
18790
  ], "cpolar resource cleanup failed");
18661
18791
  }
18662
18792
  };
18663
- //#endregion
18664
- //#region src/caddy-files.ts
18793
+ // src/caddy-files.ts
18665
18794
  /** Refuse link-shaped ancestors within Caddy's owned state root. */
18666
18795
  async function assertCaddyParents(root, target) {
18667
18796
  await assertManagedParents(root, target, "caddy");
@@ -18682,8 +18811,7 @@ async function writeCaddyPrivateFile(root, file, body) {
18682
18811
  async function removeCaddyTree(root, target) {
18683
18812
  await removeManagedTree(root, target, "caddy");
18684
18813
  }
18685
- //#endregion
18686
- //#region src/caddy-component.ts
18814
+ // src/caddy-component.ts
18687
18815
  const CADDY_VERSION = "2.11.7";
18688
18816
  const CADDY_DNS_PLUGIN_VERSION = "v0.4.3";
18689
18817
  const CADDY_DOWNLOAD_PAGE = "https://github.com/saya-ch/dsh-mobile/releases/tag/caddy-component-2.11.7-tencentcloud-0.4.3-r1";
@@ -19202,8 +19330,7 @@ var CaddyConfigStore = class {
19202
19330
  return task;
19203
19331
  }
19204
19332
  };
19205
- //#endregion
19206
- //#region src/caddy.ts
19333
+ // src/caddy.ts
19207
19334
  /** Private directories override Caddy's platform defaults; ambient API keys and admin/proxy overrides are omitted. */
19208
19335
  function caddyProcessEnvironment(config, settings, environment = process.env) {
19209
19336
  const credentials = config.credentials();
@@ -19533,8 +19660,7 @@ var CaddyController = class {
19533
19660
  }
19534
19661
  }
19535
19662
  };
19536
- //#endregion
19537
- //#region src/managed-origin.ts
19663
+ // src/managed-origin.ts
19538
19664
  /** Children never independently restore a durable enabled switch. */
19539
19665
  function createVolatileRemoteControlStore() {
19540
19666
  let value = {
@@ -19715,8 +19841,7 @@ var ManagedOriginController = class {
19715
19841
  return task;
19716
19842
  }
19717
19843
  };
19718
- //#endregion
19719
- //#region src/release-update.ts
19844
+ // src/release-update.ts
19720
19845
  const PACKAGE_NAME = "dsh-mobile";
19721
19846
  const NPM_LATEST_URL = "https://registry.npmjs.org/dsh-mobile/latest";
19722
19847
  const ANDROID_RELEASE_DESCRIPTOR_URL = "https://raw.githubusercontent.com/saya-ch/dsh-mobile/main/apps/mobile/release.json";
@@ -20179,8 +20304,7 @@ var PluginReleaseManager = class {
20179
20304
  });
20180
20305
  }
20181
20306
  };
20182
- //#endregion
20183
- //#region src/file-logger.ts
20307
+ // src/file-logger.ts
20184
20308
  const MAX_LOG_BYTES = 5242880;
20185
20309
  const MAX_QUEUED_BYTES = 262144;
20186
20310
  const PATH_PREFIX = "mobile_log";
@@ -20290,14 +20414,12 @@ async function installMobileFileLogger(ctx, stateDirectory, options = {}) {
20290
20414
  }
20291
20415
  return file;
20292
20416
  }
20293
- //#endregion
20294
- //#region src/mobile-push/contracts.ts
20417
+ // src/mobile-push/contracts.ts
20295
20418
  const OUTCOME_COALESCE_MS = 1e3;
20296
20419
  const ATTENTION_TTL_MS = 3e5;
20297
20420
  const OUTCOME_TTL_MS = 18e5;
20298
20421
  const ATTEMPT_TIMEOUT_MS = 1e4;
20299
- //#endregion
20300
- //#region src/mobile-push/requests.ts
20422
+ // src/mobile-push/requests.ts
20301
20423
  var RequestLifetimes = class {
20302
20424
  #records = /* @__PURE__ */ new Map();
20303
20425
  #byCall = /* @__PURE__ */ new Map();
@@ -20477,8 +20599,7 @@ var RequestLifetimes = class {
20477
20599
  }, 250);
20478
20600
  }
20479
20601
  };
20480
- //#endregion
20481
- //#region src/mobile-push/outcomes.ts
20602
+ // src/mobile-push/outcomes.ts
20482
20603
  const isCompletedGoalPhase = (phase) => phase === "complete";
20483
20604
  var OutcomeLifetimes = class {
20484
20605
  #ctx;
@@ -20658,8 +20779,7 @@ var OutcomeLifetimes = class {
20658
20779
  return current;
20659
20780
  }
20660
20781
  };
20661
- //#endregion
20662
- //#region src/mobile-push/reconcile.ts
20782
+ // src/mobile-push/reconcile.ts
20663
20783
  const TURN_REASONS = [
20664
20784
  "completed",
20665
20785
  "error",
@@ -20931,8 +21051,7 @@ var PushReconciler = class {
20931
21051
  return verifyOutcome(source, values, agent);
20932
21052
  }
20933
21053
  };
20934
- //#endregion
20935
- //#region src/mobile-push/host.ts
21054
+ // src/mobile-push/host.ts
20936
21055
  function hasContract(ctx) {
20937
21056
  return typeof ctx.on === "function" && typeof ctx.sessionProjections?.stateOf === "function" && typeof ctx.sessionProjections.snapshot === "function" && typeof ctx.sessionProjections.register === "function" && typeof ctx.sessionQuery?.observeSession === "function" && typeof ctx.agents?.get === "function" && typeof ctx.agents.list === "function" && typeof ctx.agents.roots === "function" && typeof ctx.agents.isOwnedBy === "function" && ctx.userQuestions !== void 0 && ctx.approval !== void 0;
20938
21057
  }
@@ -21143,8 +21262,7 @@ function createPushHost(ctx) {
21143
21262
  }
21144
21263
  };
21145
21264
  }
21146
- //#endregion
21147
- //#region src/mobile-push/endpoint.ts
21265
+ // src/mobile-push/endpoint.ts
21148
21266
  const MAX_ENDPOINT_BYTES = 2048;
21149
21267
  const APPLE_SUFFIX = ".push.apple.com";
21150
21268
  const BASE64URL = /^[A-Za-z0-9_-]+$/u;
@@ -21261,8 +21379,7 @@ async function resolveAppleEndpoint(endpoint, resolver = systemResolver) {
21261
21379
  addresses: Object.freeze([...addresses])
21262
21380
  });
21263
21381
  }
21264
- //#endregion
21265
- //#region src/mobile-push/store.ts
21382
+ // src/mobile-push/store.ts
21266
21383
  const MAX_STATE_BYTES = 4194304;
21267
21384
  const emptySnapshot = () => ({
21268
21385
  version: 1,
@@ -21525,8 +21642,7 @@ var JsonPushStore = class {
21525
21642
  }
21526
21643
  }
21527
21644
  };
21528
- //#endregion
21529
- //#region src/mobile-push/registry.ts
21645
+ // src/mobile-push/registry.ts
21530
21646
  const empty = () => ({
21531
21647
  version: 1,
21532
21648
  ownerGenerations: [],
@@ -21914,8 +22030,7 @@ var PushRegistry = class {
21914
22030
  };
21915
22031
  }
21916
22032
  };
21917
- //#endregion
21918
- //#region src/mobile-push/payload.ts
22033
+ // src/mobile-push/payload.ts
21919
22034
  const messages = {
21920
22035
  en: {
21921
22036
  question: ["Input requested", "A response needs your attention."],
@@ -21967,8 +22082,7 @@ function buildDeclarativePayload(category, locale, navigate, tag) {
21967
22082
  if (Buffer.byteLength(payload, "utf8") > 3072) throw new TypeError("push_payload_too_large");
21968
22083
  return payload;
21969
22084
  }
21970
- //#endregion
21971
- //#region src/mobile-push/retention.ts
22085
+ // src/mobile-push/retention.ts
21972
22086
  /** Pure defer-membership retention helpers shared by delivery maintenance. */
21973
22087
  /** Removes deferred-recheck entries whose owning pending intent no longer exists; returns how many were removed. */
21974
22088
  function pruneOrphanedDeferrals(deferred, pending) {
@@ -21981,8 +22095,7 @@ function pruneOrphanedDeferrals(deferred, pending) {
21981
22095
  }
21982
22096
  return removed;
21983
22097
  }
21984
- //#endregion
21985
- //#region src/mobile-push/sender.ts
22098
+ // src/mobile-push/sender.ts
21986
22099
  const MAX_RESPONSE_BYTES = 8192;
21987
22100
  const MAX_TTL_SECONDS = 1800;
21988
22101
  const CONTENT_ENCODING = "aes128gcm";
@@ -22268,8 +22381,7 @@ var PushSender = class {
22268
22381
  }
22269
22382
  }
22270
22383
  };
22271
- //#endregion
22272
- //#region src/mobile-push/delivery.ts
22384
+ // src/mobile-push/delivery.ts
22273
22385
  const RECONCILE_INTERVAL_MS = 3e4;
22274
22386
  const RETRY_BACKOFF_MS = [1e3, 5e3];
22275
22387
  const MAX_TEST_QUEUE = 64;
@@ -23055,8 +23167,7 @@ var PushDelivery = class {
23055
23167
  }, delay);
23056
23168
  }
23057
23169
  };
23058
- //#endregion
23059
- //#region src/mobile-push/gateway.ts
23170
+ // src/mobile-push/gateway.ts
23060
23171
  /** One service shared by every Gateway while each Gateway owns a separate auth/origin attachment. */
23061
23172
  var PushGatewayCoordinator = class {
23062
23173
  host;
@@ -23179,8 +23290,7 @@ var PushGatewayCoordinator = class {
23179
23290
  return true;
23180
23291
  }
23181
23292
  };
23182
- //#endregion
23183
- //#region src/installation-id.ts
23293
+ // src/installation-id.ts
23184
23294
  const INSTALLATION_ID_PATTERN = /^[a-f0-9]{64}\n$/u;
23185
23295
  const INSTALLATION_ID_BYTES = 65;
23186
23296
  async function readInstallationId(file) {
@@ -23244,8 +23354,7 @@ async function loadOrCreateInstallationId(file) {
23244
23354
  }
23245
23355
  }
23246
23356
  }
23247
- //#endregion
23248
- //#region src/gateway-identity.ts
23357
+ // src/gateway-identity.ts
23249
23358
  /**
23250
23359
  * Attach the authenticated installation identity while retaining existing entry identity rules.
23251
23360
  * @param config - Validated gateway configuration containing the legacy identity.
@@ -23265,8 +23374,7 @@ function withInstallationIdentity(config, installationId, explicitIdentity) {
23265
23374
  legacyInstanceId: config.instanceId
23266
23375
  });
23267
23376
  }
23268
- //#endregion
23269
- //#region src/lan-setup.ts
23377
+ // src/lan-setup.ts
23270
23378
  const FIREWALL_TCP_RULE = "DSH Mobile HTTPS";
23271
23379
  const FIREWALL_UDP_RULE = "DSH Mobile Discovery";
23272
23380
  async function runElevatedPowerShell(script) {
@@ -23338,8 +23446,7 @@ async function prepareManagedLanSetup(options) {
23338
23446
  androidCertificate
23339
23447
  });
23340
23448
  }
23341
- //#endregion
23342
- //#region src/plugin.ts
23449
+ // src/plugin.ts
23343
23450
  /** Stable Cordis plugin name. */
23344
23451
  const name = "dsh-mobile";
23345
23452
  /** The stock WebServer serves the control card; Connection authenticates the loopback DSH origin. */
@@ -24281,7 +24388,8 @@ async function apply(ctx, config) {
24281
24388
  }, cpolarComponent.status(), cloudflaredComponent.status(), cloudflaredTunnel.status(), frpComponent.status(), frpConfig.status(), originConfig.status(), caddyComponent.status(), irohComponent.status()),
24282
24389
  originMode: originController.mode(),
24283
24390
  caddyConfiguration: caddyConfig.status(),
24284
- caddyState: caddyController.status()
24391
+ caddyState: caddyController.status(),
24392
+ ...process.platform === "darwin" ? { macosHost: true } : {}
24285
24393
  });
24286
24394
  const lanPayload = () => ({
24287
24395
  ...loaded.kind === "unconfigured" ? {
@@ -24324,7 +24432,18 @@ async function apply(ctx, config) {
24324
24432
  windowsFirewall: process.platform === "win32"
24325
24433
  };
24326
24434
  };
24327
- const diagnosticsPayload = async () => {
24435
+ const diagnosticsPayload = async (request) => {
24436
+ const admission = ctx.connection?.admit?.(request);
24437
+ if (admission !== void 0 && "rejection" in admission) throw new HttpError(admission.rejection, "authentication_failed");
24438
+ const indexCollector = ctx.webServer;
24439
+ let indexInjections = [];
24440
+ let indexInspectionUnavailable = false;
24441
+ if (typeof indexCollector.collectIndexInjections === "function") try {
24442
+ indexInjections = indexCollector.collectIndexInjections(admission?.peer);
24443
+ } catch (inspectionError) {
24444
+ indexInspectionUnavailable = true;
24445
+ }
24446
+ else indexInspectionUnavailable = true;
24328
24447
  let interfaceName;
24329
24448
  let networkError;
24330
24449
  if (loaded.kind === "managed") try {
@@ -24354,7 +24473,8 @@ async function apply(ctx, config) {
24354
24473
  } : void 0;
24355
24474
  return collectConnectionDiagnostics({
24356
24475
  dshVersion,
24357
- competingRemoteChannelBoot: hasCompetingRemoteChannelBoot(ctx.webServer.collectIndexInjections?.() ?? []),
24476
+ competingRemoteChannelBoot: hasCompetingRemoteChannelBoot(indexInjections),
24477
+ indexInspectionUnavailable,
24358
24478
  lan: {
24359
24479
  configured: loaded.kind !== "unconfigured" || preparedLanSetup !== void 0,
24360
24480
  running: lanController.isRunning(),
@@ -24425,12 +24545,15 @@ async function apply(ctx, config) {
24425
24545
  }
24426
24546
  if (request.method !== "POST") throw new HttpError(405, "method_not_allowed");
24427
24547
  const body = await readJsonObject(request, template.maxBodyBytes);
24428
- if (Object.keys(body).length !== 1 || Object.keys(body).some((key) => key !== "reset" && key !== "excludedClientModules" && key !== "measure") || "measure" in body && body.measure !== true) throw new HttpError(400, "bad_request");
24429
- sendJson(response, 200, body.measure === true ? await gateway.measureClientModules(void 0, operation.signal) : body.reset === true ? await gateway.resetClientModules(void 0, operation.signal) : await gateway.configureClientModules(body.excludedClientModules, void 0, operation.signal), false);
24548
+ const keys = Object.keys(body);
24549
+ const configure = keys.every((key) => key === "excludedClientModules" || key === "revision") && Object.prototype.hasOwnProperty.call(body, "excludedClientModules") && (body.revision === void 0 || typeof body.revision === "string");
24550
+ const single = keys.length === 1 && (body.reset === true || body.measure === true);
24551
+ if (!configure && !single) throw new HttpError(400, "bad_request");
24552
+ sendJson(response, 200, body.measure === true ? await gateway.measureClientModules(void 0, operation.signal) : body.reset === true ? await gateway.resetClientModules(void 0, operation.signal) : await gateway.configureClientModules(body.excludedClientModules, void 0, operation.signal, typeof body.revision === "string" ? body.revision : void 0), false);
24430
24553
  } catch (error) {
24431
24554
  if (!(error instanceof ClientModuleConflictError)) throw error;
24432
24555
  sendJson(response, 409, {
24433
- error: "excluded_client_modules_invalid",
24556
+ error: error.code,
24434
24557
  detail: error.detail
24435
24558
  }, false);
24436
24559
  } finally {
@@ -24461,7 +24584,7 @@ async function apply(ctx, config) {
24461
24584
  return;
24462
24585
  }
24463
24586
  if (request.method === "GET" && target.decodedPathname === `/api/mobile-access/diagnostics`) {
24464
- sendJson(response, 200, await diagnosticsPayload(), false);
24587
+ sendJson(response, 200, await diagnosticsPayload(request), false);
24465
24588
  return;
24466
24589
  }
24467
24590
  if (request.method === "GET" && target.decodedPathname === `/api/mobile-access/release`) {
@@ -24764,7 +24887,12 @@ async function apply(ctx, config) {
24764
24887
  if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/frp/configure`) {
24765
24888
  const body = await readJsonObject(request, 4096);
24766
24889
  await remoteProviders.mutate(async () => {
24767
- await frpConfig.configure(body);
24890
+ const saved = frpConfig.settings();
24891
+ const reuseToken = body.token === "" && saved !== void 0 && body.serverAddress === saved.serverAddress && body.serverPort === saved.serverPort;
24892
+ await frpConfig.configure(reuseToken ? {
24893
+ ...body,
24894
+ token: saved.token
24895
+ } : body);
24768
24896
  if (remoteControllers.frp.status().enabled) await remoteControllers.frp.reconnect();
24769
24897
  });
24770
24898
  sendJson(response, 200, remotePayload(), false);
@@ -24811,10 +24939,7 @@ async function apply(ctx, config) {
24811
24939
  });
24812
24940
  let qrSvg = "";
24813
24941
  try {
24814
- qrSvg = await QRCode.toString(pairing.encoded, {
24815
- type: "svg",
24816
- margin: 1
24817
- });
24942
+ qrSvg = await renderIrohPairingQr(pairing.encoded);
24818
24943
  } catch {}
24819
24944
  logger.info("iroh pairing window opened");
24820
24945
  sendJson(response, 201, {
@@ -25146,7 +25271,6 @@ async function apply(ctx, config) {
25146
25271
  };
25147
25272
  }, "dsh-mobile: independent LAN and selectable remote providers with /mobile command");
25148
25273
  }
25149
- //#endregion
25150
25274
  export { AUTH_PREFIX, AccessController, AccessError, BlockedUpgradePathLog, BoundedRateLimiter, CERT_EXPIRING_DAYS, CLOUDFLARED_COMPONENT_RELEASE, CLOUDFLARED_COMPONENT_RELEASES, CPOLAR_COMPONENT_RELEASE, CPOLAR_COMPONENT_RELEASES, CSRF_COOKIE, CSRF_HEADER, ClientModuleConflictError, ClientModulePreferenceStore, CloudflaredComponentManager, CloudflaredController, CloudflaredTunnelStore, Config, CpolarComponentManager, DEFAULT_IROH_LOCAL_PORT, DEFAULT_ORIGIN_LISTEN_PORT, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, EXTENSION_LIMITS, FRP_ATTACH_DISCOVERY_PATH, FRP_ATTACH_LOCAL_DECLARATION, FRP_ATTACH_SELF_SIGNED_DECLARATION, FRP_ATTACH_VPS_DECLARATION, FRP_CADDY_IMPORT_LINE, FRP_CADDY_SNIPPET_MARKER, FRP_CADDY_SNIPPET_PATH, FRP_COMPONENT_RELEASES, FRP_DEFAULT_PROXY_NAME, FRP_DEFAULT_PUBLIC_PORT, FRP_RESERVED_PORTS, FrpComponentManager, FrpConfigStore, FrpController, IROH_COMPONENT_RELEASES, IROH_LEAF_RENEW_BEFORE_DAYS, IROH_LEAF_VALIDITY_DAYS, IROH_VERSION, IrohComponentManager, IrohController, JsonDeviceStore, JsonMobileAccessControlStore, JsonRemoteProviderStore, LOCAL_ADMIN_PREFIX, MAX_BLOCKED_UPGRADE_PATHS, MAX_EXTRA_WEBSOCKET_PATHS, MAX_PAIRING_DESCRIPTOR_BYTES, MAX_PAIRING_LIFETIME_MS, MAX_WEBSOCKET_PATH_LENGTH, MemoryDeviceStore, MobileAccessGateway, MobileAccessGatewayController, MobileAccessService, MobileExtensionError, OriginConfigStore, OriginController, PAIRING_DESCRIPTOR_LOCAL_PORT, PAIRING_DESCRIPTOR_TYPE, PAIRING_DESCRIPTOR_VERSION, REMOTE_PROVIDERS, RequestTrustPolicy, SESSION_COOKIE, TASK_EVENT_DEBOUNCE_MS, TaskEventHub, WS_PATHS, WebSocketPathStore, addressAllowed, apply, assertExtensionId, configuredRemoteProvider, createCaddySite, createFrpAttachPlan, createFrpAttachTemplate, createFrpAttachTemplateParts, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, decodePairingDescriptor, encodePairingDescriptor, ensureFrpIngressCertificate, ensureIrohTls, evaluateCertificateLifetime, frpAttachVpsParts, frpIngressPaths, frpIngressSelfCheck, frpProxyName, inject, irohGatewayConfig, irohHostname, irohOrigin, irohTlsPaths, isCloudflaredRegistration, isFrpSelfSignedIngress, isGloballyRoutableIpv4, isLoopbackAddress, issueServerCertificate, mergeSavedCloudflaredTunnelSettings, mergeSavedFrpSettings, mergeSavedFrpTarget, name, normalizeWebSocketPaths, originGatewayConfig, parseAuthority, parseCidr, parseCloudflaredOrigin, parseCloudflaredTunnelSettings, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseOriginSettings, parseRemoteProviderState, probeOriginCertificate, readCertificateRenewal, resolveAuthority, resolveFrpEntryTls, resolveFrpMode, resolveFrpPublicPort, resolveFrpVhostHttpPort, resolveHostExecution, rewriteMobileIndex, validateAttachSettings, validateCloudflaredTunnelHostname, validateCloudflaredTunnelPort, validateCloudflaredTunnelToken, validateFrpEntryTls, validateFrpMode, validateFrpProxyName, validateFrpPublicOrigin, validateFrpPublicPort, validateFrpServerAddress, validateFrpServerPort, validateFrpToken, validateFrpVhostHttpPort, validateIrohOrigin, validateOriginAllowedCidrs, validateOriginListenHost, validateOriginListenPort, validateOriginPublicOrigin, validatePairingDescriptor, validateWebSocketPath, watchTaskCompletions };
25151
25275
 
25152
25276
  //# sourceMappingURL=index.mjs.map