@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.
- package/README.i18n.yaml +2 -2
- package/README.md +41 -19
- package/README.zh.md +17 -12
- package/assets/mobile-icon-192.png +0 -0
- package/assets/mobile-icon-512.png +0 -0
- package/assets/mobile-service-worker.js +64 -0
- package/lib/client.js +37 -10
- package/lib/client.js.map +1 -1
- package/lib/index.js +290 -53
- package/lib/mobile.js +278 -17
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +6 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +5 -3
- package/lib/types/client/RemoteEntry.js +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +1 -1
- package/lib/types/client/UpdateEntry.d.ts.map +1 -1
- package/lib/types/client/UpdateEntry.js +36 -3
- package/lib/types/client/deep-link.js +1 -1
- package/lib/types/client/locales.d.ts +2 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +2 -0
- package/lib/types/gate.d.ts +9 -0
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +15 -4
- package/lib/types/index.d.ts +25 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +30 -13
- package/lib/types/mobile/PairRequiredView.d.ts +7 -0
- package/lib/types/mobile/PairRequiredView.d.ts.map +1 -0
- package/lib/types/mobile/PairRequiredView.js +28 -0
- package/lib/types/mobile/PairRequiredView.test.d.ts +2 -0
- package/lib/types/mobile/PairRequiredView.test.d.ts.map +1 -0
- package/lib/types/mobile/PairRequiredView.test.js +24 -0
- package/lib/types/mobile/index.js +14 -1
- package/lib/types/mobile/mobile-styles.d.ts +1 -1
- package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
- package/lib/types/mobile/mobile-styles.js +48 -0
- package/lib/types/mobile/pairing.d.ts +31 -0
- package/lib/types/mobile/pairing.d.ts.map +1 -0
- package/lib/types/mobile/pairing.js +62 -0
- package/lib/types/mobile/views/App.d.ts +12 -5
- package/lib/types/mobile/views/App.d.ts.map +1 -1
- package/lib/types/mobile/views/App.js +34 -7
- package/lib/types/mobile/views/App.test.d.ts +2 -0
- package/lib/types/mobile/views/App.test.d.ts.map +1 -0
- package/lib/types/mobile/views/App.test.js +52 -0
- package/lib/types/mobile-pwa.d.ts +6 -0
- package/lib/types/mobile-pwa.d.ts.map +1 -0
- package/lib/types/mobile-pwa.js +17 -0
- package/lib/types/mobile-routes.d.ts +5 -8
- package/lib/types/mobile-routes.d.ts.map +1 -1
- package/lib/types/mobile-routes.js +115 -30
- package/lib/types/pairing-access.d.ts +28 -0
- package/lib/types/pairing-access.d.ts.map +1 -0
- package/lib/types/pairing-access.js +26 -0
- package/lib/types/pairing.d.ts +27 -0
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +68 -0
- package/lib/types/pairing.persist.test.d.ts +2 -0
- package/lib/types/pairing.persist.test.d.ts.map +1 -0
- package/lib/types/pairing.persist.test.js +128 -0
- package/lib/types/routes.js +1 -1
- package/lib/types/update.d.ts +3 -3
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +34 -11
- package/package.json +12 -12
- package/src/client/PluginSettingsCard.tsx +10 -0
- package/src/client/RemoteEntry.tsx +2 -2
- package/src/client/RemoteSettingsCard.tsx +1 -0
- package/src/client/UpdateEntry.tsx +36 -4
- package/src/client/deep-link.ts +1 -1
- package/src/client/locales.ts +2 -0
- package/src/client/remote.module.css +26 -0
- package/src/gate.ts +15 -3
- package/src/index.ts +46 -15
- package/src/mobile/PairRequiredView.test.tsx +28 -0
- package/src/mobile/PairRequiredView.tsx +58 -0
- package/src/mobile/index.tsx +15 -1
- package/src/mobile/mobile-styles.ts +48 -0
- package/src/mobile/pairing.ts +77 -0
- package/src/mobile/views/App.test.tsx +62 -0
- package/src/mobile/views/App.tsx +53 -6
- package/src/mobile-pwa.ts +19 -0
- package/src/mobile-routes.ts +123 -30
- package/src/pairing-access.ts +36 -0
- package/src/pairing.persist.test.ts +145 -0
- package/src/pairing.ts +70 -1
- package/src/routes.ts +1 -1
- 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":"AACA;;;;;;;GAOG;AAEH,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAGnD,oGAAoG;AACpG,eAAO,MAAM,cAAc,YACzB,mBAAmB,EACnB,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,kBAAkB,EAClB,eAAe,EACf,iBAAiB,CACT,CAAA;AAEV,6CAA6C;AAC7C,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAA;AAEzD,oEAAoE;AACpE,MAAM,MAAM,eAAe,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,IAAI,IAAI,GAAG,WAAW,CAAA;AAE9E,wDAAwD;AACxD,MAAM,WAAW,uBAAuB,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM;IACnE,0CAA0C;IAC1C,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KAAK,MAAM,CAAA;IACnF,uCAAuC;IACvC,QAAQ,EAAE,IAAI,CAAA;IACd,4EAA4E;IAC5E,cAAc,EAAE,IAAI,CAAA;IACpB,kFAAkF;IAClF,KAAK,EAAE,SAAS,CAAA;IAChB,+BAA+B;IAC/B,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,8BAA8B;IAC9B,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,6BAA6B;IAC7B,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,uBAAuB,CAAC,IAAI,CAAC,
|
|
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,
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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 ?
|
|
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,+
|
|
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,+
|
|
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
|
-
//
|
|
74
|
-
useEffect(() =>
|
|
75
|
-
|
|
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
|
|
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',
|
package/lib/types/gate.d.ts
CHANGED
|
@@ -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
|
package/lib/types/gate.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
+
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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.
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/lib/types/index.js
CHANGED
|
@@ -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 @@
|
|
|
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
|
-
|
|
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
|
+
}
|