dsh-mobile 0.6.17 → 0.7.0

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 (43) hide show
  1. package/CHANGELOG.md +47 -11
  2. package/CONTRIBUTORS.md +12 -6
  3. package/README.en.md +22 -14
  4. package/README.md +21 -14
  5. package/SECURITY.md +1 -1
  6. package/THIRD_PARTY_NOTICES.md +33 -0
  7. package/apps/mobile/README.md +28 -3
  8. package/apps/mobile/README.zh-CN.md +28 -3
  9. package/apps/mobile/release.json +3 -3
  10. package/cordis.patch.yml +5 -9
  11. package/docs/ATTACH_EXISTING_FRPS.en.md +32 -2
  12. package/docs/ATTACH_EXISTING_FRPS.md +2 -2
  13. package/docs/CLIENT_MODULES.en.md +2 -0
  14. package/docs/CLIENT_MODULES.md +2 -0
  15. package/docs/CLOUDFLARE_TUNNEL.en.md +2 -0
  16. package/docs/CLOUDFLARE_TUNNEL.md +2 -0
  17. package/docs/IROH.en.md +44 -0
  18. package/docs/IROH.md +44 -0
  19. package/docs/MOBILE_PLUGIN_ADAPTATION.en.md +196 -0
  20. package/docs/MOBILE_PLUGIN_ADAPTATION.md +196 -0
  21. package/docs/README.md +9 -0
  22. package/docs/SELF_HOSTED_ORIGIN.en.md +26 -1
  23. package/docs/SELF_HOSTED_ORIGIN.md +27 -2
  24. package/docs/SKIN_COMPATIBILITY.en.md +2 -0
  25. package/docs/SKIN_COMPATIBILITY.md +2 -0
  26. package/docs/SLOW_CONNECTIONS.en.md +10 -2
  27. package/docs/SLOW_CONNECTIONS.md +10 -2
  28. package/docs/TRUSTED_NETWORKS.en.md +17 -0
  29. package/docs/TRUSTED_NETWORKS.md +17 -0
  30. package/lib/cli.js.map +1 -1
  31. package/lib/client.js +1081 -70
  32. package/lib/client.js.map +1 -1
  33. package/lib/index.d.mts +271 -2
  34. package/lib/index.mjs +1908 -245
  35. package/lib/index.mjs.map +1 -1
  36. package/lib/mobile-layout.js +14 -6
  37. package/lib/mobile-layout.js.map +1 -1
  38. package/native/iroh-host/LICENSE.dsh-mobile-iroh +21 -0
  39. package/native/iroh-host/LICENSE.dsh-tether +21 -0
  40. package/native/iroh-host/NOTICE.dsh-mobile-iroh +13 -0
  41. package/native/iroh-host/README.md +66 -0
  42. package/package.json +18 -5
  43. package/packages/question-fixes/package.json +1 -1
package/lib/client.js CHANGED
@@ -1021,6 +1021,105 @@ window.__ModuleLoader__.load({
1021
1021
  frpStopped: "FRP 连接已停止。",
1022
1022
  frpExited: "frpc 意外退出,请检查 VPS 配置后重新连接。"
1023
1023
  });
1024
+ Object.assign(MOBILE_CONTROL_MESSAGES.en, {
1025
+ irohName: "iroh (Experimental)",
1026
+ irohExperimental: "Experimental",
1027
+ irohNotReady: "iroh is not running. Start the channel first, then generate a pairing code.",
1028
+ irohPairingFailed: "Could not open an iroh pairing window. Reconnect the channel and retry.",
1029
+ irohPairingTitle: "Phone pairing (experimental)",
1030
+ generateIrohQr: "Generate iroh pairing QR code",
1031
+ enlargeQr: "Enlarge the QR code",
1032
+ closeEnlargedQr: "Close the enlarged QR code",
1033
+ irohPairingReady: "iroh pairing code {code} is live for five minutes. Scan it from the iroh pairing screen; opening another code replaces this one.",
1034
+ irohDescription: "Direct encrypted connection with relay fallback. No public domain or tunnel account needed.",
1035
+ prepareIroh: "Prepare iroh",
1036
+ irohInstallNote: "The Install button downloads the pinned sidecar release (dsh-mobile-iroh 0.1.0-alpha.1, about 15 MB on Windows x64, 24 MB on Linux x64) into the plugin private directory and verifies its SHA-256. An installed companion with a verified copy is reused instead, with no download.",
1037
+ irohComponentNote: "An installed sidecar is re-verified by SHA-256 on every use. It is not bundled with the plugin package; a companion-provided copy can only be removed by removing the companion.",
1038
+ irohPhaseNote: "Experimental connection. Requires the full Android App 0.7.0+ with iroh; the compatibility APK and mobile browsers cannot use it.",
1039
+ irohUnavailable: "iroh is not ready: no sidecar is installed yet. Install the official component below to download and verify it.",
1040
+ irohStarting: "Resolving the iroh endpoint identity…",
1041
+ irohConnecting: "Sidecar connected; starting the DSH authentication gateway…",
1042
+ irohCompanionMissing: "No verified iroh sidecar is available. Use Install official component below; a separate companion plugin is optional.",
1043
+ irohDataDirectoryInvalid: "Could not prepare the private sidecar directory. Check that DSH can manage its own profile directory, then retry.",
1044
+ irohSidecarInvalid: "The installed sidecar failed checksum verification. Reinstall the component.",
1045
+ irohDownloadFailed: "The sidecar download failed before verification. Check the network and retry.",
1046
+ irohPlatformUnsupported: "iroh sidecars are published for Windows x64 and Linux x64 only.",
1047
+ irohIdentityFailed: "Could not read the iroh endpoint identity from the sidecar.",
1048
+ irohLaunchFailed: "The iroh sidecar failed to start.",
1049
+ irohExited: "The iroh sidecar exited unexpectedly. Reconnect.",
1050
+ irohTimeout: "Timed out waiting for the iroh sidecar. Reconnect.",
1051
+ irohInstallConfirm: "Download and verify the pinned iroh sidecar now (about 15 MB on Windows x64, 24 MB on Linux x64)? Nothing else is installed.",
1052
+ installingIroh: "Downloading and verifying the iroh sidecar…",
1053
+ switchingIroh: "Switching to iroh…",
1054
+ reconnectingIroh: "Reconnecting the iroh transport…",
1055
+ resetIrohConfirm: "Disable the iroh channel? The companion and its pairings stay installed."
1056
+ });
1057
+ Object.assign(MOBILE_CONTROL_MESSAGES.it, {
1058
+ irohName: "iroh (Sperimentale)",
1059
+ irohExperimental: "Sperimentale",
1060
+ irohNotReady: "iroh non è attivo. Avvia il canale e genera di nuovo il codice.",
1061
+ irohPairingFailed: "Impossibile aprire una finestra di abbinamento iroh. Riconnetti il canale e riprova.",
1062
+ irohPairingTitle: "Abbinamento telefono (sperimentale)",
1063
+ generateIrohQr: "Genera QR di abbinamento iroh",
1064
+ enlargeQr: "Ingrandisci il codice QR",
1065
+ closeEnlargedQr: "Chiudi il codice QR ingrandito",
1066
+ irohPairingReady: "Il codice iroh {code} è attivo per cinque minuti. Scansionalo dalla schermata di abbinamento iroh; aprirne un altro sostituisce questo.",
1067
+ irohDescription: "Connessione cifrata diretta con ripiego su relay. Nessun dominio pubblico o account tunnel richiesto.",
1068
+ prepareIroh: "Prepara iroh",
1069
+ irohInstallNote: "Il pulsante Installa scarica la release pinnata del sidecar (dsh-mobile-iroh 0.1.0-alpha.1, circa 15 MB su Windows x64, 24 MB su Linux x64) nella directory privata del plugin e ne verifica lo SHA-256. Un complemento installato con una copia verificata viene invece riutilizzato senza scaricare nulla.",
1070
+ irohComponentNote: "Un sidecar installato viene riverificato tramite SHA-256 a ogni uso. Non è incluso nel pacchetto del plugin; una copia fornita dal complemento può essere rimossa solo rimuovendo il complemento.",
1071
+ irohPhaseNote: "Connessione sperimentale. Richiede l’App Android completa 0.7.0+ con iroh; APK compatibile e browser mobili non sono supportati.",
1072
+ irohUnavailable: "iroh non è pronto: nessun sidecar è ancora installato. Installa il componente ufficiale qui sotto per scaricarlo e verificarlo.",
1073
+ irohStarting: "Risoluzione dell’identità endpoint iroh…",
1074
+ irohConnecting: "Sidecar connesso; avvio del gateway di autenticazione DSH…",
1075
+ irohCompanionMissing: "Nessun sidecar iroh verificato disponibile. Usa Installa componente ufficiale qui sotto; il plugin complementare è facoltativo.",
1076
+ irohDataDirectoryInvalid: "Impossibile preparare la directory privata del sidecar. Verifica che DSH possa gestire la propria directory del profilo, poi riprova.",
1077
+ irohSidecarInvalid: "Il sidecar installato non ha superato la verifica del checksum. Reinstalla il componente.",
1078
+ irohDownloadFailed: "Download del sidecar non riuscito prima della verifica. Controlla la rete e riprova.",
1079
+ irohPlatformUnsupported: "I sidecar iroh sono pubblicati solo per Windows x64 e Linux x64.",
1080
+ irohIdentityFailed: "Impossibile leggere l’identità endpoint iroh dal sidecar.",
1081
+ irohLaunchFailed: "Avvio del sidecar iroh non riuscito.",
1082
+ irohExited: "Il sidecar iroh è terminato inaspettatamente. Riconnettiti.",
1083
+ irohTimeout: "Attesa del sidecar iroh scaduta. Riconnettiti.",
1084
+ irohInstallConfirm: "Scaricare e verificare ora il sidecar iroh pinnato (circa 15 MB su Windows x64, 24 MB su Linux x64)? Non viene installato altro.",
1085
+ installingIroh: "Download e verifica del sidecar iroh…",
1086
+ switchingIroh: "Passaggio a iroh…",
1087
+ reconnectingIroh: "Riconnessione del trasporto iroh…",
1088
+ resetIrohConfirm: "Disattivare il canale iroh? Complemento e abbinamenti restano installati."
1089
+ });
1090
+ Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
1091
+ irohName: "iroh(实验性)",
1092
+ irohExperimental: "实验性",
1093
+ irohNotReady: "iroh 通道未运行,请先启动通道再生成配对码。",
1094
+ irohPairingFailed: "无法打开 iroh 配对窗口,请重连通道后重试。",
1095
+ irohPairingTitle: "手机配对(实验性)",
1096
+ generateIrohQr: "生成 iroh 配对二维码",
1097
+ enlargeQr: "放大二维码",
1098
+ closeEnlargedQr: "关闭放大的二维码",
1099
+ irohPairingReady: "iroh 配对码 {code} 已生效,有效期 5 分钟。请在 App 的 iroh 配对页扫码;再生成一个会替换当前这个。",
1100
+ irohDescription: "加密直连,失败时走中继。不需要公网域名或隧道账号。",
1101
+ prepareIroh: "准备 iroh",
1102
+ irohInstallNote: "点击安装会下载固定的 sidecar 发行版(dsh-mobile-iroh 0.1.0-alpha.1,Windows x64 约 15 MB,Linux x64 约 24 MB)到插件私有目录并校验 SHA-256;已安装的配套插件如有校验通过的副本会直接复用,不再下载。",
1103
+ irohComponentNote: "已安装的 sidecar 每次使用都会重新校验 SHA-256。它不随插件包发布;配套提供的副本只能通过移除配套插件来删除。",
1104
+ irohPhaseNote: "实验性连接。需使用含 iroh 的 Android App 0.7.0 及以上完整版;兼容版 APK 和手机浏览器不支持。",
1105
+ irohUnavailable: "iroh 未就绪:尚未安装 sidecar,点击下方「安装官方组件」下载并校验。",
1106
+ irohStarting: "正在解析 iroh 端点身份…",
1107
+ irohConnecting: "Sidecar 已连接,正在启动 DSH 认证网关…",
1108
+ irohCompanionMissing: "尚无校验通过的 iroh 组件,请点击下方「安装官方组件」;无需另装配套插件。",
1109
+ irohDataDirectoryInvalid: "无法准备 sidecar 私有目录。请确认 DSH 能管理自己的配置目录后重试。",
1110
+ irohSidecarInvalid: "已安装的 sidecar 哈希校验失败,请重装组件。",
1111
+ irohDownloadFailed: "下载 sidecar 失败,还没开始校验。请检查网络后重试。",
1112
+ irohPlatformUnsupported: "iroh sidecar 仅发布 Windows x64 与 Linux x64 版本。",
1113
+ irohIdentityFailed: "无法从 sidecar 读取 iroh 端点身份。",
1114
+ irohLaunchFailed: "iroh sidecar 启动失败。",
1115
+ irohExited: "iroh sidecar 意外退出,请重新连接。",
1116
+ irohTimeout: "等待 iroh sidecar 超时,请重新连接。",
1117
+ irohInstallConfirm: "现在下载并校验固定的 iroh sidecar 吗(Windows x64 约 15 MB,Linux x64 约 24 MB)?不会安装其他任何内容。",
1118
+ installingIroh: "正在下载并校验 iroh sidecar…",
1119
+ switchingIroh: "正在切换到 iroh…",
1120
+ reconnectingIroh: "正在重连 iroh 传输…",
1121
+ resetIrohConfirm: "要停用 iroh 通道吗?配套插件与配对不受影响。"
1122
+ });
1024
1123
  Object.assign(MOBILE_CONTROL_MESSAGES.en, {
1025
1124
  requestTimeout: "The operation timed out. Confirm that DSH is still running, then try again.",
1026
1125
  diagnostics: "Diagnostics",
@@ -7857,25 +7956,64 @@ window.__ModuleLoader__.load({
7857
7956
  function ownsMobileCustomization(gateway, frontend) {
7858
7957
  return gateway === true || frontend === "dedicated";
7859
7958
  }
7860
- /** Emit each safe failure once until recovery, and ignore work after cancellation or disposal. */
7959
+ /**
7960
+ * Emit each safe failure once until recovery, and ignore work after cancellation
7961
+ * or disposal.
7962
+ *
7963
+ * A failure that only reaches the console is invisible to the user, who sees the
7964
+ * customization disappear and cannot tell it from a third-party plugin bug
7965
+ * (issue #298). The reporter therefore also publishes its current state so the
7966
+ * page can render a visible, localized notice with an explicit recovery action.
7967
+ * @param locale - resolves the active UI locale for the copy.
7968
+ * @param output - console sink for the developer-facing line.
7969
+ * @returns the reporting calls plus a subscribable snapshot.
7970
+ */
7861
7971
  function createMobileAssetLoadReporter(locale, output = console) {
7862
7972
  const reported = /* @__PURE__ */ new Set();
7973
+ const listeners = /* @__PURE__ */ new Set();
7863
7974
  let disposed = false;
7975
+ const visible = () => {
7976
+ if (reported.has("authentication_required")) return "authentication_required";
7977
+ if (reported.has("gateway_required")) return "gateway_required";
7978
+ return reported.values().next().done === true ? void 0 : reported.values().next().value;
7979
+ };
7980
+ const publish = () => {
7981
+ const state = visible();
7982
+ for (const listener of listeners) try {
7983
+ listener(state);
7984
+ } catch (error) {}
7985
+ };
7864
7986
  return {
7865
7987
  fail(reason, signal) {
7866
7988
  if (disposed || signal?.aborted === true || reported.has(reason)) return;
7867
7989
  if (reason === "resource_unavailable" && reported.has("authentication_required")) return;
7868
7990
  reported.add(reason);
7869
7991
  output.warn(`[DSH Mobile] ${COPY[locale()][reason]}`);
7992
+ publish();
7870
7993
  },
7871
7994
  recover(signal) {
7872
7995
  if (disposed || signal.aborted || reported.size === 0) return;
7873
7996
  reported.clear();
7874
7997
  output.info(`[DSH Mobile] ${COPY[locale()].recovered}`);
7998
+ publish();
7875
7999
  },
7876
8000
  dispose() {
7877
8001
  disposed = true;
7878
8002
  reported.clear();
8003
+ listeners.clear();
8004
+ },
8005
+ getState() {
8006
+ return disposed ? void 0 : visible();
8007
+ },
8008
+ subscribe(listener) {
8009
+ if (disposed) return () => {};
8010
+ listeners.add(listener);
8011
+ return () => {
8012
+ listeners.delete(listener);
8013
+ };
8014
+ },
8015
+ message(reason) {
8016
+ return COPY[locale()][reason];
7879
8017
  }
7880
8018
  };
7881
8019
  }
@@ -7925,6 +8063,125 @@ window.__ModuleLoader__.load({
7925
8063
  }
7926
8064
  }
7927
8065
  //#endregion
8066
+ //#region src/mobile-customization-notice.ts
8067
+ /** Copy for the notice, per locale, kept beside the existing dictionaries. */
8068
+ const NOTICE_COPY = {
8069
+ en: {
8070
+ title: {
8071
+ authentication_required: "Device authentication is unavailable.",
8072
+ gateway_required: "Custom mobile CSS, scripts and extensions need the paired Mobile access entry.",
8073
+ other: "Mobile customization could not be loaded."
8074
+ },
8075
+ action: "Retry loading",
8076
+ dismiss: "Dismiss"
8077
+ },
8078
+ zh: {
8079
+ title: {
8080
+ authentication_required: "设备认证暂不可用。",
8081
+ gateway_required: "自定义移动样式、脚本与扩展需要通过已配对的移动访问入口打开。",
8082
+ other: "移动定制加载失败。"
8083
+ },
8084
+ action: "重试加载",
8085
+ dismiss: "忽略"
8086
+ },
8087
+ it: {
8088
+ title: {
8089
+ authentication_required: "Autenticazione del dispositivo non disponibile.",
8090
+ gateway_required: "CSS, script ed estensioni mobili richiedono l’accesso Mobile abbinato.",
8091
+ other: "Impossibile caricare la personalizzazione mobile."
8092
+ },
8093
+ action: "Riprova il caricamento",
8094
+ dismiss: "Ignora"
8095
+ }
8096
+ };
8097
+ const NOTICE_ATTRIBUTE = "data-dsh-mobile-customization-notice";
8098
+ const NOTICE_STYLES = `
8099
+ [${NOTICE_ATTRIBUTE}]{position:fixed;z-index:1000;left:8px;right:8px;bottom:calc(env(safe-area-inset-bottom) + 8px);display:flex;align-items:flex-start;gap:10px;box-sizing:border-box;padding:10px 12px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#fff);box-shadow:0 8px 24px rgb(15 23 42 / 18%);font:13px/1.45 system-ui;color:var(--dsw-alias-label-primary,#16181d)}
8100
+ [${NOTICE_ATTRIBUTE}][hidden]{display:none}
8101
+ [${NOTICE_ATTRIBUTE}] .dshm-notice-text{flex:1 1 auto;min-width:0}
8102
+ [${NOTICE_ATTRIBUTE}] button{flex:none;box-sizing:border-box;min-height:44px;min-width:44px;padding:0 10px;border:1px solid transparent;border-radius:9px;background:transparent;color:var(--dsw-alias-label-primary,#16181d);font:600 13px/1.2 system-ui;cursor:pointer}
8103
+ [${NOTICE_ATTRIBUTE}] button.dshm-notice-action{background:var(--dsw-alias-state-business-primary,#2563eb);color:#fff;border-color:transparent}
8104
+ [${NOTICE_ATTRIBUTE}] button:hover{opacity:.9}
8105
+ [${NOTICE_ATTRIBUTE}]{max-height:40vh;overflow:auto;left:max(8px,env(safe-area-inset-left));right:max(8px,env(safe-area-inset-right));flex-wrap:wrap}
8106
+ [${NOTICE_ATTRIBUTE}] .dshm-notice-text{margin:0;flex-basis:100%}
8107
+ [${NOTICE_ATTRIBUTE}] button:focus-visible{outline:2px solid currentColor;outline-offset:2px}
8108
+ [${NOTICE_ATTRIBUTE}]{background-color:Canvas;background-image:linear-gradient(var(--dsw-alias-bg-layer-2,Canvas),var(--dsw-alias-bg-layer-2,Canvas));color:var(--dsw-alias-label-primary,CanvasText)}
8109
+ `;
8110
+ /** Copy key for a failure, so the notice and the console never disagree. */
8111
+ function titleKey(reason) {
8112
+ return reason === "authentication_required" || reason === "gateway_required" ? reason : "other";
8113
+ }
8114
+ /**
8115
+ * Mount one visible notice that follows the customization reporter's state.
8116
+ * @param document - the page that owns the notice.
8117
+ * @param options - locale lookup and the reconnect action.
8118
+ * @returns a disposer that removes the notice and its stylesheet.
8119
+ */
8120
+ function installCustomizationNotice(document, options) {
8121
+ if (!(typeof document.createElement === "function" && typeof document.body?.append === "function" && typeof document.head?.append === "function")) return () => {};
8122
+ const style = document.createElement("style");
8123
+ style.dataset.plugin = "dsh-mobile-notice";
8124
+ style.textContent = NOTICE_STYLES;
8125
+ document.head.append(style);
8126
+ const notice = document.createElement("div");
8127
+ if (typeof notice.setAttribute !== "function" || typeof notice.append !== "function") {
8128
+ style.remove();
8129
+ return () => {};
8130
+ }
8131
+ notice.setAttribute(NOTICE_ATTRIBUTE, "");
8132
+ notice.setAttribute("role", "status");
8133
+ notice.setAttribute("aria-live", "polite");
8134
+ notice.hidden = true;
8135
+ const text = document.createElement("p");
8136
+ text.className = "dshm-notice-text";
8137
+ const action = document.createElement("button");
8138
+ action.type = "button";
8139
+ action.className = "dshm-notice-action";
8140
+ const dismiss = document.createElement("button");
8141
+ dismiss.type = "button";
8142
+ notice.append(text, action, dismiss);
8143
+ document.body.append(notice);
8144
+ let current;
8145
+ let dismissed;
8146
+ const render = () => {
8147
+ const copy = NOTICE_COPY[options.locale()] ?? NOTICE_COPY.en;
8148
+ if (current === void 0 || current === dismissed) {
8149
+ notice.hidden = true;
8150
+ return;
8151
+ }
8152
+ const detail = options.message(current);
8153
+ text.textContent = `${copy.title[titleKey(current)]} ${detail}`;
8154
+ action.textContent = copy.action;
8155
+ dismiss.textContent = copy.dismiss;
8156
+ notice.hidden = false;
8157
+ };
8158
+ let disposed = false;
8159
+ const retry = () => {
8160
+ if (!disposed) options.reconnect();
8161
+ };
8162
+ const close = () => {
8163
+ if (disposed) return;
8164
+ dismissed = current;
8165
+ render();
8166
+ };
8167
+ action.addEventListener("click", retry);
8168
+ dismiss.addEventListener("click", close);
8169
+ const unsubscribe = options.subscribe((reason) => {
8170
+ if (reason !== current) dismissed = void 0;
8171
+ current = reason;
8172
+ render();
8173
+ });
8174
+ render();
8175
+ return () => {
8176
+ disposed = true;
8177
+ action.removeEventListener("click", retry);
8178
+ dismiss.removeEventListener("click", close);
8179
+ unsubscribe();
8180
+ notice.remove();
8181
+ style.remove();
8182
+ };
8183
+ }
8184
+ //#endregion
7928
8185
  //#region src/extension-limits.ts
7929
8186
  /** Maximum sizes accepted for legacy mobile customization resources. */
7930
8187
  const LEGACY_CUSTOM_ASSET_LIMIT = 262144;
@@ -7948,6 +8205,25 @@ window.__ModuleLoader__.load({
7948
8205
  function shouldRenewSession(connection, downForMs) {
7949
8206
  return isDisconnected(connection) && downForMs >= 5e3;
7950
8207
  }
8208
+ /**
8209
+ * Authentication rejection can warrant one renewal even while transport appears
8210
+ * connected. A 403 can also indicate a permission failure, so it is not proof
8211
+ * that the Session expired.
8212
+ *
8213
+ * Issue #298: DSH keeps Sessions in memory, so a Host restart kills every phone
8214
+ * Session while the device credential survives. The live WebSocket can keep
8215
+ * reporting `connected` for a while, so waiting for a disconnect never fires and
8216
+ * the customization channel stays dark. A 401/403 on an authenticated gateway
8217
+ * request is the signal that actually arrives, and it must be able to start the
8218
+ * same renewal the disconnect path uses.
8219
+ * @param status - HTTP status of a rejected gateway request.
8220
+ * @param connection - current public connection state, if known.
8221
+ * @returns whether this rejection should request a renewal.
8222
+ */
8223
+ function shouldRenewAfterRejection(status, connection) {
8224
+ if (status !== 401 && status !== 403) return false;
8225
+ return !isDisconnected(connection) || connection === void 0;
8226
+ }
7951
8227
  function isDisconnected(connection) {
7952
8228
  return connection === "connecting" || connection === "disconnected";
7953
8229
  }
@@ -7956,9 +8232,12 @@ window.__ModuleLoader__.load({
7956
8232
  * @param getConnection - Resolve the current service to reject a replaced owner.
7957
8233
  * @param page - The document's window and advertised native adapter.
7958
8234
  * @param document - The mounted document; page replacement withdraws its work.
8235
+ * @param onRejected - receives the rejection signal, so a caller that observes a
8236
+ * 401 can ask this same owner for one renewal instead of starting its own.
8237
+ * @param onRenewed - refresh read-only customization after confirmed Cookie installation.
7959
8238
  * @returns An awaited disposer, or nothing when no public connection is present.
7960
8239
  */
7961
- function installMobileConnectionRecovery(getConnection, page, document) {
8240
+ function installMobileConnectionRecovery(getConnection, page, document, onRejected, onRenewed) {
7962
8241
  const connection = getConnection();
7963
8242
  const state = connection?.state;
7964
8243
  if (connection === void 0 || state === void 0 || typeof connection.reconnect !== "function") return;
@@ -7968,6 +8247,8 @@ window.__ModuleLoader__.load({
7968
8247
  let currentState = state.getSnapshot();
7969
8248
  let downSince = isDisconnected(currentState) ? Date.now() : void 0;
7970
8249
  let attempted = false;
8250
+ let credentialsRejected = false;
8251
+ let rejectedSince;
7971
8252
  let bridge = page.__DSH_MOBILE_NATIVE__;
7972
8253
  let pending;
7973
8254
  let pendingAbort;
@@ -7981,8 +8262,13 @@ window.__ModuleLoader__.load({
7981
8262
  const next = state.getSnapshot();
7982
8263
  if (!isDisconnected(next)) {
7983
8264
  downSince = void 0;
7984
- attempted = false;
7985
- if (currentState !== next) cancelPending();
8265
+ if (currentState !== next) {
8266
+ if (next === "connected") {
8267
+ rejectedSince = void 0;
8268
+ attempted = false;
8269
+ }
8270
+ cancelPending();
8271
+ }
7986
8272
  } else if (downSince === void 0) {
7987
8273
  downSince = Date.now();
7988
8274
  attempted = false;
@@ -7999,30 +8285,63 @@ window.__ModuleLoader__.load({
7999
8285
  const renew = () => {
8000
8286
  if (!current()) {
8001
8287
  dispose();
8002
- return;
8288
+ return false;
8003
8289
  }
8004
8290
  updateState();
8005
8291
  updateBridge();
8006
8292
  const adapter = bridge;
8007
- if (document.visibilityState !== "visible" || adapter === void 0 || pending !== void 0 || attempted || downSince === void 0 || !shouldRenewSession(currentState, Date.now() - downSince)) return;
8293
+ const downLongEnough = downSince !== void 0 && shouldRenewSession(currentState, Date.now() - downSince);
8294
+ const rejectionPending = rejectedSince !== void 0;
8295
+ if (document.visibilityState !== "visible" || adapter === void 0 || pending !== void 0 || attempted || credentialsRejected || !downLongEnough && !rejectionPending) return false;
8008
8296
  attempted = true;
8009
8297
  const owner = generation;
8010
8298
  const abort = new AbortController();
8011
8299
  pendingAbort = abort;
8012
- const owns = () => current() && !abort.signal.aborted && generation === owner && page.__DSH_MOBILE_NATIVE__ === adapter && isDisconnected(state.getSnapshot());
8300
+ const owns = () => current() && !abort.signal.aborted && generation === owner && page.__DSH_MOBILE_NATIVE__ === adapter;
8013
8301
  const task = Promise.resolve().then(async () => {
8014
8302
  if (!owns()) return;
8015
8303
  const capabilities = await adapter.capabilities();
8016
8304
  if (!owns() || !capabilities.includes("auth.renew")) return;
8017
8305
  const result = await adapter.invoke("auth.renew", {}, { signal: abort.signal });
8018
8306
  if (!owns() || typeof result !== "object" || result === null || !("renewed" in result) || result.renewed !== true) return;
8307
+ rejectedSince = void 0;
8019
8308
  connection.reconnect?.();
8020
- }).catch((_error) => {}).finally(() => {
8309
+ if (current()) onRenewed?.();
8310
+ }).catch((error) => {
8311
+ if (!owns() || typeof error !== "object" || error === null || !("code" in error)) return;
8312
+ credentialsRejected = error.code === "device_revoked" || error.code === "device_expired" || error.code === "authentication_failed";
8313
+ }).finally(() => {
8021
8314
  if (pending !== task) return;
8022
8315
  pending = void 0;
8023
8316
  pendingAbort = void 0;
8024
8317
  });
8025
8318
  pending = task;
8319
+ return true;
8320
+ };
8321
+ const retry = () => {
8322
+ if (!current()) return false;
8323
+ updateState();
8324
+ updateBridge();
8325
+ if (!current() || pending !== void 0 || credentialsRejected || bridge === void 0 || rejectedSince === void 0 && downSince === void 0) return false;
8326
+ attempted = false;
8327
+ return renew();
8328
+ };
8329
+ /**
8330
+ * Record a rejected authenticated request and try the same single renewal.
8331
+ *
8332
+ * Only an owner that is still current and has not already attempted may act, so
8333
+ * a burst of 401s cannot start parallel renewals, and a revoked device cannot
8334
+ * keep retrying: revocation arrives as 401 too, but the native bridge refuses
8335
+ * `auth.renew` and the attempt is not restarted.
8336
+ * @param status - HTTP status the gateway returned.
8337
+ */
8338
+ const rejected = (status) => {
8339
+ if (!current()) return;
8340
+ updateState();
8341
+ updateBridge();
8342
+ if (!shouldRenewAfterRejection(status, state.getSnapshot())) return;
8343
+ rejectedSince ??= Date.now();
8344
+ renew();
8026
8345
  };
8027
8346
  const resume = () => {
8028
8347
  if (!current()) {
@@ -8054,6 +8373,7 @@ window.__ModuleLoader__.load({
8054
8373
  document.addEventListener("visibilitychange", resume);
8055
8374
  page.addEventListener("dsh-mobile-native-ready", updateBridge);
8056
8375
  page.addEventListener("pagehide", pagehide);
8376
+ onRejected?.(rejected, retry);
8057
8377
  return dispose;
8058
8378
  }
8059
8379
  //#endregion
@@ -8145,7 +8465,8 @@ window.__ModuleLoader__.load({
8145
8465
  draft: "",
8146
8466
  error: null,
8147
8467
  hostBusy: false,
8148
- committed: false
8468
+ committed: false,
8469
+ creating: false
8149
8470
  });
8150
8471
  /** Read navigation stops on close; committing is final until the owner withdraws this flow. */
8151
8472
  var MobileDirectoryBrowser = class {
@@ -8199,6 +8520,14 @@ window.__ModuleLoader__.load({
8199
8520
  hostBusy
8200
8521
  });
8201
8522
  }
8523
+ /** Block navigation/adoption while this flow's child-folder editor owns interaction. */
8524
+ setCreating(creating) {
8525
+ if (this.disposed || this.snapshot.creating === creating) return;
8526
+ this.publish({
8527
+ ...this.snapshot,
8528
+ creating
8529
+ });
8530
+ }
8202
8531
  /** Read a folder or the initial home; superseded results cannot publish into the new request. */
8203
8532
  navigate(path) {
8204
8533
  if (!this.canNavigate()) return false;
@@ -8281,7 +8610,7 @@ window.__ModuleLoader__.load({
8281
8610
  }
8282
8611
  /** Cancel reading immediately; a committed/Host-busy adoption cannot be reported as cancelled. */
8283
8612
  cancel() {
8284
- if (!this.canNavigate()) return false;
8613
+ if (!this.canClose()) return false;
8285
8614
  this.withdrawRead();
8286
8615
  this.publish(CLOSED);
8287
8616
  return true;
@@ -8295,9 +8624,12 @@ window.__ModuleLoader__.load({
8295
8624
  await Promise.allSettled([...this.tasks]);
8296
8625
  if (this.taskFailures.length > 0) throw new AggregateError(this.taskFailures, "Mobile directory observers failed during a listing");
8297
8626
  }
8298
- canNavigate() {
8627
+ canClose() {
8299
8628
  return !this.disposed && this.snapshot.open && !this.snapshot.hostBusy && !this.snapshot.committed;
8300
8629
  }
8630
+ canNavigate() {
8631
+ return this.canClose() && !this.snapshot.creating;
8632
+ }
8301
8633
  current(generation) {
8302
8634
  return !this.disposed && this.snapshot.open && generation === this.generation;
8303
8635
  }
@@ -8311,6 +8643,171 @@ window.__ModuleLoader__.load({
8311
8643
  for (const listener of this.listeners) listener();
8312
8644
  }
8313
8645
  };
8646
+ /** A creation failure without raw response bodies or filesystem details. */
8647
+ var MobileDirectoryCreateError = class extends Error {
8648
+ code;
8649
+ constructor(code) {
8650
+ super(`Mobile directory creation failed: ${code}`);
8651
+ this.code = code;
8652
+ this.name = "MobileDirectoryCreateError";
8653
+ }
8654
+ };
8655
+ /** Portable single-segment validation; the Host additionally enforces its filesystem rules. */
8656
+ function isMobileDirectoryName(name) {
8657
+ return name.trim() !== "" && name.length <= 255 && name !== "." && name !== ".." && !/[\\/\0]/u.test(name);
8658
+ }
8659
+ const CREATE_CLOSED = Object.freeze({
8660
+ open: false,
8661
+ parent: null,
8662
+ name: "",
8663
+ pending: false,
8664
+ error: null
8665
+ });
8666
+ /** Owns one non-replayed write, its deadline, cancellation and physical teardown. */
8667
+ var MobileDirectoryCreator = class {
8668
+ create;
8669
+ onCreated;
8670
+ deadlineMs;
8671
+ snapshot = CREATE_CLOSED;
8672
+ listeners = /* @__PURE__ */ new Set();
8673
+ tasks = /* @__PURE__ */ new Set();
8674
+ failures = [];
8675
+ active;
8676
+ generation = 0;
8677
+ disposed = false;
8678
+ constructor(create, onCreated, deadlineMs = 3e4) {
8679
+ this.create = create;
8680
+ this.onCreated = onCreated;
8681
+ this.deadlineMs = deadlineMs;
8682
+ }
8683
+ getSnapshot = () => this.snapshot;
8684
+ /** Subscribe to this editor; the returned disposer withdraws the observer. */
8685
+ subscribe(listener) {
8686
+ this.listeners.add(listener);
8687
+ return () => {
8688
+ this.listeners.delete(listener);
8689
+ };
8690
+ }
8691
+ /** Open against one already confirmed parent; cancelled physical work retains admission. */
8692
+ open(parent) {
8693
+ if (this.disposed || this.snapshot.open || this.tasks.size > 0 || !isMobileDirectoryPath(parent)) return false;
8694
+ this.publish({
8695
+ ...CREATE_CLOSED,
8696
+ open: true,
8697
+ parent
8698
+ });
8699
+ return true;
8700
+ }
8701
+ /** Edit only while no write is in flight; typing never starts a filesystem operation. */
8702
+ setName(name) {
8703
+ if (this.disposed || !this.snapshot.open || this.snapshot.pending) return;
8704
+ this.publish({
8705
+ ...this.snapshot,
8706
+ name,
8707
+ error: null
8708
+ });
8709
+ }
8710
+ /** Admit one explicit creation. Repeated clicks/Enter cannot replay an admitted request. */
8711
+ submit() {
8712
+ if (this.disposed || !this.snapshot.open || this.snapshot.pending || this.tasks.size > 0 || this.snapshot.parent === null) return false;
8713
+ const name = this.snapshot.name.trim();
8714
+ if (!isMobileDirectoryName(name)) {
8715
+ this.publish({
8716
+ ...this.snapshot,
8717
+ error: "invalid-name"
8718
+ });
8719
+ return false;
8720
+ }
8721
+ const parent = this.snapshot.parent;
8722
+ const generation = this.generation;
8723
+ const controller = new AbortController();
8724
+ this.active = controller;
8725
+ this.publish({
8726
+ ...this.snapshot,
8727
+ pending: true,
8728
+ error: null
8729
+ });
8730
+ if (!this.current(generation) || controller.signal.aborted) {
8731
+ if (this.active === controller) this.active = void 0;
8732
+ if (!this.disposed) this.publish({
8733
+ ...this.snapshot,
8734
+ pending: false
8735
+ });
8736
+ return false;
8737
+ }
8738
+ const timer = setTimeout(() => {
8739
+ controller.abort(new MobileDirectoryCreateError("timeout"));
8740
+ if (this.current(generation)) this.publish({
8741
+ ...this.snapshot,
8742
+ error: "timeout"
8743
+ });
8744
+ }, this.deadlineMs);
8745
+ const task = (async () => {
8746
+ let created;
8747
+ try {
8748
+ created = await this.create(parent, name, controller.signal);
8749
+ controller.signal.throwIfAborted();
8750
+ if (!isMobileDirectoryPath(created)) throw new MobileDirectoryCreateError("invalid-response");
8751
+ } catch (error) {
8752
+ created = void 0;
8753
+ if (this.current(generation)) {
8754
+ const failure = controller.signal.aborted ? controller.signal.reason : error;
8755
+ this.publish({
8756
+ ...this.snapshot,
8757
+ error: failure instanceof MobileDirectoryCreateError ? failure.code : "request-failed"
8758
+ });
8759
+ }
8760
+ } finally {
8761
+ clearTimeout(timer);
8762
+ if (this.active === controller) this.active = void 0;
8763
+ if (!this.disposed) this.publish({
8764
+ ...this.snapshot,
8765
+ pending: false
8766
+ });
8767
+ }
8768
+ if (created !== void 0 && this.current(generation)) {
8769
+ this.publish(CREATE_CLOSED);
8770
+ this.onCreated(created);
8771
+ }
8772
+ })();
8773
+ this.tasks.add(task);
8774
+ task.then(() => {
8775
+ this.tasks.delete(task);
8776
+ }, (error) => {
8777
+ this.tasks.delete(task);
8778
+ this.failures.push(error);
8779
+ });
8780
+ return true;
8781
+ }
8782
+ /** Withdraw the editor and late results; an already completed mkdir is not rolled back. */
8783
+ close() {
8784
+ if (this.disposed || !this.snapshot.open) return false;
8785
+ this.generation++;
8786
+ this.active?.abort();
8787
+ this.publish({
8788
+ ...CREATE_CLOSED,
8789
+ pending: this.active !== void 0
8790
+ });
8791
+ return true;
8792
+ }
8793
+ /** Stop admission synchronously and await every accepted write before completion. */
8794
+ async dispose() {
8795
+ this.disposed = true;
8796
+ this.generation++;
8797
+ this.active?.abort();
8798
+ this.snapshot = CREATE_CLOSED;
8799
+ this.listeners.clear();
8800
+ await Promise.allSettled([...this.tasks]);
8801
+ if (this.failures.length > 0) throw new AggregateError(this.failures, "Mobile directory creation observers failed");
8802
+ }
8803
+ current(generation) {
8804
+ return !this.disposed && this.snapshot.open && generation === this.generation;
8805
+ }
8806
+ publish(snapshot) {
8807
+ this.snapshot = Object.freeze(snapshot);
8808
+ for (const listener of this.listeners) listener();
8809
+ }
8810
+ };
8314
8811
  //#endregion
8315
8812
  //#region src/mobile-directory-picker.ts
8316
8813
  /** Dedicated Mobile directory-flow occupant; stock DSH retains its own chooser. */
@@ -8335,7 +8832,17 @@ window.__ModuleLoader__.load({
8335
8832
  "invalid-response": "目录响应不可用,请重试或重新连接电脑",
8336
8833
  "request-failed": "读取目录失败,请检查连接后重试",
8337
8834
  timeout: "读取目录超时,请重试或取消",
8338
- failedOpen: "无法打开目录选择器,请重新连接后重试"
8835
+ failedOpen: "无法打开目录选择器,请重新连接后重试",
8836
+ create: "新建文件夹",
8837
+ createTitle: "新建文件夹名称",
8838
+ createAction: "创建",
8839
+ createInvalid: "请输入单个文件夹名称,不能包含路径分隔符。",
8840
+ createExists: "同名文件夹已存在。",
8841
+ createFailed: "无法创建文件夹,请检查该目录的写入权限后重试。",
8842
+ createParent: "创建位置",
8843
+ creating: "正在创建文件夹…",
8844
+ createStopping: "正在结束创建请求…",
8845
+ createUnknown: "无法确认创建结果,文件夹可能已创建。请返回并重新读取目录,确认后再决定是否重试。"
8339
8846
  },
8340
8847
  en: {
8341
8848
  title: "Select workspace directory",
@@ -8357,7 +8864,17 @@ window.__ModuleLoader__.load({
8357
8864
  "invalid-response": "The directory response is unavailable. Retry or reconnect to the computer.",
8358
8865
  "request-failed": "Could not load this directory. Check the connection and retry.",
8359
8866
  timeout: "Loading timed out. Retry or cancel.",
8360
- failedOpen: "Could not open the directory picker. Reconnect and retry."
8867
+ failedOpen: "Could not open the directory picker. Reconnect and retry.",
8868
+ create: "New folder",
8869
+ createTitle: "New folder name",
8870
+ createAction: "Create",
8871
+ createInvalid: "Enter a single folder name without path separators.",
8872
+ createExists: "A folder with that name already exists.",
8873
+ createFailed: "Could not create the folder. Check write access to this directory and retry.",
8874
+ createParent: "Create in",
8875
+ creating: "Creating folder…",
8876
+ createStopping: "Finishing the creation request…",
8877
+ createUnknown: "The creation result is unknown; the folder may already exist. Go back and reload the directory before deciding whether to retry."
8361
8878
  },
8362
8879
  it: {
8363
8880
  title: "Seleziona la cartella del workspace",
@@ -8378,11 +8895,21 @@ window.__ModuleLoader__.load({
8378
8895
  unauthorized: "Autorizzazione non disponibile. Chiudi questa finestra e riconnettiti al computer.",
8379
8896
  "invalid-response": "Risposta della cartella non disponibile. Riprova o riconnettiti al computer.",
8380
8897
  "request-failed": "Impossibile caricare la cartella. Controlla la connessione e riprova.",
8898
+ create: "Nuova cartella",
8899
+ createTitle: "Nome della nuova cartella",
8900
+ createAction: "Crea",
8901
+ createInvalid: "Inserisci un singolo nome di cartella senza separatori di percorso.",
8902
+ createExists: "Esiste già una cartella con questo nome.",
8903
+ createFailed: "Impossibile creare la cartella. Controlla i permessi di scrittura e riprova.",
8381
8904
  timeout: "Caricamento scaduto. Riprova o annulla.",
8382
- failedOpen: "Impossibile aprire la selezione della cartella. Riconnettiti e riprova."
8905
+ failedOpen: "Impossibile aprire la selezione della cartella. Riconnettiti e riprova.",
8906
+ createParent: "Crea in",
8907
+ creating: "Creazione della cartella…",
8908
+ createStopping: "Chiusura della richiesta di creazione…",
8909
+ createUnknown: "Esito della creazione sconosciuto: la cartella potrebbe già esistere. Torna indietro e ricarica la directory prima di riprovare."
8383
8910
  }
8384
8911
  };
8385
- /** Authenticated read-only Gateway route; no native picker or Host service is replaced. */
8912
+ /** Authenticated directory browsing/creation route; no native picker or Host service is replaced. */
8386
8913
  const WORKSPACE_DIRECTORIES_PATH = "/mobile-access/workspace-directories";
8387
8914
  /** Ask only this plugin's live directory dialog to handle a routed native Back. */
8388
8915
  function dismissMobileDirectoryPicker(dialog) {
@@ -8399,6 +8926,32 @@ window.__ModuleLoader__.load({
8399
8926
  }
8400
8927
  return "en";
8401
8928
  }
8929
+ /** Create one child directory through the same authenticated route that lists them. */
8930
+ async function createDirectory(path, name, signal) {
8931
+ const response = await fetch(WORKSPACE_DIRECTORIES_PATH, {
8932
+ method: "POST",
8933
+ credentials: "same-origin",
8934
+ cache: "no-store",
8935
+ headers: { "content-type": "application/json" },
8936
+ body: JSON.stringify({
8937
+ path,
8938
+ name
8939
+ }),
8940
+ signal
8941
+ });
8942
+ if (!response.ok) {
8943
+ const code = response.status === 409 ? "directory-exists" : response.status === 400 ? "directory-create-failed" : response.status === 401 || response.status === 403 ? "unauthorized" : "directory-create-failed";
8944
+ try {
8945
+ await response.body?.cancel();
8946
+ } catch (error) {
8947
+ throw new MobileDirectoryCreateError(code);
8948
+ }
8949
+ throw new MobileDirectoryCreateError(code);
8950
+ }
8951
+ const payload = await response.json();
8952
+ if (typeof payload?.path !== "string" || !isMobileDirectoryPath(payload.path)) throw new MobileDirectoryCreateError("invalid-response");
8953
+ return payload.path;
8954
+ }
8402
8955
  async function readDirectories(path, signal) {
8403
8956
  const query = path === null ? "" : `?path=${encodeURIComponent(path)}`;
8404
8957
  const response = await fetch(`${WORKSPACE_DIRECTORIES_PATH}${query}`, {
@@ -8424,25 +8977,51 @@ window.__ModuleLoader__.load({
8424
8977
  const STYLES = `
8425
8978
  dialog.dshm-picker{position:fixed;inset:0;box-sizing:border-box;width:100%;max-width:none;height:100%;max-height:none;margin:0;padding:max(16px,env(safe-area-inset-top)) max(16px,env(safe-area-inset-right)) max(16px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));border:0;background:transparent;color:var(--dsw-alias-label-primary,#16181d);font:400 14px/1.5 system-ui;overflow:hidden}
8426
8979
  dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::backdrop{background:rgb(0 0 0 / 42%)}
8427
- .dshm-picker__panel{display:flex;flex-direction:column;box-sizing:border-box;width:min(680px,100%);height:min(500px,100%);min-height:0;overflow:hidden;border:1px solid var(--dsw-alias-border-l2,#dde0e4);border-radius:16px;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 16px 56px rgb(0 0 0 / 18%)}
8428
- .dshm-picker__header{display:flex;align-items:center;flex:none;gap:12px;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l2,#dde0e4)}
8429
- .dshm-picker__title{flex:1;min-width:0;margin:0;font-size:16px;font-weight:500;overflow-wrap:anywhere}
8430
- .dshm-picker__toolbar{display:flex;flex-wrap:wrap;gap:8px;flex:none;padding:12px 16px 8px}.dshm-picker__path-label{flex:1;min-width:min(180px,100%);display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--dsw-alias-label-secondary,#606873)}
8431
- .dshm-picker__path{box-sizing:border-box;width:100%;min-width:0;min-height:48px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#f7f8fa);color:var(--dsw-alias-label-primary,#16181d);font:400 16px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace}
8980
+ .dshm-picker__panel{display:flex;flex-direction:column;box-sizing:border-box;width:min(680px,100%);height:min(720px,100%);min-height:0;overflow:hidden;border:0;border-radius:20px;box-shadow:0 0 0 .5px rgba(0 0 0 / 16%),0 3px 8px rgba(0 0 0 / 4%);background:var(--dsw-alias-bg-layer-1,#fff)}
8981
+ .dshm-picker__header{display:flex;align-items:center;flex:none;gap:8px;padding:4px 8px 4px 12px;border-bottom:1px solid var(--dsw-alias-border-l2,#dde0e4)}
8982
+ .dshm-picker__title{flex:1;min-width:0;margin:0;font-size:16px;font-weight:510;line-height:24px;overflow-wrap:anywhere}
8983
+ .dshm-picker__toolbar{display:flex;align-items:center;flex-wrap:nowrap;gap:8px;flex:none;padding:8px 10px}.dshm-picker__path-label{flex:1;min-width:0;display:flex;color:var(--dsw-alias-label-secondary,#606873)}
8984
+ .dshm-picker__path-caption{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
8985
+ .dshm-picker__path{box-sizing:border-box;width:100%;min-width:0;min-height:44px;padding:6px 8px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:8px;background:var(--dsw-alias-bg-layer-2,#f7f8fa);color:var(--dsw-alias-label-primary,#16181d);font:400 16px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace}
8432
8986
  .dshm-picker__path[aria-invalid=true]{border-color:var(--dsw-alias-state-error-primary,#b42318)}.dshm-picker__go{align-self:flex-end}
8433
- .dshm-picker__navigation{display:flex;flex:none;gap:8px;padding:0 16px 8px}.dshm-picker__navigation .dshm-picker__button{flex:1;white-space:normal}
8434
- .dshm-picker__list{flex:1;min-height:0;margin:0;padding:4px 12px;overflow:auto;overscroll-behavior:contain;list-style:none;scroll-padding:4px}
8435
- .dshm-picker__row{display:flex;align-items:center;gap:12px;box-sizing:border-box;width:100%;min-height:48px;padding:10px 12px;border:0;border-radius:8px;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer;touch-action:manipulation}
8987
+ .dshm-picker__navigation{display:flex;flex:none;gap:8px;padding:0}
8988
+ .dshm-picker__list{flex:1;min-height:0;margin:0;padding:0 10px 4px;overflow:auto;overscroll-behavior:contain;list-style:none;scroll-padding:4px}
8989
+ .dshm-picker__row{display:flex;align-items:center;gap:8px;box-sizing:border-box;width:100%;min-height:44px;padding:6px 8px;border:0;border-radius:8px;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer;touch-action:manipulation}
8436
8990
  .dshm-picker__folder{flex:none;width:20px;height:20px;color:var(--dsw-alias-label-secondary,#606873)}
8437
8991
  .dshm-picker__name{flex:1;min-width:0;overflow-wrap:anywhere}.dshm-picker__row:hover:not(:disabled),.dshm-picker__button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#eef1f5)}
8438
8992
  .dshm-picker__row:active:not(:disabled),.dshm-picker__button:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active,#e4e8ee)}
8439
- .dshm-picker__button{flex:none;box-sizing:border-box;min-width:48px;min-height:48px;padding:10px 14px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:transparent;color:inherit;font:500 14px/1.4 system-ui;cursor:pointer;touch-action:manipulation}
8993
+ .dshm-picker__button{flex:none;box-sizing:border-box;min-width:44px;min-height:44px;padding:6px 10px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:8px;background:transparent;color:inherit;font:500 14px/1.4 system-ui;cursor:pointer;touch-action:manipulation}
8994
+ .dshm-picker__icon{display:inline-flex;align-items:center;justify-content:center;width:44px;padding:0;border-color:transparent}.dshm-picker__icon>svg{display:block;width:18px;height:18px}
8440
8995
  .dshm-picker :is(button,input):focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#2563eb);outline-offset:2px}.dshm-picker button:disabled{opacity:.5;cursor:default}
8441
- .dshm-picker__status{flex:none;margin:0;padding:8px 16px;font-size:13px;color:var(--dsw-alias-label-secondary,#606873);overflow-wrap:anywhere}.dshm-picker__error{color:var(--dsw-alias-state-error-primary,#b42318)}
8442
- .dshm-picker__status[hidden],.dshm-picker__button[hidden]{display:none}.dshm-picker__footer{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;flex:none;gap:8px;padding:12px 16px;border-top:1px solid var(--dsw-alias-border-l2,#dde0e4)}
8443
- .dshm-picker__confirm{border-color:transparent;background:var(--dsw-alias-button-primary-fill,#2563eb);color:var(--dsw-alias-label-primary-foreground,#fff)}.dshm-picker__confirm:hover:not(:disabled),.dshm-picker__confirm:active:not(:disabled){background:var(--dsw-alias-button-primary-hover,#1d4ed8)}
8444
- @media(max-width:420px){dialog.dshm-picker{padding:max(8px,env(safe-area-inset-top)) max(8px,env(safe-area-inset-right)) max(8px,env(safe-area-inset-bottom)) max(8px,env(safe-area-inset-left))}.dshm-picker__header,.dshm-picker__toolbar,.dshm-picker__footer{padding-left:12px;padding-right:12px}.dshm-picker__footer .dshm-picker__button{flex:1}.dshm-picker__path-label{min-width:0}}
8996
+ .dshm-picker [hidden]{display:none!important}.dshm-picker__create{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:8px;padding:12px 16px}
8997
+ .dshm-picker__create-title{margin:0;font-size:13px;color:var(--dsw-alias-label-secondary,#606873)}
8998
+ .dshm-picker__create-input{box-sizing:border-box;width:100%;min-width:0;min-height:48px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#f7f8fa);color:var(--dsw-alias-label-primary,#0f1115);font:inherit;font-size:16px;line-height:1.4}
8999
+ .dshm-picker__create-actions{display:flex;justify-content:flex-end;gap:8px}.dshm-picker__create-error{color:var(--dsw-alias-state-error-primary,#b42318)}
9000
+ .dshm-picker__create-trigger{margin-right:auto;border-color:transparent;padding-left:8px;padding-right:8px}
9001
+ .dshm-picker__status{flex:none;margin:0;padding:4px 10px;font-size:13px;color:var(--dsw-alias-label-secondary,#606873);overflow-wrap:anywhere}.dshm-picker__error{color:var(--dsw-alias-state-error-primary,#b42318)}
9002
+ .dshm-picker__status[hidden],.dshm-picker__button[hidden]{display:none}.dshm-picker__footer{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;flex:none;gap:8px;padding:6px 10px;border-top:1px solid var(--dsw-alias-border-l2,#dde0e4)}
9003
+ .dshm-picker__confirm{min-width:72px;border-color:transparent;background:var(--dsw-alias-button-primary-fill,#2563eb);color:var(--dsw-alias-label-primary-foreground,#fff)}.dshm-picker__confirm:hover:not(:disabled),.dshm-picker__confirm:active:not(:disabled){background:var(--dsw-alias-button-primary-hover,#1d4ed8)}
9004
+ @media(max-width:420px){dialog.dshm-picker{padding:max(8px,env(safe-area-inset-top)) max(8px,env(safe-area-inset-right)) max(8px,env(safe-area-inset-bottom)) max(8px,env(safe-area-inset-left))}}
8445
9005
  `;
9006
+ /**
9007
+ * Whether DSH's own browse directory picker is already mounted on this page.
9008
+ *
9009
+ * DSH's adaptive `directory-picker` resolves to
9010
+ * `@deepseek-ai/dsh-host-directory-picker-auto`, which mounts the Host backend
9011
+ * and the Client face together and picks `browse` whenever the host is remotely
9012
+ * reachable. Its client face fills the same directory-flow slots this plugin
9013
+ * would use, so a plugin-owned picker would shadow the upstream one and take
9014
+ * away its folder creation. The desktop keeps the native OS chooser; a remote
9015
+ * browser can only reach the web implementation.
9016
+ *
9017
+ * @returns true when the upstream browse surface owns those slots on this page.
9018
+ */
9019
+ function hasUpstreamBrowsePicker(view) {
9020
+ if (!("__DSH_BOOT__" in view)) return false;
9021
+ const graph = view.__DSH_BOOT__;
9022
+ if (typeof graph !== "object" || graph === null || !("entries" in graph) || !Array.isArray(graph.entries)) return false;
9023
+ return graph.entries.some((entry) => typeof entry === "object" && entry !== null && "id" in entry && entry.id === "@deepseek-ai/dsh-client-ui-directory-picker-browse");
9024
+ }
8446
9025
  function openDirectoryDialog(doc, getProps, trackDisposal) {
8447
9026
  const view = doc.defaultView;
8448
9027
  if (view === null) throw new Error("Directory picker requires a browser window");
@@ -8468,18 +9047,40 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
8468
9047
  if (name !== void 0) value.setAttribute(`data-dshm-picker-${name}`, "");
8469
9048
  return value;
8470
9049
  };
9050
+ const iconButton = (label, outline, name) => {
9051
+ const value = button("", name);
9052
+ value.classList.add("dshm-picker__icon");
9053
+ value.setAttribute("aria-label", label);
9054
+ value.title = label;
9055
+ const icon = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
9056
+ icon.setAttribute("viewBox", "0 0 24 24");
9057
+ icon.setAttribute("fill", "none");
9058
+ icon.setAttribute("stroke", "currentColor");
9059
+ icon.setAttribute("stroke-width", "1.75");
9060
+ icon.setAttribute("stroke-linecap", "round");
9061
+ icon.setAttribute("stroke-linejoin", "round");
9062
+ icon.setAttribute("aria-hidden", "true");
9063
+ const shape = doc.createElementNS("http://www.w3.org/2000/svg", "path");
9064
+ shape.setAttribute("d", outline);
9065
+ icon.append(shape);
9066
+ value.append(icon);
9067
+ return value;
9068
+ };
8471
9069
  const header = doc.createElement("header");
8472
9070
  header.className = "dshm-picker__header";
8473
9071
  const title = doc.createElement("h2");
8474
9072
  title.className = "dshm-picker__title";
8475
9073
  title.textContent = copy.title;
8476
- const close = button(copy.close);
9074
+ const close = iconButton(copy.close, "M6 6l12 12M6 18L18 6");
8477
9075
  header.append(title, close);
8478
9076
  const toolbar = doc.createElement("form");
8479
9077
  toolbar.className = "dshm-picker__toolbar";
8480
9078
  const pathLabel = doc.createElement("label");
8481
9079
  pathLabel.className = "dshm-picker__path-label";
8482
- pathLabel.textContent = copy.path;
9080
+ const pathCaption = doc.createElement("span");
9081
+ pathCaption.className = "dshm-picker__path-caption";
9082
+ pathCaption.textContent = copy.path;
9083
+ pathLabel.append(pathCaption);
8483
9084
  const path = doc.createElement("input");
8484
9085
  path.type = "text";
8485
9086
  path.className = "dshm-picker__path";
@@ -8488,16 +9089,17 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
8488
9089
  path.autocomplete = "off";
8489
9090
  path.maxLength = 8192;
8490
9091
  pathLabel.append(path);
8491
- const go = button(copy.go, "load");
9092
+ const go = iconButton(copy.go, "M5 12h14m-6-6 6 6-6 6", "load");
8492
9093
  go.type = "submit";
8493
9094
  go.classList.add("dshm-picker__go");
8494
9095
  toolbar.append(pathLabel, go);
8495
9096
  const navigation = doc.createElement("nav");
8496
9097
  navigation.className = "dshm-picker__navigation";
8497
9098
  navigation.setAttribute("aria-label", copy.title);
8498
- const home = button(copy.home, "home");
8499
- const up = button(copy.up, "up");
9099
+ const home = iconButton(copy.home, "m3 10 9-7 9 7v10a1 1 0 0 1-1 1h-5v-7H9v7H4a1 1 0 0 1-1-1Z", "home");
9100
+ const up = iconButton(copy.up, "M12 20V4m-7 7 7-7 7 7", "up");
8500
9101
  navigation.append(home, up);
9102
+ toolbar.prepend(navigation);
8501
9103
  const status = doc.createElement("p");
8502
9104
  status.className = "dshm-picker__status";
8503
9105
  status.setAttribute("role", "status");
@@ -8513,13 +9115,82 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
8513
9115
  footer.className = "dshm-picker__footer";
8514
9116
  const retry = button(copy.retry, "retry");
8515
9117
  retry.hidden = true;
9118
+ const create = button(copy.create, "create");
9119
+ create.classList.add("dshm-picker__create-trigger");
8516
9120
  const cancel = button(copy.cancel, "cancel");
8517
9121
  const confirm = button(copy.open, "confirm");
8518
9122
  confirm.classList.add("dshm-picker__confirm");
8519
- footer.append(retry, cancel, confirm);
8520
- panel.append(header, toolbar, navigation, status, error, list, footer);
9123
+ const createPanel = doc.createElement("div");
9124
+ createPanel.className = "dshm-picker__create";
9125
+ createPanel.hidden = true;
9126
+ const createTitle = doc.createElement("label");
9127
+ createTitle.className = "dshm-picker__create-title";
9128
+ createTitle.textContent = copy.createTitle;
9129
+ const createInput = doc.createElement("input");
9130
+ createInput.type = "text";
9131
+ createInput.className = "dshm-picker__create-input";
9132
+ createInput.setAttribute("data-dshm-picker-create-name", "");
9133
+ createInput.spellcheck = false;
9134
+ createInput.autocomplete = "off";
9135
+ createInput.maxLength = 255;
9136
+ createTitle.append(createInput);
9137
+ const createParent = doc.createElement("p");
9138
+ createParent.className = "dshm-picker__status";
9139
+ const createStatus = doc.createElement("p");
9140
+ createStatus.className = "dshm-picker__status";
9141
+ createStatus.setAttribute("role", "status");
9142
+ createStatus.setAttribute("aria-live", "polite");
9143
+ const createError = doc.createElement("p");
9144
+ createError.className = "dshm-picker__status dshm-picker__create-error";
9145
+ createError.setAttribute("role", "alert");
9146
+ createError.setAttribute("data-dshm-picker-create-error", "");
9147
+ createError.hidden = true;
9148
+ const createActions = doc.createElement("div");
9149
+ createActions.className = "dshm-picker__create-actions";
9150
+ const createCancel = button(copy.cancel, "create-cancel");
9151
+ const createConfirm = button(copy.createAction, "create-confirm");
9152
+ createActions.append(createCancel, createConfirm);
9153
+ createPanel.append(createParent, createTitle, createError, createStatus, createActions);
9154
+ footer.append(create, retry, cancel, confirm);
9155
+ panel.append(header, toolbar, status, error, list, createPanel, footer);
8521
9156
  dialog.append(panel);
8522
9157
  const browser = new MobileDirectoryBrowser(readDirectories);
9158
+ const creator = new MobileDirectoryCreator(createDirectory, (created) => {
9159
+ browser.navigate(created);
9160
+ });
9161
+ let creationNotice = false;
9162
+ let creationNoticeListing = null;
9163
+ const closeCreate = () => {
9164
+ const child = creator.getSnapshot();
9165
+ if (child.pending || child.error === "timeout" || child.error === "request-failed" || child.error === "invalid-response") {
9166
+ creationNotice = true;
9167
+ creationNoticeListing = browser.getSnapshot().listing;
9168
+ }
9169
+ if (creator.close()) {
9170
+ if (create.disabled) close.focus();
9171
+ else create.focus();
9172
+ }
9173
+ };
9174
+ create.addEventListener("click", () => {
9175
+ const parent = browser.getSnapshot().listing?.path;
9176
+ if (!browser.canConfirm() || parent === void 0 || !creator.open(parent)) return;
9177
+ creationNotice = false;
9178
+ createInput.focus();
9179
+ createInput.select();
9180
+ });
9181
+ createCancel.addEventListener("click", closeCreate);
9182
+ createConfirm.addEventListener("click", () => {
9183
+ creator.submit();
9184
+ });
9185
+ createInput.addEventListener("input", () => {
9186
+ creator.setName(createInput.value);
9187
+ });
9188
+ createInput.addEventListener("keydown", (event) => {
9189
+ if (event.key === "Enter" && !event.isComposing) {
9190
+ event.preventDefault();
9191
+ creator.submit();
9192
+ }
9193
+ });
8523
9194
  let disposed = false;
8524
9195
  let closedByOwner = false;
8525
9196
  let renderedListing;
@@ -8527,22 +9198,41 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
8527
9198
  const render = () => {
8528
9199
  if (disposed) return;
8529
9200
  const state = browser.getSnapshot();
9201
+ if (creationNotice && state.listing !== creationNoticeListing) creationNotice = false;
9202
+ const child = creator.getSnapshot();
8530
9203
  const adopting = state.committed || state.hostBusy;
8531
- path.disabled = adopting;
9204
+ const blocked = adopting || state.creating;
9205
+ path.disabled = blocked;
8532
9206
  if (path.value !== state.draft) path.value = state.draft;
8533
9207
  path.setAttribute("aria-invalid", String(state.error === "bad-path"));
8534
- go.disabled = adopting || state.phase === "loading" || !isMobileDirectoryPath(state.draft);
8535
- home.disabled = adopting;
8536
- up.disabled = adopting || state.listing?.parent === void 0;
9208
+ go.disabled = blocked || state.phase === "loading" || !isMobileDirectoryPath(state.draft);
9209
+ home.disabled = blocked;
9210
+ up.disabled = blocked || state.listing?.parent === void 0;
8537
9211
  confirm.disabled = !browser.canConfirm();
8538
9212
  cancel.disabled = adopting;
8539
9213
  close.disabled = adopting;
8540
9214
  retry.hidden = state.phase !== "error" || state.error === "bad-path";
8541
- retry.disabled = adopting;
8542
- status.textContent = adopting ? copy.adopting : state.phase === "loading" ? copy.loading : state.listing?.truncated === true ? copy.truncated : state.listing?.entries.length === 0 ? copy.empty : "";
8543
- status.hidden = status.textContent === "";
9215
+ retry.disabled = blocked;
9216
+ create.disabled = !browser.canConfirm();
9217
+ title.textContent = child.open ? copy.create : copy.title;
9218
+ toolbar.hidden = child.open;
9219
+ navigation.hidden = child.open;
9220
+ list.hidden = child.open;
9221
+ footer.hidden = child.open;
9222
+ createPanel.hidden = !child.open;
9223
+ if (createInput.value !== child.name) createInput.value = child.name;
9224
+ createInput.disabled = child.pending;
9225
+ createInput.setAttribute("aria-invalid", String(child.error === "invalid-name"));
9226
+ createConfirm.disabled = child.pending || !isMobileDirectoryName(child.name.trim());
9227
+ createParent.textContent = `${copy.createParent}: ${child.parent ?? ""}`;
9228
+ createError.textContent = child.error === null ? "" : child.error === "invalid-name" ? copy.createInvalid : child.error === "directory-exists" ? copy.createExists : child.error === "directory-create-failed" ? copy.createFailed : child.error === "unauthorized" ? copy.unauthorized : copy.createUnknown;
9229
+ createError.hidden = child.error === null;
9230
+ createStatus.textContent = child.pending && child.error === null ? copy.creating : "";
9231
+ createStatus.hidden = createStatus.textContent === "";
9232
+ status.textContent = child.pending ? copy.createStopping : creationNotice ? copy.createUnknown : adopting ? copy.adopting : state.phase === "loading" ? copy.loading : state.listing?.truncated === true ? copy.truncated : state.listing?.entries.length === 0 ? copy.empty : "";
9233
+ status.hidden = child.open || status.textContent === "";
8544
9234
  error.textContent = state.error === null ? "" : copy[state.error];
8545
- error.hidden = state.error === null;
9235
+ error.hidden = child.open || state.error === null;
8546
9236
  list.setAttribute("aria-busy", String(state.phase === "loading"));
8547
9237
  if (renderedListing !== state.listing) {
8548
9238
  renderedListing = state.listing;
@@ -8572,11 +9262,21 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
8572
9262
  list.append(item);
8573
9263
  }
8574
9264
  }
8575
- for (const row of list.querySelectorAll("button")) row.disabled = adopting;
9265
+ for (const row of list.querySelectorAll("button")) row.disabled = blocked;
8576
9266
  };
8577
9267
  const unsubscribe = browser.subscribe(render);
9268
+ const unsubscribeCreate = creator.subscribe(() => {
9269
+ const child = creator.getSnapshot();
9270
+ browser.setCreating(child.open || child.pending);
9271
+ render();
9272
+ });
8578
9273
  const requestClose = () => {
9274
+ if (creator.getSnapshot().open) {
9275
+ closeCreate();
9276
+ return;
9277
+ }
8579
9278
  if (disposed || !browser.cancel()) return;
9279
+ creator.close();
8580
9280
  closedByOwner = true;
8581
9281
  if (dialog.open) dialog.close();
8582
9282
  getProps().onCancel();
@@ -8597,6 +9297,8 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
8597
9297
  if (disposal !== void 0) return disposal;
8598
9298
  disposed = true;
8599
9299
  unsubscribe();
9300
+ unsubscribeCreate();
9301
+ const creationStopped = creator.dispose();
8600
9302
  const stopped = browser.dispose();
8601
9303
  const failures = [];
8602
9304
  const clean = (action) => {
@@ -8623,11 +9325,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
8623
9325
  if (trigger?.isConnected) trigger.focus({ preventScroll: true });
8624
9326
  });
8625
9327
  disposal = (async () => {
8626
- try {
8627
- await stopped;
8628
- } catch (error) {
8629
- failures.push(error);
8630
- }
9328
+ for (const result of await Promise.allSettled([stopped, creationStopped])) if (result.status === "rejected") failures.push(result.reason);
8631
9329
  if (failures.length > 0) throw new AggregateError(failures, "Mobile directory dialog disposal failed");
8632
9330
  })();
8633
9331
  return disposal;
@@ -8787,7 +9485,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
8787
9485
  //#region src/client.ts
8788
9486
  /** Map a host-reported provider onto the UI union, defaulting to the built-in one. */
8789
9487
  function parseMobileRemoteProvider(value) {
8790
- return value === "cpolar" || value === "cloudflared" || value === "frp" || value === "origin" ? value : "tailscale";
9488
+ return value === "cpolar" || value === "cloudflared" || value === "frp" || value === "origin" || value === "iroh" ? value : "tailscale";
8791
9489
  }
8792
9490
  /**
8793
9491
  * Set a surface's visibility; opening a page or overlay brings it to the front.
@@ -9333,6 +10031,23 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
9333
10031
  frp_discovery_invalid: "frpDiscoveryInvalid",
9334
10032
  frp_stopped: "frpStopped",
9335
10033
  frp_exited: "frpExited",
10034
+ iroh_companion_missing: "irohCompanionMissing",
10035
+ iroh_data_directory_invalid: "irohDataDirectoryInvalid",
10036
+ iroh_sidecar_invalid: "irohSidecarInvalid",
10037
+ iroh_download_failed: "irohDownloadFailed",
10038
+ iroh_download_size_mismatch: "irohDownloadFailed",
10039
+ iroh_download_hash_mismatch: "irohSidecarInvalid",
10040
+ iroh_download_timeout: "irohTimeout",
10041
+ iroh_platform_unsupported: "irohPlatformUnsupported",
10042
+ iroh_identity_failed: "irohIdentityFailed",
10043
+ iroh_identity_timeout: "irohIdentityFailed",
10044
+ iroh_identity_invalid: "irohIdentityFailed",
10045
+ iroh_launch_failed: "irohLaunchFailed",
10046
+ iroh_start_timeout: "irohTimeout",
10047
+ iroh_start_failed: "irohLaunchFailed",
10048
+ iroh_exited: "irohExited",
10049
+ iroh_not_ready: "irohNotReady",
10050
+ iroh_pairing_failed: "irohPairingFailed",
9336
10051
  frp_server_address_invalid: "frpServerAddressInvalid",
9337
10052
  frp_server_port_invalid: "frpServerPortInvalid",
9338
10053
  frp_token_invalid: "frpTokenInvalid",
@@ -9444,7 +10159,8 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
9444
10159
  cpolar: "cpolar",
9445
10160
  cloudflared: "cloudflared",
9446
10161
  frp: t("frpName"),
9447
- origin: t("originName")
10162
+ origin: t("originName"),
10163
+ iroh: t("irohName")
9448
10164
  };
9449
10165
  const providerId = typeof facts.provider === "string" ? facts.provider : "";
9450
10166
  const controllerCode = typeof facts.controllerCode === "string" ? facts.controllerCode : "";
@@ -9977,7 +10693,19 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
9977
10693
  originChoiceDescription.textContent = t("originDescription");
9978
10694
  originChoiceTop.append(originChoiceName);
9979
10695
  originChoice.append(originChoiceTop, originChoiceDescription);
9980
- selfHostedBody.append(frpChoice, originChoice);
10696
+ const irohChoice = element("button", "dsh-mobile-control__provider is-iroh");
10697
+ irohChoice.type = "button";
10698
+ irohChoice.setAttribute("aria-pressed", "false");
10699
+ const irohChoiceTop = element("span", "dsh-mobile-control__provider-top");
10700
+ const irohChoiceName = element("strong");
10701
+ irohChoiceName.textContent = t("irohName");
10702
+ const irohChoiceBadge = element("span", "dsh-mobile-control__provider-badge is-iroh");
10703
+ irohChoiceBadge.textContent = t("irohExperimental");
10704
+ const irohChoiceDescription = element("span", "dsh-mobile-control__provider-description");
10705
+ irohChoiceDescription.textContent = t("irohDescription");
10706
+ irohChoiceTop.append(irohChoiceName, irohChoiceBadge);
10707
+ irohChoice.append(irohChoiceTop, irohChoiceDescription);
10708
+ selfHostedBody.append(frpChoice, originChoice, irohChoice);
9981
10709
  selfHosted.append(selfHostedSummary, selfHostedBody);
9982
10710
  providerSection.append(providerHeading, providerInfo, providerChoices, selfHosted);
9983
10711
  const frpSetup = element("section", "dsh-mobile-control__frp-setup");
@@ -10461,7 +11189,77 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
10461
11189
  providerSetupMeta.append(providerSetupName, remoteStateBadge);
10462
11190
  providerSetupHeader.append(providerSetupHeading, providerSetupMeta);
10463
11191
  const providerSetupBody = element("div", "dsh-mobile-control__provider-setup-body");
10464
- providerSetupBody.append(cpolarSetup, cloudflaredSetup, frpSetup, originSetup, tailscaleInfo);
11192
+ const irohSetup = element("section", "dsh-mobile-control__frp-setup");
11193
+ irohSetup.hidden = true;
11194
+ const irohSetupTitle = element("h3", "dsh-mobile-control__section-title");
11195
+ irohSetupTitle.textContent = t("prepareIroh");
11196
+ const irohPhaseNote = element("p", "dsh-mobile-control__component-note");
11197
+ irohPhaseNote.textContent = t("irohPhaseNote");
11198
+ const irohComponentStatus = element("p", "dsh-mobile-control__component-status");
11199
+ irohComponentStatus.textContent = t("checkingComponent");
11200
+ irohComponentStatus.setAttribute("role", "status");
11201
+ irohComponentStatus.setAttribute("aria-live", "polite");
11202
+ const irohInstall = element("button", "dsh-mobile-control__primary");
11203
+ irohInstall.type = "button";
11204
+ irohInstall.textContent = t("installOfficial");
11205
+ const irohInstallProgress = element("progress", "dsh-mobile-control__progress");
11206
+ irohInstallProgress.hidden = true;
11207
+ irohInstallProgress.setAttribute("aria-hidden", "true");
11208
+ const irohPairing = element("div", "dsh-mobile-control__ws-paths");
11209
+ const irohPairingTitle = element("p", "dsh-mobile-control__component-status");
11210
+ irohPairingTitle.textContent = t("irohPairingTitle");
11211
+ const irohPair = element("button", "dsh-mobile-control__primary");
11212
+ irohPair.type = "button";
11213
+ irohPair.textContent = t("generateIrohQr");
11214
+ const irohQr = element("div", "dsh-mobile-control__qr is-expandable");
11215
+ irohQr.hidden = true;
11216
+ const irohPairStatus = element("p", "dsh-mobile-control__component-status");
11217
+ const irohCopyDescriptor = element("button", "dsh-mobile-control__secondary");
11218
+ irohCopyDescriptor.type = "button";
11219
+ irohCopyDescriptor.textContent = t("copyPairLink");
11220
+ irohCopyDescriptor.hidden = true;
11221
+ irohPairing.append(irohPairingTitle, irohPair, irohQr, irohPairStatus, irohCopyDescriptor);
11222
+ const irohDetails = element("details", "dsh-mobile-control__details");
11223
+ const irohDetailsSummary = element("summary");
11224
+ irohDetailsSummary.textContent = t("componentDetails");
11225
+ const irohDetailsBody = element("div", "dsh-mobile-control__details-body");
11226
+ const irohDetailsText = element("p");
11227
+ irohDetailsText.textContent = t("irohComponentNote");
11228
+ const irohInstallNote = element("p");
11229
+ irohInstallNote.textContent = t("irohInstallNote");
11230
+ const irohStorage = element("code", "dsh-mobile-control__storage");
11231
+ irohStorage.textContent = t("pluginPrivateDirectory");
11232
+ const irohOfficial = element("a", "dsh-mobile-control__text-link");
11233
+ irohOfficial.href = "https://www.npmjs.com/package/dsh-mobile-iroh";
11234
+ irohOfficial.target = "_blank";
11235
+ irohOfficial.rel = "noopener noreferrer";
11236
+ irohOfficial.textContent = t("officialDownload");
11237
+ irohDetailsBody.append(irohDetailsText, irohInstallNote, irohStorage, irohOfficial);
11238
+ irohDetails.append(irohDetailsSummary, irohDetailsBody);
11239
+ irohSetup.append(irohSetupTitle, irohPhaseNote, irohComponentStatus, irohInstall, irohInstallProgress, irohPairing, irohDetails);
11240
+ providerSetupBody.append(cpolarSetup, cloudflaredSetup, frpSetup, originSetup, irohSetup, tailscaleInfo);
11241
+ const irohQrZoom = element("div", "dsh-mobile-control__qr-zoom");
11242
+ irohQrZoom.hidden = true;
11243
+ const irohQrZoomImage = element("img", "dsh-mobile-control__qr-zoom-image");
11244
+ irohQrZoomImage.alt = t("pairingQr");
11245
+ const irohQrZoomClose = element("button", "dsh-mobile-control__qr-zoom-close");
11246
+ irohQrZoomClose.type = "button";
11247
+ irohQrZoomClose.textContent = "×";
11248
+ irohQrZoomClose.setAttribute("aria-label", t("closeEnlargedQr"));
11249
+ const setIrohQrZoomOpen = (open) => {
11250
+ if (open) {
11251
+ irohQrZoom.setAttribute("role", "dialog");
11252
+ irohQrZoom.setAttribute("aria-modal", "true");
11253
+ irohQrZoom.setAttribute("aria-label", t("enlargeQr"));
11254
+ } else {
11255
+ irohQrZoom.removeAttribute("role");
11256
+ irohQrZoom.removeAttribute("aria-modal");
11257
+ irohQrZoom.removeAttribute("aria-label");
11258
+ }
11259
+ irohQrZoom.hidden = !open;
11260
+ };
11261
+ irohQrZoom.append(irohQrZoomImage, irohQrZoomClose);
11262
+ root.append(irohQrZoom);
10465
11263
  const remoteAccess = element("div", "dsh-mobile-control__access");
10466
11264
  remoteAccess.hidden = true;
10467
11265
  const remoteAccessLabel = element("span", "dsh-mobile-control__access-label");
@@ -10527,6 +11325,8 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
10527
11325
  remotePairLink.hidden = true;
10528
11326
  let activeRemotePairUrl = "";
10529
11327
  let activeRemotePairExpiresAt = 0;
11328
+ let activeIrohDescriptor = "";
11329
+ let activeIrohQrSvg = "";
10530
11330
  remoteActions.append(remoteToggle, remoteLogin, remoteReconnect, remotePair, remoteCopyLink);
10531
11331
  const remoteManageRow = element("div", "dsh-mobile-control__manage-row");
10532
11332
  const remoteDevices = element("button", "dsh-mobile-control__manage");
@@ -10823,6 +11623,9 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
10823
11623
  let latestRemoteState = {};
10824
11624
  let frpInstalled = false;
10825
11625
  let frpConfigured = false;
11626
+ let irohInstalled = false;
11627
+ /** True while a companion checksum verification is in flight; polls must not wipe its progress. */
11628
+ let irohVerifying = false;
10826
11629
  let frpLayoutInitialized = false;
10827
11630
  let frpDownloadSize = "14.0";
10828
11631
  let configuredFrpServer = "";
@@ -11144,6 +11947,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
11144
11947
  const cloudflared = remoteProvider === "cloudflared";
11145
11948
  const frp = remoteProvider === "frp";
11146
11949
  const origin = remoteProvider === "origin";
11950
+ const iroh = remoteProvider === "iroh";
11147
11951
  const managedOrigin = origin && data.originMode === "managed";
11148
11952
  const tailscale = remoteProvider === "tailscale";
11149
11953
  tailscaleChoice.classList.toggle("is-selected", tailscale);
@@ -11159,17 +11963,20 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
11159
11963
  cloudflaredChoice.tabIndex = cloudflared ? 0 : -1;
11160
11964
  frpChoice.setAttribute("aria-pressed", String(frp));
11161
11965
  originChoice.setAttribute("aria-pressed", String(origin));
11162
- providerSetupName.textContent = cpolar ? "cpolar" : cloudflared ? "cloudflared" : frp ? t("frpName") : origin ? t(managedOrigin ? "caddyName" : "originName") : "Tailscale Funnel";
11966
+ irohChoice.setAttribute("aria-pressed", String(iroh));
11967
+ providerSetupName.textContent = cpolar ? "cpolar" : cloudflared ? "cloudflared" : frp ? t("frpName") : origin ? t(managedOrigin ? "caddyName" : "originName") : iroh ? t("irohName") : "Tailscale Funnel";
11163
11968
  tailscaleChoice.disabled = remoteProviderBusy;
11164
11969
  cpolarChoice.disabled = remoteProviderBusy;
11165
11970
  cloudflaredChoice.disabled = remoteProviderBusy;
11166
11971
  frpChoice.disabled = remoteProviderBusy;
11167
11972
  originChoice.disabled = remoteProviderBusy;
11973
+ irohChoice.disabled = remoteProviderBusy;
11168
11974
  cpolarSetup.hidden = !cpolar;
11169
11975
  cloudflaredSetup.hidden = !cloudflared;
11170
11976
  frpSetup.hidden = !frp;
11171
11977
  originSetup.hidden = !origin;
11172
- if (frp || origin) selfHosted.open = true;
11978
+ irohSetup.hidden = !iroh;
11979
+ if (frp || origin || iroh) selfHosted.open = true;
11173
11980
  tailscaleInfo.hidden = !tailscale;
11174
11981
  remoteReset.textContent = tailscale ? t("resetRemoteLogin") : t("resetRemoteDevices");
11175
11982
  const providers = data.providers !== null && typeof data.providers === "object" ? data.providers : {};
@@ -11324,6 +12131,30 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
11324
12131
  frpVpsGroup.open = !frpConfigured;
11325
12132
  frpLayoutInitialized = true;
11326
12133
  }
12134
+ const irohProvider = providers.iroh !== null && typeof providers.iroh === "object" ? providers.iroh : {};
12135
+ const irohComponent = irohProvider.component !== null && typeof irohProvider.component === "object" ? irohProvider.component : {};
12136
+ irohInstalled = irohComponent.installed === true;
12137
+ const irohSupported = irohComponent.supported !== false;
12138
+ const irohComponentError = typeof irohComponent.errorCode === "string" ? irohComponent.errorCode : "";
12139
+ const irohVersion = typeof irohComponent.version === "string" ? irohComponent.version : "";
12140
+ const irohDownloadBytes = typeof irohComponent.downloadBytes === "number" ? irohComponent.downloadBytes : 0;
12141
+ const irohStoragePath = typeof irohComponent.storagePath === "string" ? irohComponent.storagePath : `DSH Mobile ${t("pluginPrivateDirectory")}`;
12142
+ irohInstall.hidden = irohInstalled || !irohSupported;
12143
+ if (irohVerifying) {
12144
+ irohInstallProgress.hidden = false;
12145
+ irohInstall.disabled = true;
12146
+ irohInstall.textContent = t("installingIroh");
12147
+ irohInstall.setAttribute("aria-busy", "true");
12148
+ irohComponentStatus.textContent = t("installingIroh");
12149
+ } else {
12150
+ irohInstallProgress.hidden = true;
12151
+ irohInstall.setAttribute("aria-busy", "false");
12152
+ irohInstall.disabled = remoteProviderBusy;
12153
+ irohInstall.textContent = irohDownloadBytes > 0 ? t("installWithSize", { size: formatMegabytes(irohDownloadBytes) }) : t("installOfficial");
12154
+ irohComponentStatus.textContent = !irohSupported ? t("irohPlatformUnsupported") : irohInstalled ? t("installed") + (irohVersion === "" ? "" : ` · ${irohVersion}`) : irohComponentError === "irohSidecarInvalid" ? t("irohSidecarInvalid") : t("irohUnavailable");
12155
+ }
12156
+ irohStorage.textContent = irohStoragePath;
12157
+ irohStorage.title = irohStoragePath;
11327
12158
  const originProvider = providers.origin !== null && typeof providers.origin === "object" ? providers.origin : {};
11328
12159
  const originConfiguration = originProvider.configuration !== null && typeof originProvider.configuration === "object" ? originProvider.configuration : {};
11329
12160
  originConfigured = originConfiguration.configured === true;
@@ -11414,10 +12245,10 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
11414
12245
  remoteStatus.classList.toggle("is-running", remoteReady);
11415
12246
  const labels = {
11416
12247
  off: t("remoteOff"),
11417
- unavailable: cpolar ? t("remoteUnavailableCpolar") : cloudflared ? t("remoteUnavailableCloudflared") : frp ? t("remoteUnavailableFrp") : origin ? t("originUnavailable") : t("remoteUnavailableTailscale"),
11418
- starting: cpolar ? t("remoteStartingCpolar") : cloudflared ? t("remoteStartingCloudflared") : frp ? t("remoteStartingFrp") : origin ? t("originStarting") : t("remoteStartingTailscale"),
12248
+ unavailable: cpolar ? t("remoteUnavailableCpolar") : cloudflared ? t("remoteUnavailableCloudflared") : frp ? t("remoteUnavailableFrp") : origin ? t("originUnavailable") : iroh ? t("irohUnavailable") : t("remoteUnavailableTailscale"),
12249
+ starting: cpolar ? t("remoteStartingCpolar") : cloudflared ? t("remoteStartingCloudflared") : frp ? t("remoteStartingFrp") : origin ? t("originStarting") : iroh ? t("irohStarting") : t("remoteStartingTailscale"),
11419
12250
  "needs-login": t("remoteNeedsLogin"),
11420
- connecting: cpolar ? t("remoteConnectingCpolar") : cloudflared ? t("remoteConnectingCloudflared") : frp ? t("remoteConnectingFrp") : t("remoteConnectingTailscale"),
12251
+ connecting: cpolar ? t("remoteConnectingCpolar") : cloudflared ? t("remoteConnectingCloudflared") : frp ? t("remoteConnectingFrp") : iroh ? t("irohConnecting") : t("remoteConnectingTailscale"),
11421
12252
  ready: origin ? t(managedOrigin ? "caddyVerified" : "originReady") : t("remoteReady"),
11422
12253
  error: t("remoteError")
11423
12254
  };
@@ -11432,7 +12263,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
11432
12263
  remoteSetupRetry.disabled = remoteReconnectBusy;
11433
12264
  remoteToggle.textContent = remoteRunning ? t("disableRemote") : t("enableRemote");
11434
12265
  const originPrepared = data.originMode === "managed" ? caddySupported && caddyInstalled && caddyConfiguration.configured === true : originConfigured && !caddyModeManagedSelected;
11435
- const providerPrepared = cpolar ? cpolarInstalled && cpolarConfigured : cloudflared ? cloudflaredInstalled : frp ? frpInstalled && frpConfigured : origin ? originPrepared : true;
12266
+ const providerPrepared = cpolar ? cpolarInstalled && cpolarConfigured : cloudflared ? cloudflaredInstalled : frp ? frpInstalled && frpConfigured : origin ? originPrepared : iroh ? irohInstalled : true;
11436
12267
  remoteToggle.disabled = remoteProviderBusy || !providerPrepared && !(origin && remoteRunning);
11437
12268
  remoteLogin.hidden = !tailscale || state !== "needs-login" || remoteLoginUrl === "";
11438
12269
  remoteReconnect.hidden = needsFunnelSetup || state !== "error" && state !== "unavailable" || !providerPrepared;
@@ -11440,6 +12271,21 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
11440
12271
  remotePair.disabled = !remoteReady || remoteProviderBusy;
11441
12272
  remoteCopyLink.disabled = !remoteReady || remoteProviderBusy;
11442
12273
  remoteDevices.disabled = !remoteReady || remoteProviderBusy;
12274
+ remotePair.hidden = iroh;
12275
+ remoteCopyLink.hidden = iroh;
12276
+ if (iroh) {
12277
+ remoteQr.hidden = true;
12278
+ remotePairLink.hidden = true;
12279
+ }
12280
+ irohPair.disabled = !remoteReady || !iroh || remoteProviderBusy;
12281
+ if ((!iroh || !remoteReady) && activeIrohDescriptor !== "") {
12282
+ activeIrohDescriptor = "";
12283
+ activeIrohQrSvg = "";
12284
+ showQr("", irohQr);
12285
+ setIrohQrZoomOpen(false);
12286
+ irohCopyDescriptor.hidden = true;
12287
+ irohPairStatus.textContent = "";
12288
+ }
11443
12289
  if (origin) {
11444
12290
  remoteReconnect.disabled = remoteProviderBusy || remoteReconnectBusy;
11445
12291
  remoteReset.disabled = remoteProviderBusy;
@@ -11504,7 +12350,8 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
11504
12350
  cloudflaredChoice.disabled = true;
11505
12351
  frpChoice.disabled = true;
11506
12352
  originChoice.disabled = true;
11507
- remoteStatus.textContent = provider === "cpolar" ? t("switchingCpolar") : provider === "cloudflared" ? t("switchingCloudflared") : provider === "frp" ? t("switchingFrp") : provider === "origin" ? t("switchingOrigin") : t("switchingTailscale");
12353
+ irohChoice.disabled = true;
12354
+ remoteStatus.textContent = provider === "cpolar" ? t("switchingCpolar") : provider === "cloudflared" ? t("switchingCloudflared") : provider === "frp" ? t("switchingFrp") : provider === "origin" ? t("switchingOrigin") : provider === "iroh" ? t("switchingIroh") : t("switchingTailscale");
11508
12355
  controlRequestJson("/api/mobile-access/remote/provider", {
11509
12356
  method: "POST",
11510
12357
  body: JSON.stringify({ provider })
@@ -11531,6 +12378,111 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
11531
12378
  originChoice.addEventListener("click", () => {
11532
12379
  chooseRemoteProvider("origin");
11533
12380
  });
12381
+ irohChoice.addEventListener("click", () => {
12382
+ chooseRemoteProvider("iroh");
12383
+ });
12384
+ irohInstall.addEventListener("click", () => {
12385
+ if (remoteProviderBusy) return;
12386
+ if (!window.confirm(t("irohInstallConfirm"))) return;
12387
+ remoteProviderBusy = true;
12388
+ irohVerifying = true;
12389
+ irohInstall.disabled = true;
12390
+ irohInstall.textContent = t("installingIroh");
12391
+ irohInstall.setAttribute("aria-busy", "true");
12392
+ irohInstallProgress.hidden = false;
12393
+ irohComponentStatus.textContent = t("installingIroh");
12394
+ remoteStatus.textContent = t("installingIroh");
12395
+ controlRequestJson("/api/mobile-access/remote/iroh/component/install", {
12396
+ method: "POST",
12397
+ body: JSON.stringify({ confirm: true })
12398
+ }, LONG_CONTROL_REQUEST_TIMEOUT_MS).then(renderRemote, (error) => {
12399
+ remoteFailureText = remoteFailureTextFor(error, "installFailed");
12400
+ remoteStatus.textContent = remoteFailureText;
12401
+ irohComponentStatus.textContent = remoteFailureText;
12402
+ }).finally(() => {
12403
+ remoteProviderBusy = false;
12404
+ irohVerifying = false;
12405
+ loadRemote();
12406
+ });
12407
+ });
12408
+ const openIrohQrZoom = () => {
12409
+ if (activeIrohQrSvg === "" || irohQr.hidden) return;
12410
+ irohQrZoomImage.src = `data:image/svg+xml;base64,${btoa(activeIrohQrSvg)}`;
12411
+ setIrohQrZoomOpen(true);
12412
+ irohQrZoomClose.focus();
12413
+ };
12414
+ const closeIrohQrZoom = () => {
12415
+ setIrohQrZoomOpen(false);
12416
+ if (!irohQr.hidden) irohQr.focus();
12417
+ };
12418
+ irohQr.setAttribute("role", "button");
12419
+ irohQr.setAttribute("tabindex", "0");
12420
+ irohQr.setAttribute("aria-label", t("enlargeQr"));
12421
+ irohQr.addEventListener("click", openIrohQrZoom);
12422
+ irohQr.addEventListener("keydown", (event) => {
12423
+ if (event.key !== "Enter" && event.key !== " ") return;
12424
+ event.preventDefault();
12425
+ openIrohQrZoom();
12426
+ });
12427
+ irohQrZoom.addEventListener("click", closeIrohQrZoom);
12428
+ irohQrZoomClose.addEventListener("click", closeIrohQrZoom);
12429
+ irohQrZoom.addEventListener("keydown", (event) => {
12430
+ if (event.key !== "Escape") return;
12431
+ closeIrohQrZoom();
12432
+ });
12433
+ const openIrohPairing = () => controlRequestJson("/api/mobile-access/remote/iroh/pairing/begin", {
12434
+ method: "POST",
12435
+ body: "{}"
12436
+ }).then(async (data) => {
12437
+ const record = data;
12438
+ const qrSvg = typeof record.qrSvg === "string" ? record.qrSvg : "";
12439
+ const code = typeof record.irohCode === "string" ? record.irohCode : "";
12440
+ showQr(qrSvg, irohQr);
12441
+ if (qrSvg === "" || code === "") {
12442
+ activeIrohQrSvg = "";
12443
+ irohPairStatus.textContent = t("keyGenerationFailed");
12444
+ return;
12445
+ }
12446
+ activeIrohQrSvg = qrSvg;
12447
+ const fields = [
12448
+ "version",
12449
+ "type",
12450
+ "endpointId",
12451
+ "origin",
12452
+ "localPort",
12453
+ "irohCode",
12454
+ "gatewayToken",
12455
+ "caCertificate",
12456
+ "instanceId",
12457
+ "expiresAt"
12458
+ ];
12459
+ const descriptor = {};
12460
+ for (const field of fields) descriptor[field] = record[field];
12461
+ activeIrohDescriptor = JSON.stringify(descriptor);
12462
+ irohCopyDescriptor.hidden = false;
12463
+ irohPairStatus.textContent = t("irohPairingReady", { code });
12464
+ });
12465
+ irohPair.addEventListener("click", () => {
12466
+ if (remoteProviderBusy) return;
12467
+ remoteProviderBusy = true;
12468
+ irohPair.disabled = true;
12469
+ irohPairStatus.textContent = t("loadingRemoteStatus");
12470
+ openIrohPairing().catch((error) => {
12471
+ remoteFailureText = remoteFailureTextFor(error, "requestFailed");
12472
+ irohPairStatus.textContent = remoteFailureText;
12473
+ }).finally(() => {
12474
+ remoteProviderBusy = false;
12475
+ loadRemote();
12476
+ });
12477
+ });
12478
+ irohCopyDescriptor.addEventListener("click", () => {
12479
+ if (activeIrohDescriptor === "") return;
12480
+ navigator.clipboard.writeText(activeIrohDescriptor).then(() => {
12481
+ irohPairStatus.textContent = t("linkCopied");
12482
+ }, () => {
12483
+ irohPairStatus.textContent = t("remoteQrReady");
12484
+ });
12485
+ });
11534
12486
  cloudflaredInstall.addEventListener("click", () => {
11535
12487
  if (remoteProviderBusy) return;
11536
12488
  if (!window.confirm(t("installConfirmCloudflared", { size: cloudflaredDownloadSize }))) return;
@@ -12523,7 +13475,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
12523
13475
  remoteReconnect.disabled = true;
12524
13476
  remoteSetup.disabled = true;
12525
13477
  remoteSetupRetry.disabled = true;
12526
- remoteStatus.textContent = remoteProvider === "cpolar" ? t("reconnectingCpolar") : remoteProvider === "cloudflared" ? t("reconnectingCloudflared") : remoteProvider === "frp" ? t("reconnectingFrp") : remoteProvider === "origin" ? t("reconnectingOrigin") : t("reconnectingTailscale");
13478
+ remoteStatus.textContent = remoteProvider === "cpolar" ? t("reconnectingCpolar") : remoteProvider === "cloudflared" ? t("reconnectingCloudflared") : remoteProvider === "frp" ? t("reconnectingFrp") : remoteProvider === "origin" ? t("reconnectingOrigin") : remoteProvider === "iroh" ? t("reconnectingIroh") : t("reconnectingTailscale");
12527
13479
  controlRequestJson("/api/mobile-access/remote/reconnect", {
12528
13480
  method: "POST",
12529
13481
  body: "{}"
@@ -12644,7 +13596,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
12644
13596
  if (show) loadRemoteDevices();
12645
13597
  });
12646
13598
  remoteReset.addEventListener("click", () => {
12647
- const prompt = remoteProvider === "cpolar" ? t("resetCpolarConfirm") : remoteProvider === "cloudflared" ? t("resetCloudflaredConfirm") : remoteProvider === "frp" ? t("resetFrpConfirm") : remoteProvider === "origin" ? t("resetOriginConfirm") : t("resetTailscaleConfirm");
13599
+ const prompt = remoteProvider === "cpolar" ? t("resetCpolarConfirm") : remoteProvider === "cloudflared" ? t("resetCloudflaredConfirm") : remoteProvider === "frp" ? t("resetFrpConfirm") : remoteProvider === "origin" ? t("resetOriginConfirm") : remoteProvider === "iroh" ? t("resetIrohConfirm") : t("resetTailscaleConfirm");
12648
13600
  if (!window.confirm(prompt)) return;
12649
13601
  controlRequestJson("/api/mobile-access/remote/reset", {
12650
13602
  method: "POST",
@@ -13443,10 +14395,28 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
13443
14395
  });
13444
14396
  }
13445
14397
  };
13446
- /** Install authenticated mobile customization refresh, retaining loaded legacy assets through temporary failures. */
13447
- function installCustomAssets() {
14398
+ /**
14399
+ * Install authenticated customization refresh, retaining assets through temporary failures.
14400
+ * @param recovery - optional instance-owned authentication feedback and refresh registration.
14401
+ * @returns synchronous teardown for the customization UI and refresh admission.
14402
+ */
14403
+ function installCustomAssets(recovery) {
13448
14404
  const reporter = createMobileAssetLoadReporter(selectedMobileControlLocale);
14405
+ let refreshNow = () => {};
13449
14406
  const customizationOwner = ownsMobileCustomization(document.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") !== null, window.__DSH_MOBILE_FRONTEND__);
14407
+ const removeNotice = customizationOwner ? installCustomizationNotice(document, {
14408
+ locale: () => {
14409
+ const locale = selectedMobileControlLocale();
14410
+ return locale === "zh" || locale === "it" ? locale : "en";
14411
+ },
14412
+ reconnect: () => {
14413
+ const attempted = recovery?.retry() ?? false;
14414
+ refreshNow();
14415
+ return attempted;
14416
+ },
14417
+ subscribe: (listener) => reporter.subscribe(listener),
14418
+ message: (reason) => reporter.message(reason)
14419
+ }) : () => {};
13450
14420
  const legacyStyle = element("style");
13451
14421
  legacyStyle.dataset.plugin = "dsh-mobile-custom";
13452
14422
  document.head.append(legacyStyle);
@@ -13478,7 +14448,9 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
13478
14448
  if (reason === "authentication_required") events?.setEnabled(false);
13479
14449
  };
13480
14450
  const unavailable = (response, signal, fallback) => {
13481
- failed(response.status === 401 || response.status === 403 ? "authentication_required" : fallback, signal);
14451
+ const rejectedAuth = response.status === 401 || response.status === 403;
14452
+ failed(rejectedAuth ? "authentication_required" : fallback, signal);
14453
+ if (rejectedAuth && !disposed && !signal.aborted) recovery?.rejected(response.status);
13482
14454
  response.body?.cancel().catch((error) => {});
13483
14455
  };
13484
14456
  let expectedDefinitionId;
@@ -14038,6 +15010,10 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
14038
15010
  events?.setEnabled(true);
14039
15011
  }
14040
15012
  });
15013
+ refreshNow = () => {
15014
+ stopRefresh.refresh();
15015
+ };
15016
+ const unregisterRefresh = recovery?.registerRefresh(refreshNow);
14041
15017
  const stopEvents = startExtensionChangeStream(() => {
14042
15018
  stopRefresh.refresh();
14043
15019
  }, void 0, (payload) => {
@@ -14058,6 +15034,8 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
14058
15034
  events = stopEvents;
14059
15035
  return () => {
14060
15036
  disposed = true;
15037
+ unregisterRefresh?.();
15038
+ removeNotice();
14061
15039
  reporter.dispose();
14062
15040
  stopNativeBack();
14063
15041
  stopEvents();
@@ -14128,12 +15106,12 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
14128
15106
  /* The cloudflared setup section deliberately reuses this card, so
14129
15107
  .dsh-mobile-control__cloudflared-setup is a DOM hook for targeting rather than a
14130
15108
  style rule of its own. */
14131
- .dsh-mobile-control__cpolar-setup{margin:0 0 12px;padding:12px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__cpolar-setup[hidden],.dsh-mobile-control__cpolar-account[hidden],.dsh-mobile-control__details[hidden],.dsh-mobile-control__view.is-remote .dsh-mobile-control__actions[hidden],.dsh-mobile-control__danger[hidden]{display:none}.dsh-mobile-control__component-status,.dsh-mobile-control__component-note{margin:0 0 10px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.55}.dsh-mobile-control__cpolar-setup>.dsh-mobile-control__primary{width:100%;min-height:44px;padding:9px 12px;border-radius:10px;font:600 12px/1.3 system-ui;cursor:pointer}.dsh-mobile-control__cpolar-account{margin-top:10px}.dsh-mobile-control__link-row{display:flex;flex-wrap:wrap;gap:6px 12px;margin:0 0 10px}.dsh-mobile-control__text-link{color:var(--dsw-alias-label-primary-bluish,#2563eb);font-size:11px;text-decoration:none}.dsh-mobile-control__text-link:hover{text-decoration:underline}.dsh-mobile-control__token-label{display:flex;flex-direction:column;gap:5px;margin:0 0 8px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px}.dsh-mobile-control__token{box-sizing:border-box;width:100%;min-height:44px;padding:9px 10px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2,#fff));color:var(--dsw-alias-label-primary,#16181d);font:16px/1.4 system-ui}.dsh-mobile-control__cpolar-connect{display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:100%;min-height:44px;padding:10px 14px;border-radius:12px;font:650 13px/1.2 system-ui;cursor:pointer;transition:background-color 160ms ease,border-color 160ms ease,opacity 160ms ease}.dsh-mobile-control__cpolar-connect:hover:not(:disabled){border-color:#1d4ed8;background:#1d4ed8}.dsh-mobile-control__cpolar-connect:active:not(:disabled){border-color:#1e40af;background:#1e40af}.dsh-mobile-control__cpolar-connect:disabled{cursor:wait;opacity:.55}.dsh-mobile-control__details{margin:10px 0 0;border-top:1px solid var(--dsw-alias-border-l2,#e1e5eb);padding-top:9px}.dsh-mobile-control__details>summary{min-height:36px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:30px;cursor:pointer}.dsh-mobile-control__details-body{display:flex;flex-wrap:wrap;align-items:center;gap:7px 12px;padding:4px 0}.dsh-mobile-control__details-body p{flex:1 0 100%;margin:0;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5}.dsh-mobile-control__storage{display:block;flex:1 0 100%;max-width:100%;overflow:hidden;padding:7px 8px;border-radius:8px;background:var(--dsw-alias-bg-layer-1,#f3f5f8);color:var(--dsw-alias-label-secondary,#475569);font:11px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__danger{flex:1 0 100%;min-height:44px;margin-top:3px;padding:7px 10px;border:1px solid var(--dsw-alias-state-error-primary,#dc2626);border-radius:9px;background:transparent;color:var(--dsw-alias-label-primary,#dc2626);font:12px/1.3 system-ui;cursor:pointer}
15109
+ .dsh-mobile-control__cpolar-setup{margin:0 0 12px;padding:12px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__cpolar-setup[hidden],.dsh-mobile-control__cpolar-account[hidden],.dsh-mobile-control__details[hidden],.dsh-mobile-control__view.is-remote .dsh-mobile-control__actions[hidden],.dsh-mobile-control__danger[hidden]{display:none}.dsh-mobile-control__component-status,.dsh-mobile-control__component-note{margin:0 0 10px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.55}.dsh-mobile-control__progress{box-sizing:border-box;width:100%;height:6px;margin:0 0 10px}.dsh-mobile-control__progress[hidden]{display:none}.dsh-mobile-control__cpolar-setup>.dsh-mobile-control__primary{width:100%;min-height:44px;padding:9px 12px;border-radius:10px;font:600 12px/1.3 system-ui;cursor:pointer}.dsh-mobile-control__cpolar-account{margin-top:10px}.dsh-mobile-control__link-row{display:flex;flex-wrap:wrap;gap:6px 12px;margin:0 0 10px}.dsh-mobile-control__text-link{color:var(--dsw-alias-label-primary-bluish,#2563eb);font-size:11px;text-decoration:none}.dsh-mobile-control__text-link:hover{text-decoration:underline}.dsh-mobile-control__token-label{display:flex;flex-direction:column;gap:5px;margin:0 0 8px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px}.dsh-mobile-control__token{box-sizing:border-box;width:100%;min-height:44px;padding:9px 10px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2,#fff));color:var(--dsw-alias-label-primary,#16181d);font:16px/1.4 system-ui}.dsh-mobile-control__cpolar-connect{display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:100%;min-height:44px;padding:10px 14px;border-radius:12px;font:650 13px/1.2 system-ui;cursor:pointer;transition:background-color 160ms ease,border-color 160ms ease,opacity 160ms ease}.dsh-mobile-control__cpolar-connect:hover:not(:disabled){border-color:#1d4ed8;background:#1d4ed8}.dsh-mobile-control__cpolar-connect:active:not(:disabled){border-color:#1e40af;background:#1e40af}.dsh-mobile-control__cpolar-connect:disabled{cursor:wait;opacity:.55}.dsh-mobile-control__details{margin:10px 0 0;border-top:1px solid var(--dsw-alias-border-l2,#e1e5eb);padding-top:9px}.dsh-mobile-control__details>summary{min-height:36px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:30px;cursor:pointer}.dsh-mobile-control__details-body{display:flex;flex-wrap:wrap;align-items:center;gap:7px 12px;padding:4px 0}.dsh-mobile-control__details-body p{flex:1 0 100%;margin:0;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5}.dsh-mobile-control__storage{display:block;flex:1 0 100%;max-width:100%;overflow:hidden;padding:7px 8px;border-radius:8px;background:var(--dsw-alias-bg-layer-1,#f3f5f8);color:var(--dsw-alias-label-secondary,#475569);font:11px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__danger{flex:1 0 100%;min-height:44px;margin-top:3px;padding:7px 10px;border:1px solid var(--dsw-alias-state-error-primary,#dc2626);border-radius:9px;background:transparent;color:var(--dsw-alias-label-primary,#dc2626);font:12px/1.3 system-ui;cursor:pointer}
14132
15110
  .dsh-mobile-control__danger:hover:not(:disabled),.dsh-mobile-control__device-revoke:hover{background:var(--dsw-alias-interactive-bg-hover-danger,rgb(220 38 38 / 8%))}
14133
15111
  .dsh-mobile-control__frp-setup{margin:0;padding:12px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__frp-setup[hidden]{display:none}.dsh-mobile-control__frp-step{padding:11px 0}.dsh-mobile-control__frp-step + .dsh-mobile-control__frp-step{border-top:1px solid var(--dsw-alias-border-l2,#e1e5eb)}.dsh-mobile-control__frp-step>strong{display:block;margin-bottom:3px;font-size:12px;line-height:1.4}.dsh-mobile-control__frp-step>p{margin:0 0 9px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5}.dsh-mobile-control__frp-changes{margin:0 0 9px;padding:0;list-style:none;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5}.dsh-mobile-control__frp-changes li{position:relative;margin:0;padding-left:12px}.dsh-mobile-control__frp-changes li+li{margin-top:5px}.dsh-mobile-control__frp-changes li::before{content:"•";position:absolute;left:2px;color:var(--dsw-alias-label-tertiary,#98a1ad)}.dsh-mobile-control__frp-step>.dsh-mobile-control__frp-requirement{padding:8px 9px;border-radius:9px;background:var(--dsw-alias-bg-layer-1,#f3f5f8);color:var(--dsw-alias-label-primary,#384152);font-size:11px}.dsh-mobile-control__frp-fields{display:grid;grid-template-columns:minmax(0,1fr) 96px;gap:8px}.dsh-mobile-control__frp-fields>.dsh-mobile-control__field:nth-child(5),.dsh-mobile-control__frp-fields>.dsh-mobile-control__field:nth-child(6){grid-column:1/-1}.dsh-mobile-control__frp-fields>.dsh-mobile-control__field[hidden]{display:none}.dsh-mobile-control__frp-fields select{box-sizing:border-box;width:100%;min-height:44px;padding:9px 32px 9px 10px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);font:14px/1.4 system-ui}.dsh-mobile-control__frp-fields select:disabled{cursor:not-allowed;opacity:.55}.dsh-mobile-control__frp-hint{margin:0;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5}.dsh-mobile-control__field{display:flex;min-width:0;flex-direction:column;gap:5px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px}.dsh-mobile-control__field:nth-child(3),.dsh-mobile-control__field:nth-child(4){grid-column:1/-1}.dsh-mobile-control__field input{box-sizing:border-box;width:100%;min-height:44px;padding:9px 10px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);font:16px/1.4 system-ui}.dsh-mobile-control__frp-action{box-sizing:border-box;width:100%;min-height:44px;padding:9px 12px;border-radius:10px;font:650 12px/1.3 system-ui;cursor:pointer}.dsh-mobile-control__frp-action:disabled{cursor:not-allowed;opacity:.5}.dsh-mobile-control__remote-workspace{margin:0;padding:12px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:15px;background:var(--dsw-alias-bg-layer-1,#f7f8fa)}.dsh-mobile-control__stage-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:9px}.dsh-mobile-control__stage-header .dsh-mobile-control__section-title{margin:0}.dsh-mobile-control__stage-meta{display:flex;min-width:0;align-items:center;justify-content:flex-end;gap:5px}.dsh-mobile-control__stage-value{max-width:115px;overflow:hidden;color:var(--dsw-alias-label-primary,#16181d);font:650 11px/1.3 system-ui;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__state-badge{flex:none;padding:3px 7px;border-radius:999px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-secondary,#606873);font:650 11px/1.25 system-ui}.dsh-mobile-control__state-badge.is-ready{background:var(--dsw-alias-state-success-tertiary,#e6f7f0);color:var(--dsw-alias-label-primary,#087454)}.dsh-mobile-control__state-badge.is-busy{background:var(--dsw-alias-state-business-tertiary,#e8f0ff);color:var(--dsw-alias-label-primary,#1d4ed8)}.dsh-mobile-control__state-badge.is-attention{background:var(--dsw-alias-state-warn-tertiary,#fff4dc);color:var(--dsw-alias-label-primary,#935100)}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__status{box-sizing:border-box;margin:0 0 10px;padding:9px 10px;border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);font-size:11px;line-height:1.45}.dsh-mobile-control__provider-setup-body{margin:0 0 10px}.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__cpolar-setup{margin:0}.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__cpolar-setup>.dsh-mobile-control__section-title,.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__frp-setup>.dsh-mobile-control__section-title{display:none}.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__details{margin:0;padding:9px 10px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:11px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__actions{margin-top:2px}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__qr{margin:10px 0 0}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__manage-row{margin-top:10px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2,#e1e5eb)}
14134
15112
  .dsh-mobile-control__frp-overview{display:grid;grid-template-columns:32px minmax(0,1fr);align-items:center;gap:10px;margin:0 0 10px;padding:10px;border:1px solid var(--dsw-alias-state-success-primary,#a9dfc9);border-radius:11px;background:var(--dsw-alias-state-success-tertiary,#edf9f4)}.dsh-mobile-control__frp-overview[hidden]{display:none}.dsh-mobile-control__frp-overview-mark{display:grid;width:32px;height:32px;place-items:center;border-radius:50%;background:#087454;color:#fff;font:700 15px/1 system-ui}.dsh-mobile-control__frp-overview-body{display:flex;min-width:0;flex-direction:column;gap:2px}.dsh-mobile-control__frp-overview-body strong{color:var(--dsw-alias-label-primary,#075d46);font-size:12px;line-height:1.35}.dsh-mobile-control__frp-overview-body span{overflow:hidden;color:var(--dsw-alias-label-secondary,#357061);font:11px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__frp-group{margin:8px 0 0;overflow:hidden;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:11px;background:var(--dsw-alias-bg-layer-1,#f8fafc)}.dsh-mobile-control__frp-group>summary{box-sizing:border-box;min-height:44px;padding:12px 34px 10px 12px;color:var(--dsw-alias-label-primary,#16181d);font:650 12px/1.4 system-ui;cursor:pointer}.dsh-mobile-control__frp-group[open]>summary{border-bottom:1px solid var(--dsw-alias-border-l2,#e1e5eb);background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__frp-group>.dsh-mobile-control__frp-step{padding:12px}.dsh-mobile-control__frp-group>.dsh-mobile-control__frp-step>strong:first-child{display:none}.dsh-mobile-control__frp-group .dsh-mobile-control__frp-step{border-top:0}.dsh-mobile-control__frp-setup>.dsh-mobile-control__frp-step{margin-bottom:8px;padding:10px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:11px;background:var(--dsw-alias-bg-layer-1,#f8fafc)}.dsh-mobile-control__tunnel{margin:0 0 10px}.dsh-mobile-control__tunnel[hidden]{display:none}
14135
15113
  .dsh-mobile-control__lan-setup{box-sizing:border-box;margin:0 0 12px;padding:14px;border:1px solid #6f96db;border-radius:15px;background:var(--dsw-alias-bg-layer-1,#f5f8ff)}.dsh-mobile-control__lan-setup[hidden],.dsh-mobile-control__lan-setup-form[hidden]{display:none}.dsh-mobile-control__lan-setup>.dsh-mobile-control__section-title{margin-bottom:5px}.dsh-mobile-control__lan-setup>.dsh-mobile-control__intro{margin-bottom:12px}.dsh-mobile-control__lan-setup-form{display:grid;gap:9px}.dsh-mobile-control__lan-setup .dsh-mobile-control__field select{box-sizing:border-box;width:100%;min-height:44px;padding:9px 32px 9px 10px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);font:14px/1.4 system-ui}.dsh-mobile-control__lan-setup .dsh-mobile-control__component-note{margin:0}.dsh-mobile-control__lan-setup-actions{display:grid;grid-template-columns:1fr 1.35fr;gap:8px}.dsh-mobile-control__lan-setup-actions button{box-sizing:border-box;min-width:0;min-height:44px;padding:9px 10px;border-radius:10px;font:650 12px/1.3 system-ui;cursor:pointer}.dsh-mobile-control__lan-setup-actions button:disabled{cursor:wait;opacity:.55}.dsh-mobile-control__lan-setup>.dsh-mobile-control__status{margin:10px 0 0;line-height:1.5}.dsh-mobile-control__lan-setup>.dsh-mobile-control__status:empty{display:none}
14136
- .dsh-mobile-control__access{display:flex;align-items:baseline;gap:6px;min-width:0;margin:0 0 12px}.dsh-mobile-control__access[hidden]{display:none}.dsh-mobile-control__access-label{flex:none;color:var(--dsw-alias-label-secondary,#606873);white-space:nowrap}.dsh-mobile-control__access-label::after{content:":"}.dsh-mobile-control__access-link{min-width:0;overflow:hidden;color:var(--dsw-alias-label-primary-bluish,#2563eb);text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__access-link:hover{text-decoration:underline}.dsh-mobile-control__qr{display:flex;justify-content:center;margin:0 0 12px}.dsh-mobile-control__qr[hidden]{display:none}.dsh-mobile-control__qr img{border-radius:12px;background:#fff;padding:8px}
15114
+ .dsh-mobile-control__access{display:flex;align-items:baseline;gap:6px;min-width:0;margin:0 0 12px}.dsh-mobile-control__access[hidden]{display:none}.dsh-mobile-control__access-label{flex:none;color:var(--dsw-alias-label-secondary,#606873);white-space:nowrap}.dsh-mobile-control__access-label::after{content:":"}.dsh-mobile-control__access-link{min-width:0;overflow:hidden;color:var(--dsw-alias-label-primary-bluish,#2563eb);text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__access-link:hover{text-decoration:underline}.dsh-mobile-control__qr{display:flex;justify-content:center;margin:0 0 12px}.dsh-mobile-control__qr[hidden]{display:none}.dsh-mobile-control__qr img{border-radius:12px;background:#fff;padding:8px}.dsh-mobile-control__qr.is-expandable{cursor:zoom-in}.dsh-mobile-control__qr.is-expandable:focus-visible{outline:3px solid var(--dsw-alias-state-business-primary,#2563eb);outline-offset:2px;border-radius:12px}.dsh-mobile-control__qr-zoom{position:fixed;inset:0;z-index:1001;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.72)}.dsh-mobile-control__qr-zoom[hidden]{display:none}.dsh-mobile-control__qr-zoom-image{width:min(84vw,360px);height:min(84vw,360px);border-radius:12px;background:#fff;padding:12px}.dsh-mobile-control__qr-zoom-close{position:absolute;top:12px;right:16px;min-width:44px;min-height:44px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);font:600 18px/1 system-ui;cursor:pointer}
14137
15115
  .dsh-mobile-control__status{margin:0 0 14px;overflow-wrap:anywhere;color:var(--dsw-alias-label-secondary,#606873)}.dsh-mobile-control__status::before{display:inline-block;width:8px;height:8px;margin-right:7px;border-radius:50%;background:var(--dsw-alias-label-tertiary,#98a1ad);content:""}.dsh-mobile-control__status.is-running::before{background:var(--dsw-alias-state-success-primary,#16a36a)}.dsh-mobile-control__status.is-key{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;word-break:break-all}
14138
15116
  .dsh-mobile-control__guide{margin:0 0 14px;padding:12px;border:1px solid #6f96db;border-radius:12px;background:var(--dsw-alias-bg-layer-1,#eff6ff)}.dsh-mobile-control__guide[hidden]{display:none}.dsh-mobile-control__guide-title{margin:0;color:var(--dsw-alias-label-primary,#172554);font:650 13px/1.45 system-ui}.dsh-mobile-control__guide-summary,.dsh-mobile-control__guide-note{margin:4px 0 0;color:var(--dsw-alias-label-secondary,#475569);font-size:12px;line-height:1.5}.dsh-mobile-control__guide-steps{margin:8px 0 0;padding-left:20px;color:var(--dsw-alias-label-primary,#1e293b);font-size:12px;line-height:1.6}.dsh-mobile-control__guide-note{color:var(--dsw-alias-label-secondary,#64748b)}.dsh-mobile-control__guide-actions{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px}.dsh-mobile-control__guide-actions button{min-width:0;min-height:44px;padding:8px;border-radius:10px;font:12px/1.25 system-ui;cursor:pointer}.dsh-mobile-control__guide-actions button:disabled{cursor:not-allowed;opacity:.45}
14139
15117
  .dsh-mobile-control__extensions{margin:0 0 12px;color:var(--dsw-alias-label-secondary,#606873);font-size:12px}
@@ -14160,6 +15138,9 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
14160
15138
  ` + EXTENSION_RECOVERY_STYLES;
14161
15139
  /** Mount the desktop control or mobile feature enhancements. */
14162
15140
  function apply(ctx) {
15141
+ let gatewayRejectionSink;
15142
+ let retryRecovery;
15143
+ let refreshCustomization;
14163
15144
  ctx.effect(() => installMobileUiLocalePersistence(document, window), "dsh-mobile: browser UI locale persistence");
14164
15145
  ctx.effect(() => {
14165
15146
  if (window.__DSH_MOBILE_FRONTEND__ !== "dedicated") return;
@@ -14168,7 +15149,26 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
14168
15149
  }, "dsh-mobile: authenticated gateway client trust");
14169
15150
  ctx.effect(() => {
14170
15151
  if (!ownsMobileCustomization(document.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") !== null, window.__DSH_MOBILE_FRONTEND__)) return;
14171
- return installMobileConnectionRecovery(() => ctx.get("connection"), window, document);
15152
+ let ownedRejection;
15153
+ const recovery = installMobileConnectionRecovery(() => ctx.get("connection"), window, document, (rejected, retry) => {
15154
+ ownedRejection = rejected;
15155
+ gatewayRejectionSink = rejected;
15156
+ retryRecovery = retry;
15157
+ }, () => {
15158
+ refreshCustomization?.();
15159
+ });
15160
+ if (recovery === void 0) {
15161
+ gatewayRejectionSink = void 0;
15162
+ retryRecovery = void 0;
15163
+ return;
15164
+ }
15165
+ return () => {
15166
+ if (gatewayRejectionSink === ownedRejection) {
15167
+ gatewayRejectionSink = void 0;
15168
+ retryRecovery = void 0;
15169
+ }
15170
+ return recovery();
15171
+ };
14172
15172
  }, "dsh-mobile: connection recovery");
14173
15173
  ctx.effect(() => {
14174
15174
  const desktopAdmin = isDesktopAdminSurface(location.hostname, location.search, window.__DSH_MOBILE_FRONTEND__, location.protocol);
@@ -14178,7 +15178,18 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
14178
15178
  style.textContent = desktopAdmin ? CONTROL_STYLES : NATIVE_MOBILE_STYLES;
14179
15179
  style.textContent += CLIENT_MODULE_STYLES;
14180
15180
  document.head.append(style);
14181
- const removeCustom = !desktopAdmin || gatewayCustomization ? installCustomAssets() : () => void 0;
15181
+ const removeCustom = !desktopAdmin || gatewayCustomization ? installCustomAssets({
15182
+ rejected: (status) => {
15183
+ gatewayRejectionSink?.(status);
15184
+ },
15185
+ retry: () => retryRecovery?.() ?? false,
15186
+ registerRefresh: (refresh) => {
15187
+ refreshCustomization = refresh;
15188
+ return () => {
15189
+ if (refreshCustomization === refresh) refreshCustomization = void 0;
15190
+ };
15191
+ }
15192
+ }) : () => void 0;
14182
15193
  const removeVoiceSession = installVoiceSession(!desktopAdmin);
14183
15194
  const removeDraftTransfer = !desktopAdmin && document.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") !== null ? installDraftTransfer(ctx, window, document) : () => void 0;
14184
15195
  const removeModuleSettings = ctx.slots.inject("settings.general.item", () => ctx.slots.register({
@@ -14227,7 +15238,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
14227
15238
  getLayout: () => ctx.get("layout"),
14228
15239
  getSidebarRight: () => ctx.get("sidebarRight")
14229
15240
  }));
14230
- const removeDirectoryPicker = window.__DSH_MOBILE_FRONTEND__ === "dedicated" ? installMobileDirectoryPicker(ctx, document) : () => Promise.resolve();
15241
+ const removeDirectoryPicker = window.__DSH_MOBILE_FRONTEND__ === "dedicated" && !hasUpstreamBrowsePicker(window) ? installMobileDirectoryPicker(ctx, document) : () => Promise.resolve();
14231
15242
  return async () => {
14232
15243
  const removal = removeDirectoryPicker();
14233
15244
  try {