@linxin666/dsh-remote-web-ui 0.1.20 → 0.2.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 (91) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +41 -19
  3. package/README.zh.md +17 -12
  4. package/assets/mobile-icon-192.png +0 -0
  5. package/assets/mobile-icon-512.png +0 -0
  6. package/assets/mobile-service-worker.js +64 -0
  7. package/lib/client.js +37 -10
  8. package/lib/client.js.map +1 -1
  9. package/lib/index.js +290 -53
  10. package/lib/mobile.js +278 -17
  11. package/lib/mobile.js.map +1 -1
  12. package/lib/types/client/PluginSettingsCard.d.ts +6 -0
  13. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  14. package/lib/types/client/PluginSettingsCard.js +5 -3
  15. package/lib/types/client/RemoteEntry.js +2 -2
  16. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  17. package/lib/types/client/RemoteSettingsCard.js +1 -1
  18. package/lib/types/client/UpdateEntry.d.ts.map +1 -1
  19. package/lib/types/client/UpdateEntry.js +36 -3
  20. package/lib/types/client/deep-link.js +1 -1
  21. package/lib/types/client/locales.d.ts +2 -0
  22. package/lib/types/client/locales.d.ts.map +1 -1
  23. package/lib/types/client/locales.js +2 -0
  24. package/lib/types/gate.d.ts +9 -0
  25. package/lib/types/gate.d.ts.map +1 -1
  26. package/lib/types/gate.js +15 -4
  27. package/lib/types/index.d.ts +25 -0
  28. package/lib/types/index.d.ts.map +1 -1
  29. package/lib/types/index.js +30 -13
  30. package/lib/types/mobile/PairRequiredView.d.ts +7 -0
  31. package/lib/types/mobile/PairRequiredView.d.ts.map +1 -0
  32. package/lib/types/mobile/PairRequiredView.js +28 -0
  33. package/lib/types/mobile/PairRequiredView.test.d.ts +2 -0
  34. package/lib/types/mobile/PairRequiredView.test.d.ts.map +1 -0
  35. package/lib/types/mobile/PairRequiredView.test.js +24 -0
  36. package/lib/types/mobile/index.js +14 -1
  37. package/lib/types/mobile/mobile-styles.d.ts +1 -1
  38. package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
  39. package/lib/types/mobile/mobile-styles.js +48 -0
  40. package/lib/types/mobile/pairing.d.ts +31 -0
  41. package/lib/types/mobile/pairing.d.ts.map +1 -0
  42. package/lib/types/mobile/pairing.js +62 -0
  43. package/lib/types/mobile/views/App.d.ts +12 -5
  44. package/lib/types/mobile/views/App.d.ts.map +1 -1
  45. package/lib/types/mobile/views/App.js +34 -7
  46. package/lib/types/mobile/views/App.test.d.ts +2 -0
  47. package/lib/types/mobile/views/App.test.d.ts.map +1 -0
  48. package/lib/types/mobile/views/App.test.js +52 -0
  49. package/lib/types/mobile-pwa.d.ts +6 -0
  50. package/lib/types/mobile-pwa.d.ts.map +1 -0
  51. package/lib/types/mobile-pwa.js +17 -0
  52. package/lib/types/mobile-routes.d.ts +5 -8
  53. package/lib/types/mobile-routes.d.ts.map +1 -1
  54. package/lib/types/mobile-routes.js +115 -30
  55. package/lib/types/pairing-access.d.ts +28 -0
  56. package/lib/types/pairing-access.d.ts.map +1 -0
  57. package/lib/types/pairing-access.js +26 -0
  58. package/lib/types/pairing.d.ts +27 -0
  59. package/lib/types/pairing.d.ts.map +1 -1
  60. package/lib/types/pairing.js +68 -0
  61. package/lib/types/pairing.persist.test.d.ts +2 -0
  62. package/lib/types/pairing.persist.test.d.ts.map +1 -0
  63. package/lib/types/pairing.persist.test.js +128 -0
  64. package/lib/types/routes.js +1 -1
  65. package/lib/types/update.d.ts +3 -3
  66. package/lib/types/update.d.ts.map +1 -1
  67. package/lib/types/update.js +34 -11
  68. package/package.json +12 -12
  69. package/src/client/PluginSettingsCard.tsx +10 -0
  70. package/src/client/RemoteEntry.tsx +2 -2
  71. package/src/client/RemoteSettingsCard.tsx +1 -0
  72. package/src/client/UpdateEntry.tsx +36 -4
  73. package/src/client/deep-link.ts +1 -1
  74. package/src/client/locales.ts +2 -0
  75. package/src/client/remote.module.css +26 -0
  76. package/src/gate.ts +15 -3
  77. package/src/index.ts +46 -15
  78. package/src/mobile/PairRequiredView.test.tsx +28 -0
  79. package/src/mobile/PairRequiredView.tsx +58 -0
  80. package/src/mobile/index.tsx +15 -1
  81. package/src/mobile/mobile-styles.ts +48 -0
  82. package/src/mobile/pairing.ts +77 -0
  83. package/src/mobile/views/App.test.tsx +62 -0
  84. package/src/mobile/views/App.tsx +53 -6
  85. package/src/mobile-pwa.ts +19 -0
  86. package/src/mobile-routes.ts +123 -30
  87. package/src/pairing-access.ts +36 -0
  88. package/src/pairing.persist.test.ts +145 -0
  89. package/src/pairing.ts +70 -1
  90. package/src/routes.ts +1 -1
  91. package/src/update.ts +40 -12
@@ -39,6 +39,12 @@ export interface PluginSettingsCardProps<TKey extends string = string> {
39
39
  * already provides the selection.
40
40
  */
41
41
  alwaysOpen?: boolean;
42
+ /**
43
+ * Hide the save/discard footer: cards whose body applies its own changes
44
+ * immediately (an embedded external settings section) have no staged
45
+ * edits, so the footer would sit there permanently disabled.
46
+ */
47
+ hideFooter?: boolean;
42
48
  /** The plugin's controls. */
43
49
  children: ReactNode;
44
50
  }
@@ -1 +1 @@
1
- {"version":3,"file":"PluginSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/PluginSettingsCard.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"PluginSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/PluginSettingsCard.tsx"],"names":[],"mappings":"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"}
@@ -52,9 +52,11 @@ export function PluginSettingsCard(props) {
52
52
  : null] }));
53
53
  }
54
54
  return (_jsxs("li", { className: cardClass, children: [header, expanded
55
- ? (_jsxs("div", { className: css.body, children: [!state.writable ? _jsx("p", { className: css.readOnly, role: "status", children: props.t('settings.readOnly') }) : null, props.children, _jsxs("div", { className: css.footer, children: [state.failed
56
- ? (_jsxs("p", { className: css.failed, role: "status", children: [props.t('settings.saveFailed'), state.failedReason ? ' - ' + state.failedReason : ''] }))
57
- : null, _jsx("button", { type: "button", className: css.discard, disabled: !state.dirty || state.saving, onClick: props.onDiscard, children: props.t('settings.discard') }), _jsx("button", { type: "button", className: css.save, disabled: blocked, onClick: props.onSave, children: props.t(!state.saving ? 'settings.save' : 'settings.saving') })] })] }))
55
+ ? (_jsxs("div", { className: css.body, children: [!state.writable ? _jsx("p", { className: css.readOnly, role: "status", children: props.t('settings.readOnly') }) : null, props.children, props.hideFooter === true
56
+ ? null
57
+ : (_jsxs("div", { className: css.footer, children: [state.failed
58
+ ? (_jsxs("p", { className: css.failed, role: "status", children: [props.t('settings.saveFailed'), state.failedReason ? ' - ' + state.failedReason : ''] }))
59
+ : null, _jsx("button", { type: "button", className: css.discard, disabled: !state.dirty || state.saving, onClick: props.onDiscard, children: props.t('settings.discard') }), _jsx("button", { type: "button", className: css.save, disabled: blocked, onClick: props.onSave, children: props.t(!state.saving ? 'settings.save' : 'settings.saving') })] }))] }))
58
60
  : null] }));
59
61
  }
60
62
  /** A staged value field. `numeric` only hints the keypad: which drafts a field accepts is decided by its spec. */
@@ -188,7 +188,7 @@ export function RemoteEntry({ wide, useWorkspaces, t }) {
188
188
  }, [state]);
189
189
  return (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.entryRow, "data-rail": wide ? undefined : 'rail', children: [_jsx(UpdateEntry, { wide: wide, t: t }), _jsx(TooltipAnchor, { wide: wide, label: t('entry.label'), onClick: openPanel })] }), open && createPortal((_jsxs("div", { className: css.overlay, role: "presentation", children: [_jsx("div", { className: css.mask, "aria-hidden": "true", onClick: closePanel }), _jsx(RemotePanel, { t: t, state: state, copied: copied, onClose: closePanel, onStop: handleStop, onRefresh: handleRefresh, onCopy: handleCopy, onPickAddress: handlePickAddress, onPickPublic: handlePickPublic })] })), document.body)] }));
190
190
  }
191
- /** The trigger: an icon button matching the settings rail/row geometry. */
191
+ /** The trigger: an icon-only control with a persistent accessible label. */
192
192
  function TooltipAnchor({ wide, label, onClick }) {
193
- return (_jsx("button", { type: "button", className: css.trigger, "data-wide": wide ? undefined : 'rail', "aria-label": label, title: label, onClick: onClick, children: _jsx(PhoneIcon, { size: wide ? 16 : 18 }) }));
193
+ return (_jsx("button", { type: "button", className: css.trigger, "data-wide": wide ? 'wide' : 'rail', "aria-label": label, title: label, onClick: onClick, children: _jsx(PhoneIcon, { size: wide ? 16 : 18 }) }));
194
194
  }
@@ -1 +1 @@
1
- {"version":3,"file":"RemoteSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/RemoteSettingsCard.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAC7F,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAE1F,OAAO,EAAkD,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,IAAI,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExJ,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,6FAA6F;IAC7F,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,4CAA4C;AAC5C,MAAM,WAAW,uBAAwB,SAAQ,SAAS;IACxD,qBAAqB;IACrB,OAAO,EAAE,cAAc,CAAA;IACvB,sBAAsB;IACtB,UAAU,EAAE,cAAc,CAAA;IAC1B,gCAAgC;IAChC,cAAc,EAAE,cAAc,CAAA;IAC9B,yBAAyB;IACzB,UAAU,EAAE,cAAc,CAAA;IAC1B,0BAA0B;IAC1B,UAAU,EAAE,cAAc,CAAA;IAC1B,sBAAsB;IACtB,oBAAoB,EAAE,cAAc,CAAA;IACpC,kCAAkC;IAClC,aAAa,EAAE,cAAc,CAAA;IAC7B,iCAAiC;IACjC,UAAU,EAAE,cAAc,CAAA;IAC1B,4CAA4C;IAC5C,iBAAiB,EAAE,cAAc,CAAA;CAClC;AAED,gEAAgE;AAChE,MAAM,WAAW,sBAAuB,SAAQ,WAAW;IACzD,KAAK,EAAE;QACL,oEAAoE;QACpE,kBAAkB,EAAE,aAAa,CAAC,uBAAuB,CAAC,CAAA;KAC3D,CAAA;CACF;AAED,qEAAqE;AACrE,qBAAa,4BAA4B;IACvC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA0B;IAC/C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwC;IAE9D,iFAAiF;IACjF,YAAY,KAAK,EAAE,aAAa,CAAC,cAAc,CAAC,EAa/C;IAED,OAAO,CAAC,UAAU;IAelB;;;OAGG;IACH,MAAM,IAAI,sBAAsB,CAE/B;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;CACF;AAED,4DAA4D;AAC5D,MAAM,MAAM,uBAAuB,GACjC,YAAY,CAAC,oBAAoB,CAAC,GAChC,WAAW,CAAC,QAAQ,CAAC,GACrB,UAAU,CAAC,sBAAsB,CAAC,CAAA;AAEtC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,+BAsHhE"}
1
+ {"version":3,"file":"RemoteSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/RemoteSettingsCard.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAC7F,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAE1F,OAAO,EAAkD,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,IAAI,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExJ,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,6FAA6F;IAC7F,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,4CAA4C;AAC5C,MAAM,WAAW,uBAAwB,SAAQ,SAAS;IACxD,qBAAqB;IACrB,OAAO,EAAE,cAAc,CAAA;IACvB,sBAAsB;IACtB,UAAU,EAAE,cAAc,CAAA;IAC1B,gCAAgC;IAChC,cAAc,EAAE,cAAc,CAAA;IAC9B,yBAAyB;IACzB,UAAU,EAAE,cAAc,CAAA;IAC1B,0BAA0B;IAC1B,UAAU,EAAE,cAAc,CAAA;IAC1B,sBAAsB;IACtB,oBAAoB,EAAE,cAAc,CAAA;IACpC,kCAAkC;IAClC,aAAa,EAAE,cAAc,CAAA;IAC7B,iCAAiC;IACjC,UAAU,EAAE,cAAc,CAAA;IAC1B,4CAA4C;IAC5C,iBAAiB,EAAE,cAAc,CAAA;CAClC;AAED,gEAAgE;AAChE,MAAM,WAAW,sBAAuB,SAAQ,WAAW;IACzD,KAAK,EAAE;QACL,oEAAoE;QACpE,kBAAkB,EAAE,aAAa,CAAC,uBAAuB,CAAC,CAAA;KAC3D,CAAA;CACF;AAED,qEAAqE;AACrE,qBAAa,4BAA4B;IACvC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA0B;IAC/C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwC;IAE9D,iFAAiF;IACjF,YAAY,KAAK,EAAE,aAAa,CAAC,cAAc,CAAC,EAa/C;IAED,OAAO,CAAC,UAAU;IAelB;;;OAGG;IACH,MAAM,IAAI,sBAAsB,CAE/B;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;CACF;AAED,4DAA4D;AAC5D,MAAM,MAAM,uBAAuB,GACjC,YAAY,CAAC,oBAAoB,CAAC,GAChC,WAAW,CAAC,QAAQ,CAAC,GACrB,UAAU,CAAC,sBAAsB,CAAC,CAAA;AAEtC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,+BAuHhE"}
@@ -64,5 +64,5 @@ export function RemoteSettingsCard(props) {
64
64
  invalidLabel: t('settings.invalidNumber'),
65
65
  disabled,
66
66
  };
67
- return (_jsxs(PluginSettingsCard, { t: t, titleKey: "settings.title", descriptionKey: "settings.description", state: state, onSave: props.save, onDiscard: props.discard, children: [_jsx(BooleanField, { id: "settings-remote-enabled", label: t('settings.enabled'), hint: t('settings.enabledHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.enabled, onEdit: (text) => { props.edit('enabled', text); }, onReset: () => { props.resetField('enabled'); } }), _jsx(ValueField, { id: "settings-remote-token-ttl", label: t('settings.tokenTtlMs'), hint: t('settings.tokenTtlMsHint'), numeric: true, ...fieldProps, ...state.tokenTtlMs, onEdit: (text) => { props.edit('tokenTtlMs', text); }, onReset: () => { props.resetField('tokenTtlMs'); } }), _jsx(ValueField, { id: "settings-remote-offline", label: t('settings.offlineAfterMs'), hint: t('settings.offlineAfterMsHint'), numeric: true, ...fieldProps, ...state.offlineAfterMs, onEdit: (text) => { props.edit('offlineAfterMs', text); }, onReset: () => { props.resetField('offlineAfterMs'); } }), _jsx(ValueField, { id: "settings-remote-max-devices", label: t('settings.maxDevices'), hint: t('settings.maxDevicesHint'), numeric: true, ...fieldProps, ...state.maxDevices, onEdit: (text) => { props.edit('maxDevices', text); }, onReset: () => { props.resetField('maxDevices'); } }), _jsx(ValueField, { id: "settings-remote-cookie", label: t('settings.cookieName'), hint: t('settings.cookieNameHint'), ...fieldProps, ...state.cookieName, onEdit: (text) => { props.edit('cookieName', text); }, onReset: () => { props.resetField('cookieName'); } }), _jsx(BooleanField, { id: "settings-remote-fence", label: t('settings.requirePairingForLan'), hint: t('settings.requirePairingForLanHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.requirePairingForLan, onEdit: (text) => { props.edit('requirePairingForLan', text); }, onReset: () => { props.resetField('requirePairingForLan'); } }), _jsx(ValueField, { id: "settings-remote-public-base", label: t('settings.publicBaseUrl'), hint: t('settings.publicBaseUrlHint'), placeholder: "https://example.trycloudflare.com", ...fieldProps, ...state.publicBaseUrl, onEdit: (text) => { props.edit('publicBaseUrl', text); }, onReset: () => { props.resetField('publicBaseUrl'); } }), _jsx(BooleanField, { id: "settings-remote-auto-tunnel", label: t('settings.autoTunnel'), hint: t('settings.autoTunnelHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.autoTunnel, onEdit: (text) => { props.edit('autoTunnel', text); }, onReset: () => { props.resetField('autoTunnel'); } }), _jsx(BooleanField, { id: "settings-remote-mobile-enter", label: t('settings.mobileEnterToSend'), hint: t('settings.mobileEnterToSendHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.mobileEnterToSend, onEdit: (text) => { props.edit('mobileEnterToSend', text); }, onReset: () => { props.resetField('mobileEnterToSend'); } })] }));
67
+ return (_jsxs(PluginSettingsCard, { t: t, titleKey: "settings.title", descriptionKey: "settings.description", defaultOpen: false, state: state, onSave: props.save, onDiscard: props.discard, children: [_jsx(BooleanField, { id: "settings-remote-enabled", label: t('settings.enabled'), hint: t('settings.enabledHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.enabled, onEdit: (text) => { props.edit('enabled', text); }, onReset: () => { props.resetField('enabled'); } }), _jsx(ValueField, { id: "settings-remote-token-ttl", label: t('settings.tokenTtlMs'), hint: t('settings.tokenTtlMsHint'), numeric: true, ...fieldProps, ...state.tokenTtlMs, onEdit: (text) => { props.edit('tokenTtlMs', text); }, onReset: () => { props.resetField('tokenTtlMs'); } }), _jsx(ValueField, { id: "settings-remote-offline", label: t('settings.offlineAfterMs'), hint: t('settings.offlineAfterMsHint'), numeric: true, ...fieldProps, ...state.offlineAfterMs, onEdit: (text) => { props.edit('offlineAfterMs', text); }, onReset: () => { props.resetField('offlineAfterMs'); } }), _jsx(ValueField, { id: "settings-remote-max-devices", label: t('settings.maxDevices'), hint: t('settings.maxDevicesHint'), numeric: true, ...fieldProps, ...state.maxDevices, onEdit: (text) => { props.edit('maxDevices', text); }, onReset: () => { props.resetField('maxDevices'); } }), _jsx(ValueField, { id: "settings-remote-cookie", label: t('settings.cookieName'), hint: t('settings.cookieNameHint'), ...fieldProps, ...state.cookieName, onEdit: (text) => { props.edit('cookieName', text); }, onReset: () => { props.resetField('cookieName'); } }), _jsx(BooleanField, { id: "settings-remote-fence", label: t('settings.requirePairingForLan'), hint: t('settings.requirePairingForLanHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.requirePairingForLan, onEdit: (text) => { props.edit('requirePairingForLan', text); }, onReset: () => { props.resetField('requirePairingForLan'); } }), _jsx(ValueField, { id: "settings-remote-public-base", label: t('settings.publicBaseUrl'), hint: t('settings.publicBaseUrlHint'), placeholder: "https://example.trycloudflare.com", ...fieldProps, ...state.publicBaseUrl, onEdit: (text) => { props.edit('publicBaseUrl', text); }, onReset: () => { props.resetField('publicBaseUrl'); } }), _jsx(BooleanField, { id: "settings-remote-auto-tunnel", label: t('settings.autoTunnel'), hint: t('settings.autoTunnelHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.autoTunnel, onEdit: (text) => { props.edit('autoTunnel', text); }, onReset: () => { props.resetField('autoTunnel'); } }), _jsx(BooleanField, { id: "settings-remote-mobile-enter", label: t('settings.mobileEnterToSend'), hint: t('settings.mobileEnterToSendHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.mobileEnterToSend, onEdit: (text) => { props.edit('mobileEnterToSend', text); }, onReset: () => { props.resetField('mobileEnterToSend'); } })] }));
68
68
  }
@@ -1 +1 @@
1
- {"version":3,"file":"UpdateEntry.d.ts","sourceRoot":"","sources":["../../../src/client/UpdateEntry.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAOnE,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,IAAI,EAAE,OAAO,CAAA;IACb,CAAC,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;CACzB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,gBAAgB,+BA2ExD"}
1
+ {"version":3,"file":"UpdateEntry.d.ts","sourceRoot":"","sources":["../../../src/client/UpdateEntry.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAOnE,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,IAAI,EAAE,OAAO,CAAA;IACb,CAAC,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;CACzB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,gBAAgB,+BA2GxD"}
@@ -20,14 +20,33 @@ import css from "./remote.module.css";
20
20
  export function UpdateEntry({ wide, t }) {
21
21
  const [open, setOpen] = useState(false);
22
22
  const [view, setView] = useState({ kind: "checking" });
23
+ const [updateAvailable, setUpdateAvailable] = useState(false);
23
24
  const runToken = useRef(0);
25
+ const availabilityToken = useRef(0);
26
+ const mounted = useRef(false);
27
+ const probeAvailability = useCallback(async () => {
28
+ const token = ++availabilityToken.current;
29
+ try {
30
+ const status = await fetchUpdateStatus();
31
+ if (token === availabilityToken.current) {
32
+ setUpdateAvailable(status.mode === "npm" && status.outdated);
33
+ }
34
+ }
35
+ catch {
36
+ if (token === availabilityToken.current)
37
+ setUpdateAvailable(false);
38
+ }
39
+ }, []);
24
40
  const check = useCallback(async () => {
41
+ const availabilityCheck = ++availabilityToken.current;
25
42
  setView({ kind: "checking" });
26
43
  let status;
27
44
  try {
28
45
  status = await fetchUpdateStatus();
29
46
  }
30
47
  catch (error) {
48
+ if (availabilityCheck === availabilityToken.current)
49
+ setUpdateAvailable(false);
31
50
  // HTTP 404: the update route is not mounted — the host process runs an
32
51
  // older plugin build (client refreshed, host did not). Restarting dsh
33
52
  // web loads the new plugin; a plain network failure gets the generic
@@ -39,6 +58,9 @@ export function UpdateEntry({ wide, t }) {
39
58
  setView({ kind: "error", message: t("update.offline"), detail: t("update.offlineDetail") });
40
59
  return;
41
60
  }
61
+ if (availabilityCheck === availabilityToken.current) {
62
+ setUpdateAvailable(status.mode === "npm" && status.outdated);
63
+ }
42
64
  if (status.error === "registry-unreachable") {
43
65
  setView({ kind: "result", status });
44
66
  return;
@@ -52,6 +74,8 @@ export function UpdateEntry({ wide, t }) {
52
74
  const token = ++runToken.current;
53
75
  try {
54
76
  const result = await runUpdate();
77
+ if (result.ok && mounted.current)
78
+ setUpdateAvailable(false);
55
79
  if (token !== runToken.current)
56
80
  return;
57
81
  setView({ kind: "done", result });
@@ -70,7 +94,16 @@ export function UpdateEntry({ wide, t }) {
70
94
  runToken.current++;
71
95
  setOpen(false);
72
96
  }, []);
73
- // Unmount safety: an in-flight update must not land on a dead component.
74
- useEffect(() => () => { runToken.current++; }, []);
75
- return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: css.trigger, "data-wide": wide ? undefined : "rail", "aria-label": t("update.label"), title: t("update.label"), onClick: openPanel, children: _jsx(IconDownloadOutline16, { size: wide ? 16 : 18 }) }), open && createPortal((_jsxs("div", { className: css.overlay, role: "presentation", children: [_jsx("div", { className: css.mask, "aria-hidden": "true", onClick: closePanel }), _jsx(UpdatePanel, { t: t, view: view, onClose: closePanel, onRecheck: () => { void check(); } })] })), document.body)] }));
97
+ // Check once after mount without opening the panel or starting an update.
98
+ useEffect(() => {
99
+ mounted.current = true;
100
+ void probeAvailability();
101
+ return () => {
102
+ mounted.current = false;
103
+ runToken.current++;
104
+ availabilityToken.current++;
105
+ };
106
+ }, [probeAvailability]);
107
+ const updateLabel = updateAvailable ? t("update.availableLabel") : t("update.label");
108
+ return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: css.trigger, "data-wide": wide ? undefined : "rail", "data-update-available": updateAvailable ? "true" : undefined, "aria-label": updateLabel, title: updateLabel, onClick: openPanel, children: _jsx(IconDownloadOutline16, { size: wide ? 16 : 18 }) }), open && createPortal((_jsxs("div", { className: css.overlay, role: "presentation", children: [_jsx("div", { className: css.mask, "aria-hidden": "true", onClick: closePanel }), _jsx(UpdatePanel, { t: t, view: view, onClose: closePanel, onRecheck: () => { void check(); } })] })), document.body)] }));
76
109
  }
@@ -74,7 +74,7 @@ async function runAccept(token, page) {
74
74
  // mobile surface can open the intended workspace instead of losing the
75
75
  // QR context at this navigation boundary.
76
76
  if (isMobileSurface()) {
77
- url.pathname = '/m';
77
+ url.pathname = '/m/';
78
78
  page.navigate(`${url.pathname}${url.search}${url.hash}`);
79
79
  }
80
80
  else {
@@ -71,6 +71,7 @@ export declare const zh: {
71
71
  'settings.saveFailed': string;
72
72
  'settings.invalidNumber': string;
73
73
  'update.label': string;
74
+ 'update.availableLabel': string;
74
75
  'update.title': string;
75
76
  'update.checking': string;
76
77
  'update.upToDate': string;
@@ -179,6 +180,7 @@ export declare const en: {
179
180
  'settings.saveFailed': string;
180
181
  'settings.invalidNumber': string;
181
182
  'update.label': string;
183
+ 'update.availableLabel': string;
182
184
  'update.title': string;
183
185
  'update.checking': string;
184
186
  'update.upToDate': string;
@@ -1 +1 @@
1
- {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAE/E,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwGmB,CAAA;AAElC,sCAAsC;AACtC,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,EAAE,CAAA;AAEvC,wDAAwD;AACxD,MAAM,MAAM,eAAe,GAAG,SAAS,CAAA;AAEvC,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwGsB,CAAA"}
1
+ {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAE/E,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyGmB,CAAA;AAElC,sCAAsC;AACtC,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,EAAE,CAAA;AAEvC,wDAAwD;AACxD,MAAM,MAAM,eAAe,GAAG,SAAS,CAAA;AAEvC,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyGsB,CAAA"}
@@ -73,6 +73,7 @@ export const zh = {
73
73
  'settings.invalidNumber': '请输入数字,留空则使用默认值。',
74
74
  // 更新面板(侧边栏更新按钮)。
75
75
  'update.label': '检查更新',
76
+ 'update.availableLabel': '发现新版本,检查更新',
76
77
  'update.title': '检查更新',
77
78
  'update.checking': '正在检查远程仓库中的新版本…',
78
79
  'update.upToDate': '当前已是最新版本',
@@ -179,6 +180,7 @@ export const en = {
179
180
  'settings.invalidNumber': 'Enter a number, or leave blank to use the default.',
180
181
  // Update panel (the sidebar update trigger).
181
182
  'update.label': 'Check for updates',
183
+ 'update.availableLabel': 'New version available. Check for updates',
182
184
  'update.title': 'Check for updates',
183
185
  'update.checking': 'Checking the remote repository for newer releases…',
184
186
  'update.upToDate': 'Everything is up to date',
@@ -50,4 +50,13 @@ export declare function isLoopbackClient(request: IncomingMessage): boolean;
50
50
  * return false (without calling it) to veto with 403.
51
51
  */
52
52
  export declare function makeGateListener(service: PairingService, requirePairingForLan?: boolean | (() => boolean), enabled?: boolean | (() => boolean)): (request: IncomingMessage, method: string | undefined, next: () => boolean | Promise<boolean>) => boolean | Promise<boolean>;
53
+ /**
54
+ * Whether a request carries a live, non-revoked paired-device cookie for
55
+ * this service. Sibling host routes outside /api (aionui-panel, etc.) use
56
+ * the same check via the remoteWebUiPairing service.
57
+ * @param service - the pairing service that owns the device table.
58
+ * @param request - the incoming HTTP request.
59
+ * @returns true when the cookie names a live session (and lastSeenAt was refreshed).
60
+ */
61
+ export declare function isPairedDeviceRequest(service: PairingService, request: IncomingMessage): boolean;
53
62
  //# sourceMappingURL=gate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../src/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAGlD;;;;;;;GAOG;AAEH;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CASvF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,GAAG,SAAS,CAQvE;AAED,oGAAoG;AACpG,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAKlE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,cAAc,EACvB,oBAAoB,GAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAQ,EACtD,OAAO,GAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAQ,GACxC,CAAC,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAW9H"}
1
+ {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../src/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAGlD;;;;;;;GAOG;AAEH;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CASvF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,GAAG,SAAS,CAQvE;AAED,oGAAoG;AACpG,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAKlE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,cAAc,EACvB,oBAAoB,GAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAQ,EACtD,OAAO,GAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAQ,GACxC,CAAC,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAS9H;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAIhG"}
package/lib/types/gate.js CHANGED
@@ -86,9 +86,20 @@ export function makeGateListener(service, requirePairingForLan = true, enabled =
86
86
  const require = typeof requirePairingForLan === 'function' ? requirePairingForLan() : requirePairingForLan;
87
87
  if (!require)
88
88
  return next();
89
- const deviceId = readCookie(request.headers.cookie, service.config.cookieName);
90
- if (deviceId === undefined)
91
- return false;
92
- return service.touchDevice(deviceId) ? next() : false;
89
+ return isPairedDeviceRequest(service, request) ? next() : false;
93
90
  };
94
91
  }
92
+ /**
93
+ * Whether a request carries a live, non-revoked paired-device cookie for
94
+ * this service. Sibling host routes outside /api (aionui-panel, etc.) use
95
+ * the same check via the remoteWebUiPairing service.
96
+ * @param service - the pairing service that owns the device table.
97
+ * @param request - the incoming HTTP request.
98
+ * @returns true when the cookie names a live session (and lastSeenAt was refreshed).
99
+ */
100
+ export function isPairedDeviceRequest(service, request) {
101
+ const deviceId = readCookie(request.headers.cookie, service.config.cookieName);
102
+ if (deviceId === undefined)
103
+ return false;
104
+ return service.touchDevice(deviceId);
105
+ }
@@ -10,6 +10,7 @@
10
10
  import type { IncomingMessage } from 'node:http';
11
11
  import type { Context } from '@deepseek-ai/cordis';
12
12
  import z from 'schemastery';
13
+ import { type PairingConfig } from './pairing.ts';
13
14
  declare module '@deepseek-ai/cordis' {
14
15
  interface Events {
15
16
  /**
@@ -57,6 +58,14 @@ export interface Config {
57
58
  * warning (LAN-only behavior preserved). Ignored while `autoTunnel` is on.
58
59
  */
59
60
  publicBaseUrl?: string;
61
+ /**
62
+ * Absolute path to a JSON file where paired device sessions are persisted.
63
+ * When set, a paired phone keeps its session across `dsh web` restarts
64
+ * (the phone-side cookie already lives 365 days), so re-scanning the QR
65
+ * after each restart is unnecessary. Unset keeps sessions memory-only
66
+ * (previous behavior).
67
+ */
68
+ devicesFile?: string;
60
69
  /**
61
70
  * When true, the plugin runs its own Cloudflare quick tunnel (the
62
71
  * cloudflared binary ships with the package — no user-side install) and
@@ -76,6 +85,22 @@ export interface Config {
76
85
  enabled?: boolean;
77
86
  }
78
87
  export declare const Config: z<Config>;
88
+ /**
89
+ * Fully resolved config: every field non-optional except `publicBaseUrl`,
90
+ * which legitimately resolves to `undefined` when unset (the schema keeps it
91
+ * optional, so `Required` alone would over-narrow it to `string`).
92
+ */
93
+ type ResolvedConfig = Required<Omit<Config, 'publicBaseUrl' | 'devicesFile'>> & {
94
+ publicBaseUrl: string | undefined;
95
+ devicesFile: string | undefined;
96
+ };
97
+ /**
98
+ * The single mapping from resolved plugin config to the pairing service
99
+ * config. Both the constructed service and every live settings sync reuse
100
+ * it, so no field can be silently dropped when the web settings surface
101
+ * pushes a new value into the running service.
102
+ */
103
+ export declare function pairingConfigOf(resolved: Pick<ResolvedConfig, 'tokenTtlMs' | 'offlineAfterMs' | 'maxDevices' | 'cookieName' | 'devicesFile'>): PairingConfig;
79
104
  /**
80
105
  * Mount the pairing service, routes, gate listener, and presence sweep.
81
106
  * @param ctx - host plugin context carrying webServer.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,CAAC,MAAM,aAAa,CAAA;AAoB3B,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,MAAM;QACd;;;;;WAKG;QACH,UAAU,CACR,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,IAAI,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GACrC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;KAC9B;CACF;AAED,iCAAiC;AACjC,eAAO,MAAM,IAAI,kBAAkB,CAAA;AAEnC,+DAA+D;AAC/D,eAAO,MAAM,MAAM,UAA4B,CAAA;AAE/C;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,uDAAqC,CAAA;AAElF,qEAAqE;AACrE,MAAM,WAAW,MAAM;IACrB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,2EAA2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,eAAO,MAAM,MAAM,EAAE,CAAC,CAAC,MAAM,CAU3B,CAAA;AAyBF;;;;GAIG;AACH,eAAO,MAAM,KAAK,kBAAuD,CAAA;AAEzE,iBAAS,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAkOtD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,CAAC,MAAM,aAAa,CAAA;AAE3B,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAmBjE,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,MAAM;QACd;;;;;WAKG;QACH,UAAU,CACR,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,IAAI,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GACrC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;KAC9B;CACF;AAED,iCAAiC;AACjC,eAAO,MAAM,IAAI,kBAAkB,CAAA;AAEnC,+DAA+D;AAC/D,eAAO,MAAM,MAAM,UAA4B,CAAA;AAE/C;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,uDAAqC,CAAA;AAElF,qEAAqE;AACrE,MAAM,WAAW,MAAM;IACrB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,2EAA2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,eAAO,MAAM,MAAM,EAAE,CAAC,CAAC,MAAM,CAW3B,CAAA;AAKF;;;;GAIG;AACH,KAAK,cAAc,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,eAAe,GAAG,aAAa,CAAC,CAAC,GAAG;IAC9E,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAA;CAChC,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,IAAI,CAC5C,cAAc,EACd,YAAY,GAAG,gBAAgB,GAAG,YAAY,GAAG,YAAY,GAAG,aAAa,CAC9E,GAAG,aAAa,CAQhB;AAgBD;;;;GAIG;AACH,eAAO,MAAM,KAAK,kBAAuD,CAAA;AAEzE,iBAAS,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAgOtD"}
@@ -12,7 +12,8 @@ import { setInterval as nodeSetInterval } from 'node:timers';
12
12
  import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings';
13
13
  import z from 'schemastery';
14
14
  import { PairingService } from './pairing.js';
15
- import { makeGateListener } from './gate.js';
15
+ import { isPairedDeviceRequest, makeGateListener } from './gate.js';
16
+ import { RemoteWebUiPairing } from './pairing-access.js';
16
17
  import { isTrustedApiRequest, makeRoutes } from './routes.js';
17
18
  import { makeMobileRoutes } from './mobile-routes.js';
18
19
  import { makeMobileApiRoutes } from './mobile-api.js';
@@ -38,12 +39,28 @@ export const Config = z.object({
38
39
  cookieName: z.string().min(1).default('dsh_pair'),
39
40
  requirePairingForLan: z.boolean().default(true),
40
41
  publicBaseUrl: z.string(),
42
+ devicesFile: z.string(),
41
43
  autoTunnel: z.boolean().default(false),
42
44
  mobileEnterToSend: z.boolean().default(true),
43
45
  enabled: z.boolean().default(true),
44
46
  });
45
47
  /** Presence sweep cadence (a stale device flips to disconnected within two sweeps). */
46
48
  const SWEEP_INTERVAL_MS = 10_000;
49
+ /**
50
+ * The single mapping from resolved plugin config to the pairing service
51
+ * config. Both the constructed service and every live settings sync reuse
52
+ * it, so no field can be silently dropped when the web settings surface
53
+ * pushes a new value into the running service.
54
+ */
55
+ export function pairingConfigOf(resolved) {
56
+ return {
57
+ tokenTtlMs: resolved.tokenTtlMs,
58
+ offlineAfterMs: resolved.offlineAfterMs,
59
+ maxDevices: resolved.maxDevices,
60
+ cookieName: resolved.cookieName,
61
+ devicesFile: resolved.devicesFile,
62
+ };
63
+ }
47
64
  /** Schema defaults, re-read for hand-built test contexts (the loader applies them normally). */
48
65
  const DEFAULTS = {
49
66
  tokenTtlMs: 10 * 60_000,
@@ -52,6 +69,7 @@ const DEFAULTS = {
52
69
  cookieName: 'dsh_pair',
53
70
  requirePairingForLan: true,
54
71
  publicBaseUrl: undefined,
72
+ devicesFile: undefined,
55
73
  autoTunnel: false,
56
74
  mobileEnterToSend: true,
57
75
  enabled: true,
@@ -70,6 +88,7 @@ function applyImpl(ctx, config) {
70
88
  cookieName: config?.cookieName ?? DEFAULTS.cookieName,
71
89
  requirePairingForLan: config?.requirePairingForLan ?? DEFAULTS.requirePairingForLan,
72
90
  publicBaseUrl: config?.publicBaseUrl,
91
+ devicesFile: config?.devicesFile,
73
92
  autoTunnel: config?.autoTunnel ?? DEFAULTS.autoTunnel,
74
93
  mobileEnterToSend: config?.mobileEnterToSend ?? DEFAULTS.mobileEnterToSend,
75
94
  enabled: config?.enabled ?? DEFAULTS.enabled,
@@ -87,17 +106,13 @@ function applyImpl(ctx, config) {
87
106
  cookieName: value.cookieName ?? DEFAULTS.cookieName,
88
107
  requirePairingForLan: value.requirePairingForLan ?? DEFAULTS.requirePairingForLan,
89
108
  publicBaseUrl: value.publicBaseUrl,
109
+ devicesFile: value.devicesFile ?? DEFAULTS.devicesFile,
90
110
  autoTunnel: value.autoTunnel ?? DEFAULTS.autoTunnel,
91
111
  mobileEnterToSend: value.mobileEnterToSend ?? DEFAULTS.mobileEnterToSend,
92
112
  enabled: value.enabled ?? DEFAULTS.enabled,
93
113
  };
94
114
  };
95
- const service = new PairingService({
96
- tokenTtlMs: resolved.tokenTtlMs,
97
- offlineAfterMs: resolved.offlineAfterMs,
98
- maxDevices: resolved.maxDevices,
99
- cookieName: resolved.cookieName,
100
- });
115
+ const service = new PairingService(pairingConfigOf(resolved));
101
116
  // ── auto tunnel ─────────────────────────────────────────────────────────
102
117
  // The minted public URL becomes the QR base (and the pairing fence's
103
118
  // trusted host). Phone /api traffic rides the plugin's own /m/api channel,
@@ -228,14 +243,16 @@ function applyImpl(ctx, config) {
228
243
  ];
229
244
  const gate = makeGateListener(service, () => resolve().requirePairingForLan, () => resolve().enabled);
230
245
  ctx.effect(() => ctx.on('api/gate', gate), 'remote-web-ui: api gate');
246
+ // Sibling plugins (aionui-panel, …) look this up by name. Absent when this
247
+ // plugin is not installed; stop() / enabled=false still refuse cookies.
248
+ new RemoteWebUiPairing(ctx, (request) => {
249
+ if (!resolve().enabled)
250
+ return false;
251
+ return isPairedDeviceRequest(service, request);
252
+ });
231
253
  const sync = () => {
232
254
  const value = resolve();
233
- service.config = {
234
- tokenTtlMs: value.tokenTtlMs,
235
- offlineAfterMs: value.offlineAfterMs,
236
- maxDevices: value.maxDevices,
237
- cookieName: value.cookieName,
238
- };
255
+ service.config = pairingConfigOf(value);
239
256
  // The auto tunnel owns the public base while enabled: the minted URL
240
257
  // lands in the service through the tunnel's phase listener. The manual
241
258
  // publicBaseUrl applies only when the auto tunnel is off.
@@ -0,0 +1,7 @@
1
+ export interface PairRequiredViewProps {
2
+ initialError?: string;
3
+ onPaired(path: string): void;
4
+ }
5
+ /** Collect a fresh desktop-issued pairing link when this client has no paired cookie. */
6
+ export declare function PairRequiredView({ initialError, onPaired }: PairRequiredViewProps): import("react").JSX.Element;
7
+ //# sourceMappingURL=PairRequiredView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PairRequiredView.d.ts","sourceRoot":"","sources":["../../../src/mobile/PairRequiredView.tsx"],"names":[],"mappings":"AAIA,MAAM,WAAW,qBAAqB;IACpC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAED,yFAAyF;AACzF,wBAAgB,gBAAgB,CAAC,EAAE,YAAY,EAAE,QAAQ,EAAE,EAAE,qBAAqB,+BA+CjF"}
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** Device-pairing gate for installed mobile web-app contexts with isolated storage. */
3
+ import { useState } from 'react';
4
+ import { acceptMobilePair, mobilePairPath, parseMobilePairInput } from './pairing.js';
5
+ /** Collect a fresh desktop-issued pairing link when this client has no paired cookie. */
6
+ export function PairRequiredView({ initialError, onPaired }) {
7
+ const [value, setValue] = useState('');
8
+ const [error, setError] = useState(initialError);
9
+ const [submitting, setSubmitting] = useState(false);
10
+ const submit = async (event) => {
11
+ event.preventDefault();
12
+ const input = parseMobilePairInput(value);
13
+ if (input === undefined) {
14
+ setError('请输入有效的配对链接。');
15
+ return;
16
+ }
17
+ setSubmitting(true);
18
+ setError(undefined);
19
+ const result = await acceptMobilePair(input.token);
20
+ setSubmitting(false);
21
+ if (!result.ok) {
22
+ setError(result.message);
23
+ return;
24
+ }
25
+ onPaired(mobilePairPath(input.workspaceId));
26
+ };
27
+ return (_jsx("main", { className: "mobile mobile-pair", "aria-labelledby": "mobile-pair-title", children: _jsxs("form", { className: "mobile-pairCard", onSubmit: (event) => { void submit(event); }, children: [_jsx("h1", { id: "mobile-pair-title", className: "mobile-title", children: "\u8BBE\u5907\u914D\u5BF9" }), _jsx("p", { className: "mobile-muted", children: "\u7C98\u8D34\u684C\u9762\u7AEF\u590D\u5236\u7684\u914D\u5BF9\u94FE\u63A5\u4EE5\u8FDE\u63A5\u6B64\u8BBE\u5907\u3002" }), _jsx("label", { className: "mobile-pairLabel", htmlFor: "mobile-pair-link", children: "\u914D\u5BF9\u94FE\u63A5" }), _jsx("input", { id: "mobile-pair-link", className: "mobile-pairInput", value: value, onChange: (event) => { setValue(event.target.value); }, autoComplete: "off", autoCapitalize: "off", spellCheck: false }), error === undefined ? null : _jsx("p", { className: "mobile-error", role: "alert", children: error }), _jsx("button", { className: "mobile-new mobile-pairSubmit", type: "submit", disabled: submitting, children: submitting ? '正在配对' : '配对' })] }) }));
28
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=PairRequiredView.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PairRequiredView.test.d.ts","sourceRoot":"","sources":["../../../src/mobile/PairRequiredView.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // @vitest-environment jsdom
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
5
+ import { PairRequiredView } from './PairRequiredView.js';
6
+ afterEach(() => {
7
+ cleanup();
8
+ vi.unstubAllGlobals();
9
+ });
10
+ describe('PairRequiredView', () => {
11
+ it('accepts a pasted link in the installed app context', async () => {
12
+ const fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 }));
13
+ vi.stubGlobal('fetch', fetch);
14
+ const onPaired = vi.fn();
15
+ render(_jsx(PairRequiredView, { onPaired: onPaired }));
16
+ fireEvent.change(screen.getByLabelText('配对链接'), { target: { value: 'https://phone.example/m/?pair=tok-1&workspace=ws-7' } });
17
+ fireEvent.click(screen.getByRole('button', { name: '配对' }));
18
+ await waitFor(() => expect(onPaired).toHaveBeenCalledWith('/m/?workspace=ws-7'));
19
+ });
20
+ it('shows an initial QR failure without starting the mobile data channel', () => {
21
+ render(_jsx(PairRequiredView, { initialError: "\u914D\u5BF9\u94FE\u63A5\u5DF2\u88AB\u4F7F\u7528\u3002", onPaired: vi.fn() }));
22
+ expect(screen.getByRole('alert').textContent).toContain('配对链接已被使用。');
23
+ });
24
+ });
@@ -11,6 +11,8 @@ import { createRoot } from 'react-dom/client';
11
11
  import { App } from './views/App.js';
12
12
  import { mobileCss } from './mobile-styles.js';
13
13
  import { initMobileTheme } from './mobile-theme.js';
14
+ import { registerMobilePwa } from '../mobile-pwa.js';
15
+ import { consumeMobilePairUrl } from './pairing.js';
14
16
  // Apply the persisted (or default light) theme before first paint, so the
15
17
  // page never flashes the wrong palette.
16
18
  initMobileTheme();
@@ -22,4 +24,15 @@ document.head.appendChild(style);
22
24
  const root = document.getElementById('root');
23
25
  if (root === null)
24
26
  throw new Error('mobile: #root missing');
25
- createRoot(root).render(_jsx(App, {}));
27
+ void bootMobile(root);
28
+ async function bootMobile(mount) {
29
+ const pair = await consumeMobilePairUrl(window.location.href);
30
+ if (pair.kind === 'accepted') {
31
+ window.location.replace(pair.path);
32
+ return;
33
+ }
34
+ if (pair.kind === 'failed')
35
+ window.history.replaceState(null, '', pair.path);
36
+ void registerMobilePwa();
37
+ createRoot(mount).render(_jsx(App, { initialPairError: pair.kind === 'failed' ? pair.message : undefined }));
38
+ }