dsh-mobile 0.6.15 → 0.6.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -6,6 +6,44 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
8
  //#region src/client-messages.ts
9
+ const frpFormMessages = {
10
+ en: {
11
+ frpServerAddressInvalid: "Enter the frps IP address or full hostname only, without https://, a port, or a path.",
12
+ frpServerPortInvalid: "Enter the frps control port as a whole number from 1 to 65535.",
13
+ frpTokenInvalid: "Enter a shared token of 16–512 characters without spaces or line breaks. Re-enter it when changing saved connection settings.",
14
+ frpPublicOriginInvalid: "Enter an HTTPS address such as https://phone.example.com or https://1.2.3.4, without a path, credentials, or a non-standard port. An IPv4 address must be publicly routable.",
15
+ frpPublicOriginHint: "Include https://. Do not include a path or non-standard port. For self-signed ingress, enter https://PUBLIC_IP here and set its port separately below.",
16
+ frpModeInvalid: "Choose either managed deployment or an existing frps.",
17
+ frpVhostPortInvalid: "Enter the real frps vhostHTTPPort as a whole number from 1 to 65535.",
18
+ frpPublicPortInvalid: "Enter the public ingress port as a whole number from 1 to 65535, excluding 3080, 3443, and 3444. Keep this port out of the HTTPS address above.",
19
+ frpActionBusy: "Another remote operation is in progress. Wait for it to finish before saving.",
20
+ frpInstallBeforeConnect: "Install the local frpc component in step 3 before saving and verifying the connection."
21
+ },
22
+ it: {
23
+ frpServerAddressInvalid: "Inserisci solo l’indirizzo IP o il nome host completo di frps, senza https://, porta o percorso.",
24
+ frpServerPortInvalid: "Inserisci la porta di controllo frps come numero intero da 1 a 65535.",
25
+ frpTokenInvalid: "Inserisci un token condiviso di 16–512 caratteri senza spazi o ritorni a capo. Reinseriscilo quando modifichi una connessione salvata.",
26
+ frpPublicOriginInvalid: "Inserisci un indirizzo HTTPS come https://phone.example.com o https://1.2.3.4, senza percorso, credenziali o porta non standard. Un indirizzo IPv4 deve essere pubblico e instradabile.",
27
+ frpPublicOriginHint: "Includi https://, senza percorso o porta non standard. Per l’ingresso autofirmato usa https://IP_PUBBLICO e indica la porta separatamente qui sotto.",
28
+ frpModeInvalid: "Scegli la distribuzione gestita oppure un frps esistente.",
29
+ frpVhostPortInvalid: "Inserisci il vero vhostHTTPPort di frps come numero intero da 1 a 65535.",
30
+ frpPublicPortInvalid: "Inserisci una porta pubblica intera da 1 a 65535, esclusi 3080, 3443 e 3444. Non aggiungerla all’indirizzo HTTPS sopra.",
31
+ frpActionBusy: "È in corso un’altra operazione remota. Attendi prima di salvare.",
32
+ frpInstallBeforeConnect: "Installa il componente frpc locale al punto 3 prima di salvare e verificare la connessione."
33
+ },
34
+ zh: {
35
+ frpServerAddressInvalid: "请只填写 frps 的 IP 或完整域名,不要包含 https://、端口或路径。",
36
+ frpServerPortInvalid: "请填写 1–65535 的整数,作为 frps 控制端口。",
37
+ frpTokenInvalid: "共享 Token 必须为 16–512 个字符,不能包含空格或换行。修改已保存的连接信息时,请重新输入 Token。",
38
+ frpPublicOriginInvalid: "请填写 https://phone.example.com 或 https://1.2.3.4 形式的 HTTPS 地址,不含路径、账号密码或非标准端口;IPv4 必须为可路由的公网地址。",
39
+ frpPublicOriginHint: "必须包含 https://,不要带路径或非标准端口。自签入口在此填 https://公网IP,端口在下方单独填写。",
40
+ frpModeInvalid: "请选择“由插件部署 frps”或“接入我已有的 frps”。",
41
+ frpVhostPortInvalid: "请填写你 frps 真实的 vhostHTTPPort,必须为 1–65535 的整数。",
42
+ frpPublicPortInvalid: "公网入口端口必须为 1–65535 的整数,不能使用 3080、3443、3444;不要把它加到上方 HTTPS 地址中。",
43
+ frpActionBusy: "另一项远程操作正在进行,请等待完成后再保存。",
44
+ frpInstallBeforeConnect: "请先在第 3 步安装本机 frpc 组件,再保存并验证连接。"
45
+ }
46
+ };
9
47
  const storageMessages = {
10
48
  en: {
11
49
  funnelStateInvalid: "The private Tailscale state folder cannot be accessed safely. Check its permissions or directory links; reinstalling the component is not required.",
@@ -61,6 +99,7 @@ window.__ModuleLoader__.load({
61
99
  };
62
100
  const MOBILE_CONTROL_MESSAGES = {
63
101
  en: {
102
+ ...frpFormMessages.en,
64
103
  mobileAccess: "Mobile access",
65
104
  collapseMobileAccess: "Collapse Mobile access",
66
105
  downloadAndroid: "Download latest Android app",
@@ -229,6 +268,7 @@ window.__ModuleLoader__.load({
229
268
  wsPathsAdvancedHint: "Manual entries are rarely necessary: use the buttons above. Only fill this in when you know the exact path."
230
269
  },
231
270
  it: {
271
+ ...frpFormMessages.it,
232
272
  mobileAccess: "Accesso mobile",
233
273
  collapseMobileAccess: "Riduci Accesso mobile",
234
274
  downloadAndroid: "Scarica l’ultima app Android",
@@ -396,7 +436,7 @@ window.__ModuleLoader__.load({
396
436
  wsPathsAnnounce: "{n} connessioni di plugin in attesa di approvazione",
397
437
  wsPathsAdvancedHint: "Le voci manuali servono raramente: usa i pulsanti qui sopra. Compila solo se conosci il percorso esatto."
398
438
  },
399
- zh: {}
439
+ zh: { ...frpFormMessages.zh }
400
440
  };
401
441
  for (const locale of [
402
442
  "en",
@@ -2494,6 +2534,85 @@ window.__ModuleLoader__.load({
2494
2534
  return createFrpAttachTemplateParts(settings, options).text;
2495
2535
  }
2496
2536
  //#endregion
2537
+ //#region src/frp-form-validation.ts
2538
+ /** Field ownership for errors returned by the Host parser. */
2539
+ const FRP_FORM_ERRORS = {
2540
+ frp_server_address_invalid: {
2541
+ field: "serverAddress",
2542
+ code: "frp_server_address_invalid",
2543
+ messageKey: "frpServerAddressInvalid"
2544
+ },
2545
+ frp_server_port_invalid: {
2546
+ field: "serverPort",
2547
+ code: "frp_server_port_invalid",
2548
+ messageKey: "frpServerPortInvalid"
2549
+ },
2550
+ frp_token_invalid: {
2551
+ field: "token",
2552
+ code: "frp_token_invalid",
2553
+ messageKey: "frpTokenInvalid"
2554
+ },
2555
+ frp_public_origin_invalid: {
2556
+ field: "publicOrigin",
2557
+ code: "frp_public_origin_invalid",
2558
+ messageKey: "frpPublicOriginInvalid"
2559
+ },
2560
+ frp_attach_mode_requires_vhost_port: {
2561
+ field: "vhostHttpPort",
2562
+ code: "frp_attach_mode_requires_vhost_port",
2563
+ messageKey: "frpAttachModeRequiresVhostPort"
2564
+ },
2565
+ frp_entry_tls_invalid: {
2566
+ field: "entryTls",
2567
+ code: "frp_entry_tls_invalid",
2568
+ messageKey: "frpEntryTlsInvalid"
2569
+ },
2570
+ frp_self_signed_requires_public_ipv4: {
2571
+ field: "publicOrigin",
2572
+ code: "frp_self_signed_requires_public_ipv4",
2573
+ messageKey: "frpSelfSignedRequiresPublicIpv4"
2574
+ }
2575
+ };
2576
+ /** Collect field errors using the Host validators; token omission is permitted only for saved configuration. */
2577
+ function validateFrpForm(form, allowSavedToken = false) {
2578
+ const issues = [];
2579
+ const check = (field, messageKey, validate) => {
2580
+ try {
2581
+ validate();
2582
+ } catch (error) {
2583
+ issues.push({
2584
+ field,
2585
+ messageKey,
2586
+ code: error instanceof Error ? error.message : "frp_settings_invalid"
2587
+ });
2588
+ }
2589
+ };
2590
+ check("serverAddress", "frpServerAddressInvalid", () => validateFrpServerAddress(form.serverAddress));
2591
+ check("serverPort", "frpServerPortInvalid", () => validateFrpServerPort(form.serverPort));
2592
+ if (form.token !== "" || !allowSavedToken) check("token", "frpTokenInvalid", () => validateFrpToken(form.token));
2593
+ check("publicOrigin", "frpPublicOriginInvalid", () => validateFrpPublicOrigin(form.publicOrigin));
2594
+ check("mode", "frpModeInvalid", () => validateFrpMode(form.mode));
2595
+ check("entryTls", "frpEntryTlsInvalid", () => validateFrpEntryTls(form.entryTls));
2596
+ if (form.entryTls === "self-signed" && form.mode !== "attach") issues.push(FRP_FORM_ERRORS.frp_entry_tls_invalid);
2597
+ if (form.entryTls !== "self-signed" && form.vhostHttpPort !== void 0) check("vhostHttpPort", "frpVhostPortInvalid", () => validateFrpVhostHttpPort(form.vhostHttpPort));
2598
+ else if (form.entryTls !== "self-signed" && form.mode === "attach") issues.push(FRP_FORM_ERRORS.frp_attach_mode_requires_vhost_port);
2599
+ if (form.publicPort !== void 0 || form.entryTls === "self-signed") check("publicPort", "frpPublicPortInvalid", () => validateFrpPublicPort(form.publicPort));
2600
+ if (issues.length === 0) try {
2601
+ parseFrpSettings({
2602
+ ...form,
2603
+ token: form.token === "" && allowSavedToken ? "0123456789abcdef" : form.token
2604
+ });
2605
+ } catch (error) {
2606
+ const code = error instanceof Error ? error.message : "frp_settings_invalid";
2607
+ issues.push(FRP_FORM_ERRORS[code] ?? {
2608
+ field: "mode",
2609
+ code,
2610
+ messageKey: "frpInputInvalid"
2611
+ });
2612
+ }
2613
+ return issues;
2614
+ }
2615
+ //#endregion
2497
2616
  //#region node_modules/@noble/hashes/esm/utils.js
2498
2617
  /*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) */
2499
2618
  /** Checks if something is Uint8Array. Be careful: nodejs Buffer will return true. */
@@ -4362,14 +4481,27 @@ window.__ModuleLoader__.load({
4362
4481
  icon.append(body, lens);
4363
4482
  return icon;
4364
4483
  };
4365
- const createMediaAction = (text) => {
4484
+ const createMediaAction = (text, kind = "camera") => {
4366
4485
  const button = document.createElement("button");
4367
4486
  button.type = "button";
4368
4487
  button.className = "dsh-mobile-media-action";
4369
4488
  button.lang = language;
4370
- button.dataset.dshMobileMediaAction = "camera";
4489
+ button.dataset.dshMobileMediaAction = kind;
4490
+ button.setAttribute("role", "option");
4491
+ button.setAttribute("aria-selected", "false");
4492
+ button.setAttribute("aria-label", text);
4371
4493
  const icon = document.createElement("span");
4372
- icon.append(mediaIcon());
4494
+ const graphic = mediaIcon();
4495
+ if (kind === "gallery") {
4496
+ graphic.replaceChildren();
4497
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
4498
+ path.setAttribute("d", "M2 2h12v12H2z M2 11l4-4 3 3 2-2 3 3 M10 5h.01");
4499
+ path.setAttribute("stroke", "currentColor");
4500
+ path.setAttribute("stroke-width", "1.3");
4501
+ path.setAttribute("stroke-linejoin", "round");
4502
+ graphic.append(path);
4503
+ }
4504
+ icon.append(graphic);
4373
4505
  button.append(icon);
4374
4506
  const caption = document.createElement("span");
4375
4507
  caption.textContent = text;
@@ -4462,6 +4594,9 @@ window.__ModuleLoader__.load({
4462
4594
  document.body.append(mediaToast);
4463
4595
  const cameraButton = createMediaAction(label("Scatta foto", "Take photo", "拍照"));
4464
4596
  cameraButton.disabled = true;
4597
+ const galleryButton = createMediaAction(label("Galleria", "Gallery", "相册"), "gallery");
4598
+ galleryButton.disabled = true;
4599
+ let galleryBusy = false;
4465
4600
  let branchToastTimer = 0;
4466
4601
  let mediaToastTimer = 0;
4467
4602
  const showMediaToast = (message) => {
@@ -4549,40 +4684,48 @@ window.__ModuleLoader__.load({
4549
4684
  const sectionTitle = Array.from(commandMenu.querySelectorAll("[role=\"listbox\"] [role=\"presentation\"]")).find((candidate) => !candidate.hasAttribute("data-source") && candidate.textContent?.trim() === addGroupLabel);
4550
4685
  if (sectionTitle === void 0) {
4551
4686
  cameraButton.remove();
4687
+ galleryButton.remove();
4552
4688
  return false;
4553
4689
  }
4554
- cameraButton.className = "dsh-mobile-media-action";
4555
4690
  const nativeItem = (() => {
4556
4691
  let sibling = sectionTitle.nextElementSibling;
4557
4692
  while (sibling instanceof HTMLElement && sibling.getAttribute("role") !== "presentation") {
4558
- if (sibling.matches("button[role=\"option\"]:not([aria-selected=\"true\"])")) return sibling;
4693
+ if (!sibling.hasAttribute("data-dsh-mobile-media-action") && sibling.matches("button[role=\"option\"]:not([aria-selected=\"true\"])")) return sibling;
4559
4694
  sibling = sibling.nextElementSibling;
4560
4695
  }
4561
4696
  })();
4562
- if (nativeItem !== void 0) {
4563
- cameraButton.className = nativeItem.className;
4564
- const cameraIcon = cameraButton.firstElementChild;
4565
- const cameraLabel = cameraButton.lastElementChild;
4566
- const nativeIcon = firstByClassSuffix(nativeItem, "_itemIcon");
4567
- const nativeLabel = firstByClassSuffix(nativeItem, "_itemName");
4568
- if (cameraIcon instanceof HTMLElement && nativeIcon !== void 0) cameraIcon.className = nativeIcon.className;
4569
- if (cameraLabel instanceof HTMLElement && nativeLabel !== void 0) cameraLabel.className = nativeLabel.className;
4570
- }
4571
- cameraButton.setAttribute("role", "option");
4572
- cameraButton.setAttribute("aria-selected", "false");
4573
- cameraButton.setAttribute("aria-label", label("Scatta foto", "Take photo", "拍照"));
4697
+ for (const button of [cameraButton, galleryButton]) {
4698
+ const className = nativeItem?.className ?? "dsh-mobile-media-action";
4699
+ if (button.className !== className) button.className = className;
4700
+ const icon = button.firstElementChild;
4701
+ const caption = button.lastElementChild;
4702
+ const nativeIcon = nativeItem === void 0 ? void 0 : firstByClassSuffix(nativeItem, "_itemIcon");
4703
+ const nativeLabel = nativeItem === void 0 ? void 0 : firstByClassSuffix(nativeItem, "_itemName");
4704
+ if (icon instanceof HTMLElement && nativeIcon !== void 0 && icon.className !== nativeIcon.className) icon.className = nativeIcon.className;
4705
+ if (caption instanceof HTMLElement && nativeLabel !== void 0 && caption.className !== nativeLabel.className) caption.className = nativeLabel.className;
4706
+ }
4574
4707
  let lastItem = sectionTitle;
4575
4708
  let sibling = sectionTitle.nextElementSibling;
4576
4709
  while (sibling instanceof HTMLElement && sibling.getAttribute("role") !== "presentation") {
4577
- if (sibling !== cameraButton) lastItem = sibling;
4710
+ if (sibling !== cameraButton && sibling !== galleryButton) lastItem = sibling;
4578
4711
  sibling = sibling.nextElementSibling;
4579
4712
  }
4580
- if (cameraButton.parentElement !== sectionTitle.parentElement || cameraButton.previousElementSibling !== lastItem) lastItem.after(cameraButton);
4713
+ if (boundComposer?.closest(".dshm-main") !== null && boundComposer !== null) {
4714
+ if (galleryButton.parentElement !== sectionTitle.parentElement || galleryButton.previousElementSibling !== sectionTitle) sectionTitle.after(galleryButton);
4715
+ if (cameraButton.parentElement !== galleryButton.parentElement || cameraButton.previousElementSibling !== galleryButton) galleryButton.after(cameraButton);
4716
+ } else {
4717
+ galleryButton.remove();
4718
+ if (cameraButton.parentElement !== sectionTitle.parentElement || cameraButton.previousElementSibling !== lastItem) lastItem.after(cameraButton);
4719
+ }
4581
4720
  return true;
4582
4721
  };
4583
4722
  const setCameraActionDisabled = (disabled, title) => {
4584
4723
  if (cameraButton.disabled !== disabled) cameraButton.disabled = disabled;
4585
4724
  if (cameraButton.title !== title) cameraButton.title = title;
4725
+ const galleryDisabled = disabled || galleryBusy;
4726
+ if (galleryButton.disabled !== galleryDisabled) galleryButton.disabled = galleryDisabled;
4727
+ const galleryTitle = disabled ? title : label("Scegli immagini da allegare", "Choose images to attach", "选择图片并附加到当前消息");
4728
+ if (galleryButton.title !== galleryTitle) galleryButton.title = galleryTitle;
4586
4729
  };
4587
4730
  const quietMediaPointer = (event) => {
4588
4731
  event.preventDefault();
@@ -4614,6 +4757,34 @@ window.__ModuleLoader__.load({
4614
4757
  showMediaToast(code === "payload_too_large" ? label("La foto supera il limite di 8 MiB", "The photo exceeds the 8 MiB limit", "照片超过 8 MiB 限制") : label("Impossibile aggiungere la foto", "Could not attach the photo", "无法附加照片"));
4615
4758
  });
4616
4759
  };
4760
+ const chooseGallery = () => {
4761
+ if (disposed || galleryBusy || galleryButton.disabled || boundComposer?.closest(".dshm-main") == null) return;
4762
+ if (!canAcceptComposerDrop()) {
4763
+ setCameraActionDisabled(true, label("Allegati immagine non disponibili", "Image attachments are unavailable", "图片附件不可用"));
4764
+ dismissCommandMenu();
4765
+ return;
4766
+ }
4767
+ const context = mediaRequestContext();
4768
+ galleryBusy = true;
4769
+ galleryButton.disabled = true;
4770
+ galleryButton.setAttribute("aria-busy", "true");
4771
+ dismissCommandMenu();
4772
+ pickBrowserFiles({
4773
+ accept: "image/*",
4774
+ multiple: true,
4775
+ signal: mediaPickerAbortController.signal
4776
+ }).then((files) => {
4777
+ if (files !== void 0) deliverImages(files, context);
4778
+ }).catch((_error) => {
4779
+ if (mediaRequestIsCurrent(context)) showMediaToast(label("Impossibile aggiungere le immagini", "Could not attach the images", "无法附加图片"));
4780
+ }).finally(() => {
4781
+ galleryBusy = false;
4782
+ galleryButton.removeAttribute("aria-busy");
4783
+ if (!disposed) schedule();
4784
+ });
4785
+ };
4786
+ galleryButton.addEventListener("pointerdown", quietMediaPointer);
4787
+ galleryButton.addEventListener("click", chooseGallery);
4617
4788
  cameraButton.addEventListener("pointerdown", quietMediaPointer);
4618
4789
  cameraButton.addEventListener("click", takePhoto);
4619
4790
  const showBranchToast = () => {
@@ -4878,6 +5049,7 @@ window.__ModuleLoader__.load({
4878
5049
  syncMediaBinding(null);
4879
5050
  setCameraActionDisabled(true, label("Apri prima una sessione", "Open a session first", "请先打开会话"));
4880
5051
  cameraButton.remove();
5052
+ galleryButton.remove();
4881
5053
  return;
4882
5054
  }
4883
5055
  if (center !== void 0) {
@@ -4917,6 +5089,7 @@ window.__ModuleLoader__.load({
4917
5089
  syncMediaBinding(null);
4918
5090
  setCameraActionDisabled(true, label("Apri prima una sessione", "Open a session first", "请先打开会话"));
4919
5091
  cameraButton.remove();
5092
+ galleryButton.remove();
4920
5093
  }
4921
5094
  if (composerRow instanceof HTMLElement) {
4922
5095
  composerRow.dataset.dshMobileComposerRow = "true";
@@ -4936,7 +5109,10 @@ window.__ModuleLoader__.load({
4936
5109
  setCameraActionDisabled(mediaDisabled, mediaTitle);
4937
5110
  const commandMenu = composerCard?.querySelector("[data-trigger-menu]") ?? null;
4938
5111
  if (commandMenu !== null) placeCameraAction(commandMenu);
4939
- else cameraButton.remove();
5112
+ else {
5113
+ cameraButton.remove();
5114
+ galleryButton.remove();
5115
+ }
4940
5116
  } else syncComposerEnterNewline(null);
4941
5117
  if (composerTrailing !== void 0 && composerTrailing !== composerTools) {
4942
5118
  composerTrailing.dataset.dshMobileComposerTrailing = "true";
@@ -5052,6 +5228,9 @@ window.__ModuleLoader__.load({
5052
5228
  applyStrip(0);
5053
5229
  cameraButton.removeEventListener("pointerdown", quietMediaPointer);
5054
5230
  cameraButton.removeEventListener("click", takePhoto);
5231
+ galleryButton.removeEventListener("pointerdown", quietMediaPointer);
5232
+ galleryButton.removeEventListener("click", chooseGallery);
5233
+ galleryButton.remove();
5055
5234
  if (branchToastTimer !== 0) window.clearTimeout(branchToastTimer);
5056
5235
  if (mediaToastTimer !== 0) window.clearTimeout(mediaToastTimer);
5057
5236
  branchToast.remove();
@@ -5184,6 +5363,363 @@ window.__ModuleLoader__.load({
5184
5363
  release();
5185
5364
  };
5186
5365
  }
5366
+ const LEASE_MS = 3e4;
5367
+ function transferableText(text) {
5368
+ return !/[\uE100-\uE11D\uFFFC]/u.test(text) && new TextEncoder().encode(text).length <= 262144;
5369
+ }
5370
+ function snapshotShape(value) {
5371
+ if (value === null || typeof value !== "object") return false;
5372
+ const s = value;
5373
+ return s.version === 1 && typeof s.token === "string" && /^[a-f0-9]{48}$/u.test(s.token) && typeof s.href === "string" && s.href.length <= 8192 && typeof s.sessionId === "string" && s.sessionId.length > 0 && s.sessionId.length <= 1024 && typeof s.text === "string" && transferableText(s.text) && Number.isSafeInteger(s.revision) && (s.revision ?? -1) >= 0;
5374
+ }
5375
+ function plain(s) {
5376
+ return typeof s.draft === "string" && Number.isSafeInteger(s.draftRev) && s.draftRev >= 0 && s.phase === "plain" && s.claim === void 0 && Array.isArray(s.attachmentIds) && s.attachmentIds.length === 0 && Array.isArray(s.occurrences) && s.occurrences.length === 0 && Array.isArray(s.queue) && s.queue.length === 0;
5377
+ }
5378
+ /** Public state subscriptions are kept for the lifetime of each observed composer,
5379
+ * not just the lease: attachment/submit uncertainty stays sticky until reload. */
5380
+ function createDraftTransfer(view, page) {
5381
+ const observed = /* @__PURE__ */ new Map();
5382
+ const subscriptions = /* @__PURE__ */ new Set();
5383
+ let epoch = 0, risky = false, disposed = false, composing = false;
5384
+ let lease;
5385
+ let timer;
5386
+ let imported;
5387
+ let importTimer;
5388
+ let importClosed = false;
5389
+ const clearImport = () => {
5390
+ if (imported !== void 0) importClosed = true;
5391
+ imported = void 0;
5392
+ if (importTimer !== void 0) view.clearTimeout(importTimer);
5393
+ importTimer = void 0;
5394
+ };
5395
+ const invalidate = () => {
5396
+ epoch += 1;
5397
+ };
5398
+ const clearLease = () => {
5399
+ lease = void 0;
5400
+ if (timer !== void 0) view.clearTimeout(timer);
5401
+ timer = void 0;
5402
+ };
5403
+ const risk = () => {
5404
+ risky = true;
5405
+ invalidate();
5406
+ };
5407
+ const onActivity = (event) => {
5408
+ const inComposer = Boolean(event.target?.closest?.("[data-composer-card]"));
5409
+ if ([
5410
+ "input",
5411
+ "beforeinput",
5412
+ "paste",
5413
+ "compositionstart",
5414
+ "compositionend"
5415
+ ].includes(event.type) && !inComposer) {
5416
+ risk();
5417
+ return;
5418
+ }
5419
+ if (event.type === "compositionstart" || event.type === "compositionend") {
5420
+ composing = event.type === "compositionstart";
5421
+ invalidate();
5422
+ return;
5423
+ }
5424
+ if (event.type === "change" && event.target?.type !== "file") return;
5425
+ if (event.type === "paste") {
5426
+ const clipboard = event.clipboardData;
5427
+ if (clipboard !== null && clipboard !== void 0 && clipboard.files.length === 0 && clipboard.types.length === 1 && clipboard.types[0] === "text/plain" && Array.from(clipboard.items).every((item) => item.kind === "string" && item.type === "text/plain")) {
5428
+ invalidate();
5429
+ return;
5430
+ }
5431
+ }
5432
+ if (event.type === "input" || event.type === "beforeinput") invalidate();
5433
+ else risk();
5434
+ };
5435
+ const onClick = (event) => {
5436
+ if (event.target?.closest?.("[data-composer-card] button, [data-dsh-mobile-media-action], [data-voice-activity]")) risk();
5437
+ };
5438
+ const checkVoice = () => {
5439
+ if (page.querySelector("[data-voice-activity=\"requesting\"], [data-voice-activity=\"recording\"], [data-voice-activity=\"transcribing\"]") !== null) risk();
5440
+ };
5441
+ const voiceObserver = new MutationObserver(checkVoice);
5442
+ voiceObserver.observe(page.documentElement, {
5443
+ subtree: true,
5444
+ childList: true,
5445
+ attributes: true,
5446
+ attributeFilter: ["data-voice-activity"]
5447
+ });
5448
+ checkVoice();
5449
+ page.addEventListener("click", onClick, true);
5450
+ const events = [
5451
+ "compositionstart",
5452
+ "compositionend",
5453
+ "drop",
5454
+ "paste",
5455
+ "change",
5456
+ "submit",
5457
+ "input",
5458
+ "beforeinput",
5459
+ "dsh-mobile:voice-start",
5460
+ "dsh-mobile:voice-stop"
5461
+ ];
5462
+ for (const name of events) page.addEventListener(name, onActivity, true);
5463
+ const current = () => {
5464
+ if (disposed || risky || composing || observed.size !== 1) return void 0;
5465
+ for (const modal of page.querySelectorAll("[role=\"dialog\"][aria-modal=\"true\"]")) if (!modal.hidden && !modal.closest("[hidden], [inert], [aria-hidden=\"true\"]") && modal.getClientRects().length > 0) {
5466
+ const style = view.getComputedStyle(modal);
5467
+ if (style.display !== "none" && style.visibility !== "hidden") return void 0;
5468
+ }
5469
+ const entry = observed.values().next().value;
5470
+ if (entry === void 0 || !plain(entry.input.state.getSnapshot())) return void 0;
5471
+ return entry;
5472
+ };
5473
+ const api = {
5474
+ capture() {
5475
+ if (lease !== void 0) return null;
5476
+ const entry = current();
5477
+ if (entry === void 0) return null;
5478
+ const state = entry.input.state.getSnapshot();
5479
+ if (!transferableText(state.draft)) return null;
5480
+ const bytes = /* @__PURE__ */ new Uint8Array(24);
5481
+ try {
5482
+ view.crypto.getRandomValues(bytes);
5483
+ } catch {
5484
+ return null;
5485
+ }
5486
+ lease = {
5487
+ version: 1,
5488
+ token: Array.from(bytes, (n) => n.toString(16).padStart(2, "0")).join(""),
5489
+ href: view.location.href,
5490
+ sessionId: entry.sessionId,
5491
+ text: state.draft,
5492
+ revision: epoch
5493
+ };
5494
+ timer = view.setTimeout(clearLease, LEASE_MS);
5495
+ return { ...lease };
5496
+ },
5497
+ validate(value) {
5498
+ if (!snapshotShape(value) || lease === void 0) return false;
5499
+ const entry = current();
5500
+ return entry !== void 0 && value.token === lease.token && value.href === lease.href && value.href === view.location.href && value.sessionId === lease.sessionId && value.sessionId === entry.sessionId && value.revision === lease.revision && value.revision === epoch && value.text === lease.text && value.text === entry.input.state.getSnapshot().draft;
5501
+ },
5502
+ restore(value) {
5503
+ if (!snapshotShape(value)) return false;
5504
+ if (imported !== void 0) return api.verify(value);
5505
+ if (importClosed) return false;
5506
+ const entry = current();
5507
+ if (entry === void 0 || entry.sessionId !== value.sessionId) return false;
5508
+ const state = entry.input.state.getSnapshot();
5509
+ if (state.draft !== "" && state.draft !== value.text) return false;
5510
+ const wrote = state.draft !== value.text;
5511
+ imported = {
5512
+ snapshot: { ...value },
5513
+ entry,
5514
+ epoch,
5515
+ href: view.location.href,
5516
+ wrote,
5517
+ actualText: state.draft,
5518
+ successful: false
5519
+ };
5520
+ importTimer = view.setTimeout(clearImport, LEASE_MS);
5521
+ let writeFailed = false;
5522
+ if (wrote) try {
5523
+ entry.input.setDraft(value.text);
5524
+ } catch {
5525
+ writeFailed = true;
5526
+ }
5527
+ const actual = entry.input.state.getSnapshot();
5528
+ if (current() !== entry || !plain(actual)) return false;
5529
+ imported.epoch = epoch;
5530
+ imported.actualText = actual.draft;
5531
+ imported.successful = !writeFailed && actual.draft === value.text;
5532
+ return imported.successful;
5533
+ },
5534
+ verify(value) {
5535
+ if (!snapshotShape(value) || imported === void 0) return false;
5536
+ const saved = imported.snapshot;
5537
+ return imported.successful && value.token === saved.token && value.href === saved.href && value.sessionId === saved.sessionId && value.text === saved.text && value.revision === saved.revision && current() === imported.entry && epoch === imported.epoch && view.location.href === imported.href && imported.entry.input.state.getSnapshot().draft === saved.text;
5538
+ },
5539
+ rollback(value) {
5540
+ if (!snapshotShape(value) || imported === void 0) return false;
5541
+ const record = imported;
5542
+ const saved = record.snapshot;
5543
+ if (value.token !== saved.token || value.href !== saved.href || value.sessionId !== saved.sessionId || value.text !== saved.text || value.revision !== saved.revision || current() !== record.entry || epoch !== record.epoch || view.location.href !== record.href || record.entry.input.state.getSnapshot().draft !== record.actualText) return false;
5544
+ if (record.wrote) record.entry.input.setDraft("");
5545
+ const restored = !record.wrote || record.entry.input.state.getSnapshot().draft === "";
5546
+ clearImport();
5547
+ return restored;
5548
+ },
5549
+ release(value) {
5550
+ if (!snapshotShape(value)) return;
5551
+ if (value.token === lease?.token) clearLease();
5552
+ if (value.token === imported?.snapshot.token) clearImport();
5553
+ }
5554
+ };
5555
+ return {
5556
+ api: {
5557
+ capture() {
5558
+ try {
5559
+ return api.capture();
5560
+ } catch {
5561
+ risk();
5562
+ clearLease();
5563
+ return null;
5564
+ }
5565
+ },
5566
+ validate(value) {
5567
+ try {
5568
+ return api.validate(value);
5569
+ } catch {
5570
+ risk();
5571
+ return false;
5572
+ }
5573
+ },
5574
+ restore(value) {
5575
+ try {
5576
+ return api.restore(value);
5577
+ } catch {
5578
+ risk();
5579
+ return false;
5580
+ }
5581
+ },
5582
+ verify(value) {
5583
+ try {
5584
+ return api.verify(value);
5585
+ } catch {
5586
+ risk();
5587
+ return false;
5588
+ }
5589
+ },
5590
+ rollback(value) {
5591
+ try {
5592
+ return api.rollback(value);
5593
+ } catch {
5594
+ risk();
5595
+ return false;
5596
+ }
5597
+ },
5598
+ release(value) {
5599
+ try {
5600
+ api.release(value);
5601
+ } catch {}
5602
+ }
5603
+ },
5604
+ observe(sessionId, input) {
5605
+ if (disposed) return () => void 0;
5606
+ const key = {}, entry = {
5607
+ sessionId,
5608
+ input
5609
+ };
5610
+ observed.set(key, entry);
5611
+ invalidate();
5612
+ const changed = () => {
5613
+ invalidate();
5614
+ try {
5615
+ if (!plain(input.state.getSnapshot())) risky = true;
5616
+ } catch {
5617
+ risky = true;
5618
+ }
5619
+ };
5620
+ changed();
5621
+ const unsubscribe = input.state.subscribe(changed);
5622
+ const remove = () => {
5623
+ if (!subscriptions.delete(remove)) return;
5624
+ unsubscribe();
5625
+ observed.delete(key);
5626
+ invalidate();
5627
+ };
5628
+ subscriptions.add(remove);
5629
+ return remove;
5630
+ },
5631
+ dispose() {
5632
+ if (disposed) return;
5633
+ disposed = true;
5634
+ voiceObserver.disconnect();
5635
+ page.removeEventListener("click", onClick, true);
5636
+ clearLease();
5637
+ clearImport();
5638
+ for (const remove of subscriptions) remove();
5639
+ subscriptions.clear();
5640
+ observed.clear();
5641
+ for (const name of events) page.removeEventListener(name, onActivity, true);
5642
+ }
5643
+ };
5644
+ }
5645
+ /** Install only on the authenticated Mobile frontend; identity comes from the
5646
+ * real public session-scoped composer slot, state/writes from the public facade. */
5647
+ function installDraftTransfer(ctx, view, page) {
5648
+ const controller = createDraftTransfer(view, page);
5649
+ const previous = Object.getOwnPropertyDescriptor(view, "__dshMobileDraftTransfer");
5650
+ const mounted = /* @__PURE__ */ new Map();
5651
+ let active = true;
5652
+ let requestedSession;
5653
+ let importedToken;
5654
+ const api = {
5655
+ ...controller.api,
5656
+ restore(value) {
5657
+ if (!active || !snapshotShape(value)) return false;
5658
+ if (importedToken !== void 0) return controller.api.restore(value);
5659
+ if (mounted.size === 0 || mounted.size === 1 && ![...mounted.values()].includes(value.sessionId)) {
5660
+ if (ctx.get("sessions")?.list?.getSnapshot?.().byId[value.sessionId] === void 0) return false;
5661
+ if (requestedSession !== value.sessionId) {
5662
+ const navigation = ctx.get("uiWorkspace");
5663
+ if (typeof navigation?.openSession !== "function") return false;
5664
+ try {
5665
+ navigation.openSession(value.sessionId);
5666
+ } catch {
5667
+ return false;
5668
+ }
5669
+ requestedSession = value.sessionId;
5670
+ }
5671
+ return false;
5672
+ }
5673
+ const restored = controller.api.restore(value);
5674
+ if (restored) importedToken = value.token;
5675
+ return restored;
5676
+ },
5677
+ release(value) {
5678
+ controller.api.release(value);
5679
+ if (snapshotShape(value) && value.token === importedToken) importedToken = void 0;
5680
+ }
5681
+ };
5682
+ Object.defineProperty(view, "__dshMobileDraftTransfer", {
5683
+ configurable: true,
5684
+ value: api
5685
+ });
5686
+ function Observer({ sessionId }) {
5687
+ (0, react.useEffect)(() => {
5688
+ if (!active) return;
5689
+ const sessions = ctx.get("sessions");
5690
+ const conversation = ctx.get("conversation");
5691
+ if (typeof sessions?.scope !== "function" || typeof conversation?.input?.for !== "function") return;
5692
+ const scope = sessions.scope(sessionId);
5693
+ if (scope === void 0) return;
5694
+ const key = {};
5695
+ mounted.set(key, sessionId);
5696
+ const remove = controller.observe(sessionId, conversation.input.for(scope));
5697
+ return () => {
5698
+ mounted.delete(key);
5699
+ remove();
5700
+ };
5701
+ }, [sessionId]);
5702
+ return (0, react.createElement)("span", {
5703
+ hidden: true,
5704
+ "data-dsh-mobile-draft-observer": sessionId
5705
+ });
5706
+ }
5707
+ const removeSlot = ctx.slots.inject("conversation.input.overlay", () => ctx.slots.register({
5708
+ name: "conversation.input.overlay",
5709
+ id: "dsh-mobile-draft-transfer"
5710
+ }, Observer));
5711
+ return () => {
5712
+ if (!active) return;
5713
+ active = false;
5714
+ removeSlot();
5715
+ mounted.clear();
5716
+ controller.dispose();
5717
+ if (view.__dshMobileDraftTransfer === api) {
5718
+ if (previous === void 0) delete view.__dshMobileDraftTransfer;
5719
+ else Object.defineProperty(view, "__dshMobileDraftTransfer", previous);
5720
+ }
5721
+ };
5722
+ }
5187
5723
  //#endregion
5188
5724
  //#region src/mobile-font-capabilities.ts
5189
5725
  const NO_ADVANCED_MOBILE_TYPOGRAPHY = Object.freeze({
@@ -7199,6 +7735,161 @@ window.__ModuleLoader__.load({
7199
7735
  assetDepth: 8
7200
7736
  });
7201
7737
  //#endregion
7738
+ //#region src/client-recovery.ts
7739
+ /** Only a visible, disconnected public transport needs an immediate resume nudge. */
7740
+ function shouldNudgeReconnect(visibility, connection) {
7741
+ return visibility === "visible" && connection !== "connected";
7742
+ }
7743
+ /** A persistent, known disconnection can request one native renewal. */
7744
+ function shouldRenewSession(connection, downForMs) {
7745
+ return isDisconnected(connection) && downForMs >= 5e3;
7746
+ }
7747
+ function isDisconnected(connection) {
7748
+ return connection === "connecting" || connection === "disconnected";
7749
+ }
7750
+ /**
7751
+ * Own resume nudges and one native renewal per continuous disconnection.
7752
+ * @param getConnection - Resolve the current service to reject a replaced owner.
7753
+ * @param page - The document's window and advertised native adapter.
7754
+ * @param document - The mounted document; page replacement withdraws its work.
7755
+ * @returns An awaited disposer, or nothing when no public connection is present.
7756
+ */
7757
+ function installMobileConnectionRecovery(getConnection, page, document) {
7758
+ const connection = getConnection();
7759
+ const state = connection?.state;
7760
+ if (connection === void 0 || state === void 0 || typeof connection.reconnect !== "function") return;
7761
+ const origin = page.location.origin;
7762
+ let disposed = false;
7763
+ let generation = 0;
7764
+ let currentState = state.getSnapshot();
7765
+ let downSince = isDisconnected(currentState) ? Date.now() : void 0;
7766
+ let attempted = false;
7767
+ let bridge = page.__DSH_MOBILE_NATIVE__;
7768
+ let pending;
7769
+ let pendingAbort;
7770
+ let cleanup;
7771
+ const current = () => !disposed && page.document === document && page.location.origin === origin && getConnection() === connection && connection.state === state;
7772
+ const cancelPending = () => {
7773
+ generation += 1;
7774
+ pendingAbort?.abort(new DOMException("mobile recovery owner ended", "AbortError"));
7775
+ };
7776
+ const updateState = () => {
7777
+ const next = state.getSnapshot();
7778
+ if (!isDisconnected(next)) {
7779
+ downSince = void 0;
7780
+ attempted = false;
7781
+ if (currentState !== next) cancelPending();
7782
+ } else if (downSince === void 0) {
7783
+ downSince = Date.now();
7784
+ attempted = false;
7785
+ }
7786
+ currentState = next;
7787
+ };
7788
+ const updateBridge = () => {
7789
+ const next = page.__DSH_MOBILE_NATIVE__;
7790
+ if (bridge === next) return;
7791
+ cancelPending();
7792
+ bridge = next;
7793
+ attempted = false;
7794
+ };
7795
+ const renew = () => {
7796
+ if (!current()) {
7797
+ dispose();
7798
+ return;
7799
+ }
7800
+ updateState();
7801
+ updateBridge();
7802
+ const adapter = bridge;
7803
+ if (document.visibilityState !== "visible" || adapter === void 0 || pending !== void 0 || attempted || downSince === void 0 || !shouldRenewSession(currentState, Date.now() - downSince)) return;
7804
+ attempted = true;
7805
+ const owner = generation;
7806
+ const abort = new AbortController();
7807
+ pendingAbort = abort;
7808
+ const owns = () => current() && !abort.signal.aborted && generation === owner && page.__DSH_MOBILE_NATIVE__ === adapter && isDisconnected(state.getSnapshot());
7809
+ const task = Promise.resolve().then(async () => {
7810
+ if (!owns()) return;
7811
+ const capabilities = await adapter.capabilities();
7812
+ if (!owns() || !capabilities.includes("auth.renew")) return;
7813
+ const result = await adapter.invoke("auth.renew", {}, { signal: abort.signal });
7814
+ if (!owns() || typeof result !== "object" || result === null || !("renewed" in result) || result.renewed !== true) return;
7815
+ connection.reconnect?.();
7816
+ }).catch((_error) => {}).finally(() => {
7817
+ if (pending !== task) return;
7818
+ pending = void 0;
7819
+ pendingAbort = void 0;
7820
+ });
7821
+ pending = task;
7822
+ };
7823
+ const resume = () => {
7824
+ if (!current()) {
7825
+ dispose();
7826
+ return;
7827
+ }
7828
+ updateState();
7829
+ updateBridge();
7830
+ if (shouldNudgeReconnect(document.visibilityState, currentState)) connection.reconnect?.();
7831
+ renew();
7832
+ };
7833
+ const stopState = state.subscribe(updateState);
7834
+ const timer = setInterval(renew, 2e3);
7835
+ const pagehide = () => {
7836
+ dispose();
7837
+ };
7838
+ const dispose = () => {
7839
+ if (cleanup !== void 0) return cleanup;
7840
+ disposed = true;
7841
+ cancelPending();
7842
+ clearInterval(timer);
7843
+ stopState();
7844
+ document.removeEventListener("visibilitychange", resume);
7845
+ page.removeEventListener("dsh-mobile-native-ready", updateBridge);
7846
+ page.removeEventListener("pagehide", pagehide);
7847
+ cleanup = Promise.resolve(pending);
7848
+ return cleanup;
7849
+ };
7850
+ document.addEventListener("visibilitychange", resume);
7851
+ page.addEventListener("dsh-mobile-native-ready", updateBridge);
7852
+ page.addEventListener("pagehide", pagehide);
7853
+ return dispose;
7854
+ }
7855
+ //#endregion
7856
+ //#region src/browser-ui-locale.ts
7857
+ const UI_LOCALE_COOKIE = "dsh_ma_ui_locale";
7858
+ /** Host-only, non-secret preference, independent of the short authentication session. */
7859
+ function browserUiLocaleCookie(locale, secure) {
7860
+ return `${UI_LOCALE_COOKIE}=${locale}; Path=/mobile-access; Max-Age=31536000; SameSite=Lax${secure ? "; Secure" : ""}`;
7861
+ }
7862
+ //#endregion
7863
+ //#region src/mobile-ui-locale.ts
7864
+ /** Remember the DSH display language only on authenticated Mobile Gateway documents. */
7865
+ function installMobileUiLocalePersistence(page, view) {
7866
+ if (page.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") === null) return () => {};
7867
+ let disposed = false;
7868
+ let previous;
7869
+ const sync = () => {
7870
+ if (disposed) return;
7871
+ const language = page.documentElement.lang.trim().toLowerCase();
7872
+ if (language === "") return;
7873
+ const base = language.split(/[-_]/u)[0];
7874
+ const locale = base === "zh" || base === "it" ? base : "en";
7875
+ if (locale === previous) return;
7876
+ try {
7877
+ page.cookie = browserUiLocaleCookie(locale, view.location.protocol === "https:");
7878
+ previous = locale;
7879
+ } catch {}
7880
+ };
7881
+ const observer = new view.MutationObserver(sync);
7882
+ observer.observe(page.documentElement, {
7883
+ attributes: true,
7884
+ attributeFilter: ["lang"]
7885
+ });
7886
+ sync();
7887
+ return () => {
7888
+ disposed = true;
7889
+ observer.disconnect();
7890
+ };
7891
+ }
7892
+ //#endregion
7202
7893
  //#region src/client.ts
7203
7894
  /** Map a host-reported provider onto the UI union, defaulting to the built-in one. */
7204
7895
  function parseMobileRemoteProvider(value) {
@@ -7747,6 +8438,10 @@ window.__ModuleLoader__.load({
7747
8438
  frp_discovery_invalid: "frpDiscoveryInvalid",
7748
8439
  frp_stopped: "frpStopped",
7749
8440
  frp_exited: "frpExited",
8441
+ frp_server_address_invalid: "frpServerAddressInvalid",
8442
+ frp_server_port_invalid: "frpServerPortInvalid",
8443
+ frp_token_invalid: "frpTokenInvalid",
8444
+ frp_public_origin_invalid: "frpPublicOriginInvalid",
7750
8445
  frp_attach_mode_requires_vhost_port: "frpAttachModeRequiresVhostPort",
7751
8446
  frp_attach_cert_unknown: "frpAttachCertUnknownError",
7752
8447
  frp_entry_tls_invalid: "frpEntryTlsInvalid",
@@ -8480,6 +9175,52 @@ window.__ModuleLoader__.load({
8480
9175
  frpPortLabel.append(frpPort);
8481
9176
  frpTokenLabel.append(frpToken);
8482
9177
  frpOriginLabel.append(frpOrigin);
9178
+ const frpInputs = {
9179
+ serverAddress: frpServer,
9180
+ serverPort: frpPort,
9181
+ token: frpToken,
9182
+ publicOrigin: frpOrigin,
9183
+ mode: frpMode,
9184
+ entryTls: frpEntryTls,
9185
+ vhostHttpPort: frpVhostPort,
9186
+ publicPort: frpPublicPort
9187
+ };
9188
+ const frpInputLabels = {
9189
+ serverAddress: frpServerLabel,
9190
+ serverPort: frpPortLabel,
9191
+ token: frpTokenLabel,
9192
+ publicOrigin: frpOriginLabel,
9193
+ mode: frpModeLabel,
9194
+ entryTls: frpEntryTlsLabel,
9195
+ vhostHttpPort: frpVhostLabel,
9196
+ publicPort: frpPublicPortLabel
9197
+ };
9198
+ const frpLabelKeys = {
9199
+ serverAddress: "frpServerAddress",
9200
+ serverPort: "frpServerPort",
9201
+ token: "frpToken",
9202
+ publicOrigin: "frpPublicOrigin",
9203
+ mode: "frpMode",
9204
+ entryTls: "frpEntryTls",
9205
+ vhostHttpPort: "frpVhostHttpPort",
9206
+ publicPort: "frpPublicPort"
9207
+ };
9208
+ const frpErrors = /* @__PURE__ */ new Map();
9209
+ for (const [name, input] of Object.entries(frpInputs)) {
9210
+ input.id = `dsh-mobile-frp-${name}`;
9211
+ input.setAttribute("aria-label", t(frpLabelKeys[name]));
9212
+ const error = element("p", "dsh-mobile-control__origin-feedback is-error");
9213
+ error.id = `${input.id}-error`;
9214
+ error.hidden = true;
9215
+ input.setAttribute("aria-describedby", [input.getAttribute("aria-describedby"), error.id].filter(Boolean).join(" "));
9216
+ frpInputLabels[name].append(error);
9217
+ frpErrors.set(name, error);
9218
+ }
9219
+ const frpOriginHint = element("p", "dsh-mobile-control__frp-hint");
9220
+ frpOriginHint.id = "dsh-mobile-frp-origin-hint";
9221
+ frpOriginHint.textContent = t("frpPublicOriginHint");
9222
+ frpOriginLabel.append(frpOriginHint);
9223
+ frpOrigin.setAttribute("aria-describedby", `${frpOriginHint.id} ${frpOrigin.getAttribute("aria-describedby") ?? ""}`);
8483
9224
  frpFields.append(frpServerLabel, frpPortLabel, frpTokenLabel, frpOriginLabel, frpModeLabel, frpEntryTlsLabel, frpVhostLabel, frpPublicPortLabel);
8484
9225
  frpStep1.append(frpStep1Title, frpStep1Text, frpFields);
8485
9226
  const frpStep2 = element("section", "dsh-mobile-control__frp-step");
@@ -8581,10 +9322,18 @@ window.__ModuleLoader__.load({
8581
9322
  frpAppRequirement.textContent = t("frpAppRequirement");
8582
9323
  const frpConfigurationStatus = element("p", "dsh-mobile-control__component-status");
8583
9324
  frpConfigurationStatus.textContent = t("frpConfigurationMissing");
9325
+ const frpFeedback = element("p", "dsh-mobile-control__origin-feedback is-error");
9326
+ frpFeedback.setAttribute("role", "status");
9327
+ frpFeedback.setAttribute("aria-live", "polite");
9328
+ frpFeedback.hidden = true;
9329
+ const frpActionHint = element("p", "dsh-mobile-control__frp-hint");
9330
+ frpActionHint.id = "dsh-mobile-frp-action-hint";
9331
+ frpActionHint.hidden = true;
8584
9332
  const frpConfigure = element("button", "dsh-mobile-control__primary dsh-mobile-control__frp-action");
8585
9333
  frpConfigure.type = "button";
8586
9334
  frpConfigure.textContent = t("frpSaveConnect");
8587
- frpStep4.append(frpStep4Title, frpStep4Text, frpAppRequirement, frpConfigurationStatus, frpConfigure);
9335
+ frpConfigure.setAttribute("aria-describedby", frpActionHint.id);
9336
+ frpStep4.append(frpStep4Title, frpStep4Text, frpAppRequirement, frpConfigurationStatus, frpFeedback, frpActionHint, frpConfigure);
8588
9337
  const frpDetails = element("details", "dsh-mobile-control__details");
8589
9338
  const frpDetailsSummary = element("summary");
8590
9339
  frpDetailsSummary.textContent = t("frpComponentDetails");
@@ -9660,6 +10409,8 @@ window.__ModuleLoader__.load({
9660
10409
  frpInstall.textContent = frpDownloadBytes > 0 ? t("installWithSize", { size: formatMegabytes(frpDownloadBytes) }) : t("installFrpc");
9661
10410
  frpInstall.disabled = remoteProviderBusy;
9662
10411
  frpConfigure.disabled = remoteProviderBusy || !frpInstalled;
10412
+ frpActionHint.hidden = !frpConfigure.disabled;
10413
+ frpActionHint.textContent = t(remoteProviderBusy ? "frpActionBusy" : !frpSupported ? "frpUnsupported" : "frpInstallBeforeConnect");
9663
10414
  vpsDeploy.disabled = remoteProviderBusy || !validVpsDeploymentForm();
9664
10415
  vpsCopyUninstall.disabled = vpsDeploy.disabled;
9665
10416
  vpsUninstall.disabled = vpsDeploy.disabled;
@@ -10097,7 +10848,6 @@ window.__ModuleLoader__.load({
10097
10848
  loadRemote();
10098
10849
  });
10099
10850
  });
10100
- const validFrpServer = (value) => value === value.trim() && value.length > 0 && value.length <= 253 && !/[\s\u0000-\u001f\u007f/\\@?#]/u.test(value);
10101
10851
  const frpForm = () => {
10102
10852
  const vhostHttpPort = Number(frpVhostPort.value);
10103
10853
  const publicPort = Number(frpPublicPort.value);
@@ -10109,8 +10859,8 @@ window.__ModuleLoader__.load({
10109
10859
  publicOrigin: String(frpOrigin.value ?? "").trim(),
10110
10860
  mode,
10111
10861
  entryTls: frpEntryTls.value === "self-signed" ? "self-signed" : "public-ip-cert",
10112
- ...Number.isSafeInteger(vhostHttpPort) && vhostHttpPort >= 1 ? { vhostHttpPort } : {},
10113
- ...Number.isSafeInteger(publicPort) && publicPort >= 1 ? { publicPort } : {}
10862
+ ...frpEntryTls.value === "self-signed" || frpVhostPort.value.trim() === "" ? {} : { vhostHttpPort },
10863
+ ...frpEntryTls.value === "self-signed" ? { publicPort } : {}
10114
10864
  };
10115
10865
  };
10116
10866
  /**
@@ -10127,20 +10877,39 @@ window.__ModuleLoader__.load({
10127
10877
  ...form.publicPort === void 0 ? {} : { publicPort: form.publicPort }
10128
10878
  });
10129
10879
  const validFrpForm = (form) => {
10130
- if (!validFrpServer(form.serverAddress)) return false;
10131
- try {
10132
- createFrpServerTemplateForClipboard(form.serverPort, form.token, form.publicOrigin);
10133
- return true;
10134
- } catch {
10135
- return false;
10136
- }
10137
- };
10880
+ return validateFrpForm(form).length === 0;
10881
+ };
10882
+ let frpValidationShown = false;
10883
+ let frpValidationAllowsSavedToken = false;
10884
+ const renderFrpIssues = (issues, focus = false) => {
10885
+ for (const [name, input] of Object.entries(frpInputs)) {
10886
+ const issue = issues.find((item) => item.field === name);
10887
+ const feedback = frpErrors.get(name);
10888
+ if (issue === void 0) input.removeAttribute("aria-invalid");
10889
+ else input.setAttribute("aria-invalid", "true");
10890
+ feedback.hidden = issue === void 0;
10891
+ feedback.textContent = issue === void 0 ? "" : t(issue.messageKey);
10892
+ }
10893
+ frpFeedback.hidden = issues.length === 0;
10894
+ frpFeedback.textContent = issues.map((issue) => t(issue.messageKey)).join(" ");
10895
+ if (issues.length !== 0 && focus) {
10896
+ frpConnectionGroup.open = true;
10897
+ frpInputs[issues[0].field].focus();
10898
+ }
10899
+ };
10900
+ const checkFrpForm = (form, allowSavedToken = false) => {
10901
+ const issues = validateFrpForm(form, allowSavedToken);
10902
+ frpValidationShown = true;
10903
+ frpValidationAllowsSavedToken = allowSavedToken;
10904
+ renderFrpIssues(issues, true);
10905
+ return issues.length === 0;
10906
+ };
10907
+ for (const [name, input] of Object.entries(frpInputs)) input.addEventListener(name === "mode" || name === "entryTls" ? "change" : "input", () => {
10908
+ if (frpValidationShown) renderFrpIssues(validateFrpForm(frpForm(), frpValidationAllowsSavedToken));
10909
+ });
10138
10910
  frpCopyTemplate.addEventListener("click", () => {
10139
10911
  const form = frpForm();
10140
- if (!validFrpForm(form)) {
10141
- remoteStatus.textContent = t("frpInputInvalid");
10142
- return;
10143
- }
10912
+ if (!checkFrpForm(form)) return;
10144
10913
  navigator.clipboard.writeText(createFrpServerTemplateForClipboard(form.serverPort, form.token, form.publicOrigin)).then(() => {
10145
10914
  remoteStatus.textContent = t("templateCopied");
10146
10915
  }, () => {
@@ -10150,10 +10919,13 @@ window.__ModuleLoader__.load({
10150
10919
  const frpAttachErrorText = (code) => t(REMOTE_ERROR_MESSAGE_KEYS[code] ?? "frpInputInvalid");
10151
10920
  const showFrpAttachError = (code) => {
10152
10921
  const message = frpAttachErrorText(code);
10153
- if (code === "frp_attach_mode_requires_vhost_port") {
10154
- frpVhostPort.setAttribute("aria-invalid", "true");
10155
- frpVhostHint.textContent = message;
10156
- frpVhostPort.focus();
10922
+ const issue = FRP_FORM_ERRORS[code];
10923
+ if (issue !== void 0) {
10924
+ frpValidationShown = true;
10925
+ renderFrpIssues([issue], true);
10926
+ } else {
10927
+ frpFeedback.textContent = message;
10928
+ frpFeedback.hidden = false;
10157
10929
  }
10158
10930
  remoteStatus.textContent = message;
10159
10931
  };
@@ -10166,10 +10938,7 @@ window.__ModuleLoader__.load({
10166
10938
  };
10167
10939
  frpCopyAttachPlan.addEventListener("click", () => {
10168
10940
  const form = frpForm();
10169
- if (!validFrpServer(form.serverAddress) || !Number.isSafeInteger(form.serverPort) || form.serverPort < 1 || form.serverPort > 65535 || form.publicOrigin === "") {
10170
- remoteStatus.textContent = t("frpInputInvalid");
10171
- return;
10172
- }
10941
+ if (!checkFrpForm(form, frpConfigured)) return;
10173
10942
  if (form.token !== "") {
10174
10943
  const attachForm = attachClipboardForm(form);
10175
10944
  const code = frpAttachFormErrorCode(attachForm);
@@ -10202,7 +10971,7 @@ window.__ModuleLoader__.load({
10202
10971
  remoteStatus.textContent = t("frpAttachPlanCopied");
10203
10972
  });
10204
10973
  }, (error) => {
10205
- remoteStatus.textContent = frpAttachErrorText(String(error));
10974
+ showFrpAttachError(error instanceof Error ? error.message : String(error));
10206
10975
  }).catch((error) => {
10207
10976
  remoteStatus.textContent = t("frpAttachPlanFailed", { error: String(error) });
10208
10977
  }).finally(() => {
@@ -10213,10 +10982,7 @@ window.__ModuleLoader__.load({
10213
10982
  /** Copy a token-bearing frpc.toml only from the token the user just typed. */
10214
10983
  frpCopyAttachToken.addEventListener("click", () => {
10215
10984
  const form = frpForm();
10216
- if (!validFrpServer(form.serverAddress) || !Number.isSafeInteger(form.serverPort) || form.serverPort < 1 || form.serverPort > 65535 || form.publicOrigin === "") {
10217
- remoteStatus.textContent = t("frpInputInvalid");
10218
- return;
10219
- }
10985
+ if (!checkFrpForm(form, frpConfigured)) return;
10220
10986
  if (form.token !== "") {
10221
10987
  const attachForm = attachClipboardForm(form);
10222
10988
  const code = frpAttachFormErrorCode(attachForm);
@@ -10563,15 +11329,13 @@ window.__ModuleLoader__.load({
10563
11329
  const form = frpForm();
10564
11330
  const unchanged = frpConfigured && form.token === "" && form.serverAddress === configuredFrpServer && form.serverPort === configuredFrpPort && form.publicOrigin === configuredFrpOrigin && form.mode === (frpConfiguredMode === "attach" ? "attach" : "deploy") && form.entryTls === (frpConfiguredEntryTls === "self-signed" ? "self-signed" : "public-ip-cert") && (form.vhostHttpPort ?? configuredFrpVhostPort) === configuredFrpVhostPort && (form.publicPort ?? configuredFrpPublicPort) === configuredFrpPublicPort;
10565
11331
  if (!unchanged) {
10566
- const code = form.mode === "attach" ? frpAttachFormErrorCode(attachClipboardForm(form)) : validFrpForm(form) ? void 0 : "frp_settings_invalid";
10567
- if (code !== void 0) {
10568
- if (form.mode === "attach") showFrpAttachError(code);
10569
- else remoteStatus.textContent = t("frpInputInvalid");
10570
- return;
10571
- }
11332
+ if (!checkFrpForm(form)) return;
10572
11333
  }
11334
+ renderFrpIssues([]);
10573
11335
  remoteProviderBusy = true;
10574
11336
  frpConfigure.disabled = true;
11337
+ frpActionHint.hidden = false;
11338
+ frpActionHint.textContent = t("frpActionBusy");
10575
11339
  frpConfigure.setAttribute("aria-busy", "true");
10576
11340
  frpConfigure.textContent = t("saving");
10577
11341
  (unchanged ? Promise.resolve({}) : controlRequestJson("/api/mobile-access/remote/frp/configure", {
@@ -10585,8 +11349,12 @@ window.__ModuleLoader__.load({
10585
11349
  body: JSON.stringify({ running: true })
10586
11350
  });
10587
11351
  }).then(renderRemote, (error) => {
10588
- remoteFailureText = t("configureFailed", { error: String(error) });
10589
- remoteStatus.textContent = remoteFailureText;
11352
+ const code = error instanceof Error ? error.message : String(error);
11353
+ if (FRP_FORM_ERRORS[code] !== void 0) showFrpAttachError(code);
11354
+ else {
11355
+ remoteFailureText = t("configureFailed", { error: String(error) });
11356
+ remoteStatus.textContent = remoteFailureText;
11357
+ }
10590
11358
  }).finally(() => {
10591
11359
  remoteProviderBusy = false;
10592
11360
  frpConfigure.setAttribute("aria-busy", "false");
@@ -12460,6 +13228,8 @@ window.__ModuleLoader__.load({
12460
13228
  .dsh-mobile-control__switcher{display:grid;grid-template-columns:repeat(2,1fr);gap:4px;margin:0 0 14px;padding:4px;border-radius:12px;background:var(--dsw-alias-bg-layer-1,#f3f5f8)}.dsh-mobile-control__switcher[hidden]{display:none}.dsh-mobile-control__tab{min-height:36px;border:0;border-radius:9px;background:transparent;color:var(--dsw-alias-label-secondary,#606873);font:600 13px/1 system-ui;cursor:pointer}.dsh-mobile-control__tab.is-active{background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);box-shadow:0 1px 3px rgb(15 23 42 / 10%)}.dsh-mobile-control__view[hidden]{display:none}.dsh-mobile-control__intro{margin:0 0 12px;color:var(--dsw-alias-label-secondary,#606873);font-size:12px;line-height:1.55}.dsh-mobile-control__http-frame-warning{box-sizing:border-box;margin:-4px 0 12px;padding:8px 10px;border:1px solid #d6a83f;border-radius:9px;background:var(--dsw-alias-bg-layer-1,#fff7df);color:var(--dsw-alias-label-primary,#694b00);font-size:11px;line-height:1.5;overflow-wrap:anywhere}.dsh-mobile-control__http-frame-warning[hidden]{display:none}.dsh-mobile-control__view.is-remote .dsh-mobile-control__actions{display:grid;grid-template-columns:1fr 1fr;gap:8px}.dsh-mobile-control__view.is-remote .dsh-mobile-control__actions button[hidden]{display:none}
12461
13229
  .dsh-mobile-control__provider-section{position:relative;margin:0 0 14px}.dsh-mobile-control__section-title{margin:0 0 8px;color:var(--dsw-alias-label-primary,#16181d);font:650 13px/1.4 system-ui}.dsh-mobile-control__provider-section>.dsh-mobile-control__section-title{padding-right:42px}.dsh-mobile-control__provider-choices{display:grid;grid-template-columns:1fr;gap:8px}.dsh-mobile-control__provider{display:flex;min-width:0;flex-direction:column;gap:6px;min-height:94px;padding:10px 11px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);text-align:left;cursor:pointer;touch-action:manipulation;transition:border-color 160ms ease,background-color 160ms ease,box-shadow 160ms ease}.dsh-mobile-control__provider:hover{border-color:#6f96db;background:var(--dsw-alias-interactive-bg-hover-solid,var(--dsw-alias-bg-layer-1,#f8fbff))}.dsh-mobile-control__provider.is-selected{border-color:var(--dsw-alias-label-primary-bluish,#2563eb);background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-bg-layer-1,#f5f8ff));box-shadow:0 0 0 1px var(--dsw-alias-label-primary-bluish,#2563eb) inset}.dsh-mobile-control__provider:disabled{cursor:wait;opacity:.62}.dsh-mobile-control__provider-top{display:flex;min-width:0;align-items:flex-start;justify-content:space-between;gap:5px}.dsh-mobile-control__provider-top strong{min-width:0;font-size:12px;line-height:1.3}.dsh-mobile-control__provider-badge{flex:none;padding:2px 5px;border-radius:999px;background:var(--dsw-alias-state-business-tertiary,#e8f0ff);color:var(--dsw-alias-label-primary,#1d4ed8);font:650 11px/1.25 system-ui}.dsh-mobile-control__provider-badge.is-cpolar{background:var(--dsw-alias-state-success-tertiary,#eaf8f2);color:var(--dsw-alias-label-primary,#087454)}.dsh-mobile-control__provider-description{color:var(--dsw-alias-label-secondary,#606873);font-size:12px;line-height:1.45}.dsh-mobile-control__provider-info{position:absolute;z-index:5;top:-13px;right:-8px}.dsh-mobile-control__provider-info-button{display:flex;align-items:center;justify-content:center;width:44px;height:44px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--dsw-alias-label-secondary,#475569);cursor:pointer;touch-action:manipulation}.dsh-mobile-control__provider-info-button:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f5f9);color:var(--dsw-alias-label-primary-bluish,#2563eb)}.dsh-mobile-control__provider-info-glyph{display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:18px;height:18px;border:1.5px solid currentColor;border-radius:50%;font:700 12px/1 system-ui}.dsh-mobile-control__provider-info-popover{position:absolute;z-index:6;top:38px;right:4px;box-sizing:border-box;width:min(292px,calc(100vw - 72px));padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#fff);box-shadow:0 10px 28px rgb(15 23 42 / 16%)}.dsh-mobile-control__provider-info-popover[hidden]{display:none}.dsh-mobile-control__provider-info-popover strong,.dsh-mobile-control__provider-info-popover span{display:block}.dsh-mobile-control__provider-info-popover strong{margin-bottom:3px;font-size:12px}.dsh-mobile-control__provider-info-popover span{color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.55}
12462
13230
  .dsh-mobile-control__self-hosted{margin:8px 0 0;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:12px;background:var(--dsw-alias-bg-layer-1,#f8fafc)}.dsh-mobile-control__self-hosted-summary{display:flex;box-sizing:border-box;min-height:48px;align-items:center;justify-content:space-between;gap:10px;padding:8px 11px;cursor:pointer;list-style-position:inside}.dsh-mobile-control__self-hosted-summary>span:first-child{display:flex;min-width:0;flex-direction:column;gap:1px}.dsh-mobile-control__self-hosted-summary strong{font-size:11px}.dsh-mobile-control__self-hosted-summary span span{color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.35}.dsh-mobile-control__provider-badge.is-frp{background:var(--dsw-alias-bg-layer-1,#eef0f3);color:var(--dsw-alias-label-secondary,#475569)}.dsh-mobile-control__self-hosted-body{padding:0 8px 8px}.dsh-mobile-control__provider.is-frp{width:100%;min-height:64px;background:var(--dsw-alias-bg-layer-2,#fff)}
13231
+ .dsh-mobile-control__frp-setup input[aria-invalid=true],.dsh-mobile-control__frp-setup select[aria-invalid=true]{border-color:var(--dsw-alias-state-error-primary,#bc3030)}
13232
+ .dsh-mobile-control__frp-setup .dsh-mobile-control__field>.dsh-mobile-control__origin-feedback{margin:0}
12463
13233
  /* The cloudflared setup section deliberately reuses this card, so
12464
13234
  .dsh-mobile-control__cloudflared-setup is a DOM hook for targeting rather than a
12465
13235
  style rule of its own. */
@@ -12495,10 +13265,16 @@ window.__ModuleLoader__.load({
12495
13265
  ` + EXTENSION_RECOVERY_STYLES;
12496
13266
  /** Mount the desktop control or mobile feature enhancements. */
12497
13267
  function apply(ctx) {
13268
+ ctx.effect(() => installMobileUiLocalePersistence(document, window), "dsh-mobile: browser UI locale persistence");
12498
13269
  ctx.effect(() => {
12499
13270
  if (window.__DSH_MOBILE_FRONTEND__ !== "dedicated") return;
12500
- return trustAuthenticatedGatewayConnection(ctx.get("connection"));
13271
+ const connection = ctx.get("connection");
13272
+ if (connection !== void 0) return trustAuthenticatedGatewayConnection(connection);
12501
13273
  }, "dsh-mobile: authenticated gateway client trust");
13274
+ ctx.effect(() => {
13275
+ if (!ownsMobileCustomization(document.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") !== null, window.__DSH_MOBILE_FRONTEND__)) return;
13276
+ return installMobileConnectionRecovery(() => ctx.get("connection"), window, document);
13277
+ }, "dsh-mobile: connection recovery");
12502
13278
  ctx.effect(() => {
12503
13279
  const desktopAdmin = isDesktopAdminSurface(location.hostname, location.search, window.__DSH_MOBILE_FRONTEND__, location.protocol);
12504
13280
  const gatewayCustomization = ownsMobileCustomization(document.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") !== null, window.__DSH_MOBILE_FRONTEND__);
@@ -12509,6 +13285,7 @@ window.__ModuleLoader__.load({
12509
13285
  document.head.append(style);
12510
13286
  const removeCustom = !desktopAdmin || gatewayCustomization ? installCustomAssets() : () => void 0;
12511
13287
  const removeVoiceSession = installVoiceSession(!desktopAdmin);
13288
+ const removeDraftTransfer = !desktopAdmin && document.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") !== null ? installDraftTransfer(ctx, window, document) : () => void 0;
12512
13289
  const removeModuleSettings = ctx.slots.inject("settings.general.item", () => ctx.slots.register({
12513
13290
  name: "settings.general.item",
12514
13291
  id: "dsh-mobile-client-modules",
@@ -12562,6 +13339,7 @@ window.__ModuleLoader__.load({
12562
13339
  removeCustom();
12563
13340
  removeSurface();
12564
13341
  removeVoiceSession();
13342
+ removeDraftTransfer();
12565
13343
  localFont.dispose();
12566
13344
  style.remove();
12567
13345
  };
@@ -12612,6 +13390,7 @@ window.__ModuleLoader__.load({
12612
13390
  removeControl();
12613
13391
  removeCustom();
12614
13392
  removeVoiceSession();
13393
+ removeDraftTransfer();
12615
13394
  style.remove();
12616
13395
  };
12617
13396
  }, "dsh-mobile: stock mobile adaptation and local control");
@@ -12660,6 +13439,8 @@ window.__ModuleLoader__.load({
12660
13439
  exports.selectMobileControlLocale = selectMobileControlLocale;
12661
13440
  exports.selectedMobileControlLocale = selectedMobileControlLocale;
12662
13441
  exports.setMobileSurfaceOpen = setMobileSurfaceOpen;
13442
+ exports.shouldNudgeReconnect = shouldNudgeReconnect;
13443
+ exports.shouldRenewSession = shouldRenewSession;
12663
13444
  exports.startExtensionChangeStream = startExtensionChangeStream;
12664
13445
  exports.startLifecycleRefreshScheduler = startLifecycleRefreshScheduler;
12665
13446
  exports.trustAuthenticatedGatewayConnection = trustAuthenticatedGatewayConnection;