@linxin666/dsh-remote-web-ui 0.2.4 → 0.2.6
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 +148 -77
- package/README.zh.md +32 -26
- package/lib/client.js +603 -68
- package/lib/client.js.map +1 -1
- package/lib/index.js +667 -32
- package/lib/mobile.js +3 -1
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/FenceNotice.d.ts +21 -0
- package/lib/types/client/FenceNotice.d.ts.map +1 -0
- package/lib/types/client/FenceNotice.js +10 -0
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +18 -11
- package/lib/types/client/RemotePanel.d.ts +10 -4
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +7 -6
- package/lib/types/client/RemoteSettingsCard.d.ts +4 -0
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +3 -1
- package/lib/types/client/device-name.d.ts +3 -0
- package/lib/types/client/device-name.d.ts.map +1 -0
- package/lib/types/client/device-name.js +30 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +48 -0
- package/lib/types/client/locales.d.ts +51 -1
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +79 -29
- package/lib/types/client/pair-api.d.ts +35 -0
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +39 -0
- package/lib/types/client/remote-channel.d.ts +93 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -0
- package/lib/types/client/remote-channel.js +246 -0
- package/lib/types/dsh-home.d.ts +17 -0
- package/lib/types/dsh-home.d.ts.map +1 -0
- package/lib/types/dsh-home.js +34 -0
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +5 -2
- package/lib/types/index.d.ts +19 -12
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +73 -5
- package/lib/types/loopback-proxy.d.ts +28 -0
- package/lib/types/loopback-proxy.d.ts.map +1 -0
- package/lib/types/loopback-proxy.js +112 -0
- package/lib/types/mobile/views/SessionListView.d.ts.map +1 -1
- package/lib/types/mobile/views/SessionListView.js +6 -1
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +6 -2
- package/lib/types/mobile-routes.js +1 -1
- package/lib/types/pairing.d.ts +63 -11
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +137 -20
- package/lib/types/pairing.persist.test.js +60 -5
- package/lib/types/posture.d.ts +75 -0
- package/lib/types/posture.d.ts.map +1 -0
- package/lib/types/posture.js +116 -0
- package/lib/types/remote-api.d.ts +60 -0
- package/lib/types/remote-api.d.ts.map +1 -0
- package/lib/types/remote-api.js +181 -0
- package/lib/types/remote-methods.d.ts +33 -0
- package/lib/types/remote-methods.d.ts.map +1 -0
- package/lib/types/remote-methods.js +54 -0
- package/lib/types/routes.d.ts +4 -0
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +33 -4
- package/package.json +2 -2
- package/src/client/FenceNotice.tsx +43 -0
- package/src/client/RemoteEntry.tsx +19 -9
- package/src/client/RemotePanel.tsx +83 -10
- package/src/client/RemoteSettingsCard.tsx +16 -0
- package/src/client/device-name.ts +31 -0
- package/src/client/index.ts +46 -0
- package/src/client/locales.ts +79 -29
- package/src/client/pair-api.ts +60 -0
- package/src/client/remote-channel.ts +269 -0
- package/src/client/remote.module.css +240 -16
- package/src/client/settings-card.module.css +2 -0
- package/src/dsh-home.ts +36 -0
- package/src/gate.ts +5 -2
- package/src/index.ts +89 -17
- package/src/loopback-proxy.ts +126 -0
- package/src/mobile/views/SessionListView.tsx +6 -1
- package/src/mobile-api.ts +6 -2
- package/src/mobile-routes.ts +1 -1
- package/src/pairing.persist.test.ts +64 -5
- package/src/pairing.ts +167 -20
- package/src/posture.ts +154 -0
- package/src/remote-api.ts +217 -0
- package/src/remote-methods.ts +62 -0
- package/src/routes.ts +33 -4
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unpaired-desktop notice: a full-page blocking surface rendered when the remote channel
|
|
3
|
+
* (see remote-channel.ts) refuses a call because this desktop browser has no
|
|
4
|
+
* live paired-device cookie. Retires automatically once a gated call
|
|
5
|
+
* succeeds (the channel reports pairing), so it never outlives the
|
|
6
|
+
* unpaired state it describes.
|
|
7
|
+
*/
|
|
8
|
+
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots';
|
|
9
|
+
/** Notice props: localized copy. */
|
|
10
|
+
export interface FenceNoticeProps {
|
|
11
|
+
t: TranslateNS<'remote'>;
|
|
12
|
+
/** Retry after the user has opened a freshly issued computer pairing link. */
|
|
13
|
+
onRetry: () => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Render the unpaired blocking page.
|
|
17
|
+
* @param props - localized copy.
|
|
18
|
+
* @returns the notice element.
|
|
19
|
+
*/
|
|
20
|
+
export declare function FenceNotice({ t, onRetry }: FenceNoticeProps): import("react").JSX.Element;
|
|
21
|
+
//# sourceMappingURL=FenceNotice.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FenceNotice.d.ts","sourceRoot":"","sources":["../../../src/client/FenceNotice.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAGnE,oCAAoC;AACpC,MAAM,WAAW,gBAAgB;IAC/B,CAAC,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IACxB,8EAA8E;IAC9E,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,gBAAgB,+BAoB3D"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import css from './remote.module.css';
|
|
3
|
+
/**
|
|
4
|
+
* Render the unpaired blocking page.
|
|
5
|
+
* @param props - localized copy.
|
|
6
|
+
* @returns the notice element.
|
|
7
|
+
*/
|
|
8
|
+
export function FenceNotice({ t, onRetry }) {
|
|
9
|
+
return (_jsx("div", { className: css.fencePage, role: "dialog", "aria-modal": "true", "aria-labelledby": "remote-fence-title", children: _jsxs("main", { className: css.fenceCard, "data-dsh-plugin": "remote-web-ui", children: [_jsx("div", { className: css.fenceMark, "aria-hidden": "true", children: "\u00D7" }), _jsx("p", { className: css.fenceEyebrow, children: t('fence.unpaired.eyebrow') }), _jsx("h1", { id: "remote-fence-title", className: css.fenceTitle, children: t('fence.unpaired.title') }), _jsx("p", { className: css.fenceDetail, children: t('fence.unpaired.hint') }), _jsxs("ol", { className: css.fenceSteps, children: [_jsx("li", { children: t('fence.unpaired.stepDesktop') }), _jsx("li", { children: t('fence.unpaired.stepLink') }), _jsx("li", { children: t('fence.unpaired.stepOpen') })] }), _jsx("button", { className: css.fenceRetry, type: "button", onClick: onRetry, children: t('fence.unpaired.retry') }), _jsx("p", { className: css.fenceFootnote, children: t('fence.unpaired.footnote') })] }) }));
|
|
10
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RemoteEntry.d.ts","sourceRoot":"","sources":["../../../src/client/RemoteEntry.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAQjF,wEAAwE;AACxE,MAAM,MAAM,gBAAgB,GAAG,YAAY,CAAC,gBAAgB,CAAC,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"RemoteEntry.d.ts","sourceRoot":"","sources":["../../../src/client/RemoteEntry.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAQjF,wEAAwE;AACxE,MAAM,MAAM,gBAAgB,GAAG,YAAY,CAAC,gBAAgB,CAAC,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAA;AA0BrF;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,EAAE,EAAE,gBAAgB,+BA+LvE"}
|
|
@@ -11,7 +11,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
11
11
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
12
12
|
import { createPortal } from 'react-dom';
|
|
13
13
|
import { RemotePanel } from './RemotePanel.js';
|
|
14
|
-
import { copyText, issuePair, stopPair } from './pair-api.js';
|
|
14
|
+
import { copyText, issuePair, revokePair, stopPair } from './pair-api.js';
|
|
15
15
|
import { PhoneIcon } from './PhoneIcon.js';
|
|
16
16
|
import { UpdateEntry } from './UpdateEntry.js';
|
|
17
17
|
import css from './remote.module.css';
|
|
@@ -34,7 +34,9 @@ function mergeFrame(state, frame) {
|
|
|
34
34
|
phase: frame.phase,
|
|
35
35
|
deviceCount: frame.deviceCount,
|
|
36
36
|
onlineCount: frame.onlineCount,
|
|
37
|
+
devices: frame.devices ?? [],
|
|
37
38
|
...(frame.tunnel !== undefined ? { tunnel: frame.tunnel } : {}),
|
|
39
|
+
...(frame.posture !== undefined ? { posture: frame.posture } : {}),
|
|
38
40
|
};
|
|
39
41
|
}
|
|
40
42
|
/**
|
|
@@ -50,7 +52,7 @@ export function RemoteEntry({ wide, useWorkspaces, t }) {
|
|
|
50
52
|
// pure), so mint decisions read this ref instead.
|
|
51
53
|
const stateRef = useRef(state);
|
|
52
54
|
useEffect(() => { stateRef.current = state; }, [state]);
|
|
53
|
-
const [copied, setCopied] = useState(
|
|
55
|
+
const [copied, setCopied] = useState(undefined);
|
|
54
56
|
const eventSource = useRef(undefined);
|
|
55
57
|
// Generation counter for the open flow: closing (or re-opening) the panel
|
|
56
58
|
// bumps it, so an in-flight issue() that resolves after a close does not
|
|
@@ -92,6 +94,7 @@ export function RemoteEntry({ wide, useWorkspaces, t }) {
|
|
|
92
94
|
phase: 'waiting',
|
|
93
95
|
deviceCount: 0,
|
|
94
96
|
onlineCount: 0,
|
|
97
|
+
devices: [],
|
|
95
98
|
// Whether this QR is built on the configured public (tunneled) base.
|
|
96
99
|
public: publicBaseUrl !== undefined && result.url.startsWith(publicBaseUrl),
|
|
97
100
|
...(publicBaseUrl !== undefined ? { publicBaseUrl } : {}),
|
|
@@ -174,7 +177,13 @@ export function RemoteEntry({ wide, useWorkspaces, t }) {
|
|
|
174
177
|
// keeps the UI honest, and the status stream confirms the stopped phase.
|
|
175
178
|
void stopPair().catch(() => { });
|
|
176
179
|
// Optimistic fallback; the status stream confirms with the stopped phase.
|
|
177
|
-
setState(previous => previous.kind === 'ready' ? { ...previous, phase: 'stopped' } : previous);
|
|
180
|
+
setState(previous => previous.kind === 'ready' ? { ...previous, phase: 'stopped', devices: [] } : previous);
|
|
181
|
+
}, []);
|
|
182
|
+
const handleRevoke = useCallback((deviceId) => {
|
|
183
|
+
void revokePair(deviceId).catch(() => { });
|
|
184
|
+
setState(previous => previous.kind === 'ready'
|
|
185
|
+
? { ...previous, devices: previous.devices.filter(device => device.id !== deviceId) }
|
|
186
|
+
: previous);
|
|
178
187
|
}, []);
|
|
179
188
|
const handleRefresh = useCallback(() => {
|
|
180
189
|
void mint().then(setState);
|
|
@@ -187,17 +196,15 @@ export function RemoteEntry({ wide, useWorkspaces, t }) {
|
|
|
187
196
|
const handlePickPublic = useCallback(() => {
|
|
188
197
|
void mint().then(setState);
|
|
189
198
|
}, [mint]);
|
|
190
|
-
const handleCopy = useCallback(() => {
|
|
191
|
-
|
|
192
|
-
return;
|
|
193
|
-
void copyText(state.url).then((ok) => {
|
|
199
|
+
const handleCopy = useCallback((target, url) => {
|
|
200
|
+
void copyText(url).then((ok) => {
|
|
194
201
|
if (!ok)
|
|
195
202
|
return;
|
|
196
|
-
setCopied(
|
|
197
|
-
window.setTimeout(() => { setCopied(
|
|
203
|
+
setCopied(target);
|
|
204
|
+
window.setTimeout(() => { setCopied(undefined); }, 1500);
|
|
198
205
|
});
|
|
199
|
-
}, [
|
|
200
|
-
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)] }));
|
|
206
|
+
}, []);
|
|
207
|
+
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, onRevoke: handleRevoke })] })), document.body)] }));
|
|
201
208
|
}
|
|
202
209
|
/** The trigger: an icon-only control with a persistent accessible label. */
|
|
203
210
|
function TooltipAnchor({ wide, label, onClick }) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
2
|
import type { PairingPhase } from '../pairing.ts';
|
|
3
|
-
import { type TunnelStatusFrame } from './pair-api.ts';
|
|
3
|
+
import { type DeviceFrame, type PostureFrame, type TunnelStatusFrame } from './pair-api.ts';
|
|
4
4
|
/** The panel's view state, owned by the entry component. */
|
|
5
5
|
export type PanelState = {
|
|
6
6
|
kind: 'lan-required';
|
|
@@ -17,6 +17,8 @@ export type PanelState = {
|
|
|
17
17
|
phase: PairingPhase;
|
|
18
18
|
deviceCount: number;
|
|
19
19
|
onlineCount: number;
|
|
20
|
+
/** Authorized devices for the roster under the QR card. */
|
|
21
|
+
devices: DeviceFrame[];
|
|
20
22
|
/** The LAN literal the current QR was built from. */
|
|
21
23
|
address: string;
|
|
22
24
|
/** Every constructible LAN literal (interface order). */
|
|
@@ -27,25 +29,29 @@ export type PanelState = {
|
|
|
27
29
|
publicBaseUrl?: string;
|
|
28
30
|
/** Auto-tunnel status, while the auto-tunnel feature is active. */
|
|
29
31
|
tunnel?: TunnelStatusFrame;
|
|
32
|
+
/** Latest /api posture probe, once a round has completed. */
|
|
33
|
+
posture?: PostureFrame;
|
|
30
34
|
};
|
|
31
35
|
/** Full panel props: copy + view state + actions. */
|
|
32
36
|
export interface RemotePanelProps {
|
|
33
37
|
t: TranslateNS<'remote'>;
|
|
34
38
|
state: PanelState;
|
|
35
|
-
copied:
|
|
39
|
+
copied: 'phone' | 'desktop' | undefined;
|
|
36
40
|
onClose(): void;
|
|
37
41
|
onStop(): void;
|
|
38
42
|
onRefresh(): void;
|
|
39
|
-
onCopy(): void;
|
|
43
|
+
onCopy(target: 'phone' | 'desktop', url: string): void;
|
|
40
44
|
/** Re-mint the QR against a different LAN address. */
|
|
41
45
|
onPickAddress(address: string): void;
|
|
42
46
|
/** Re-mint the QR against the configured public (tunneled) base. */
|
|
43
47
|
onPickPublic(): void;
|
|
48
|
+
/** Revoke one paired device. */
|
|
49
|
+
onRevoke(deviceId: string): void;
|
|
44
50
|
}
|
|
45
51
|
/**
|
|
46
52
|
* Render the pairing panel.
|
|
47
53
|
* @param props - copy, state, and actions.
|
|
48
54
|
* @returns the panel element tree.
|
|
49
55
|
*/
|
|
50
|
-
export declare function RemotePanel({ t, state, copied, onClose, onStop, onRefresh, onCopy, onPickAddress, onPickPublic }: RemotePanelProps): import("react").JSX.Element;
|
|
56
|
+
export declare function RemotePanel({ t, state, copied, onClose, onStop, onRefresh, onCopy, onPickAddress, onPickPublic, onRevoke }: RemotePanelProps): import("react").JSX.Element;
|
|
51
57
|
//# sourceMappingURL=RemotePanel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RemotePanel.d.ts","sourceRoot":"","sources":["../../../src/client/RemotePanel.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,
|
|
1
|
+
{"version":3,"file":"RemotePanel.d.ts","sourceRoot":"","sources":["../../../src/client/RemotePanel.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAIL,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACvB,MAAM,eAAe,CAAA;AAItB,4DAA4D;AAC5D,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,MAAM,CAAC,EAAE,iBAAiB,CAAA;CAAE,GACpD;IAAE,IAAI,EAAE,mBAAmB,CAAA;CAAE,GAC7B;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GACvB;IACE,IAAI,EAAE,OAAO,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,YAAY,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,2DAA2D;IAC3D,OAAO,EAAE,WAAW,EAAE,CAAA;IACtB,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAA;IACf,yDAAyD;IACzD,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,yEAAyE;IACzE,MAAM,EAAE,OAAO,CAAA;IACf,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mEAAmE;IACnE,MAAM,CAAC,EAAE,iBAAiB,CAAA;IAC1B,6DAA6D;IAC7D,OAAO,CAAC,EAAE,YAAY,CAAA;CACvB,CAAA;AAEL,qDAAqD;AACrD,MAAM,WAAW,gBAAgB;IAC/B,CAAC,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IACxB,KAAK,EAAE,UAAU,CAAA;IACjB,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAAA;IACvC,OAAO,IAAI,IAAI,CAAA;IACf,MAAM,IAAI,IAAI,CAAA;IACd,SAAS,IAAI,IAAI,CAAA;IACjB,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IACtD,sDAAsD;IACtD,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACpC,oEAAoE;IACpE,YAAY,IAAI,IAAI,CAAA;IACpB,gCAAgC;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACjC;AAgBD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,QAAQ,EAAE,EAAE,gBAAgB,+BAyK5I"}
|
|
@@ -7,8 +7,9 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
7
7
|
*/
|
|
8
8
|
import clsx from 'clsx';
|
|
9
9
|
import { QRCodeSVG } from 'qrcode.react';
|
|
10
|
-
import { IconCloseOutline16, IconCopyOutline16,
|
|
11
|
-
import { formatClock } from './pair-api.js';
|
|
10
|
+
import { IconCloseOutline16, IconCopyOutline16, IconRefreshOutline16, IconStopFill16, } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
11
|
+
import { desktopPairUrl, formatClock, formatLastSeen, } from './pair-api.js';
|
|
12
|
+
import { deviceNameFromUserAgent } from './device-name.js';
|
|
12
13
|
import css from './remote.module.css';
|
|
13
14
|
/** Badge text + tone per phase (ready states only). */
|
|
14
15
|
function statusOf(t, state) {
|
|
@@ -25,10 +26,10 @@ function statusOf(t, state) {
|
|
|
25
26
|
* @param props - copy, state, and actions.
|
|
26
27
|
* @returns the panel element tree.
|
|
27
28
|
*/
|
|
28
|
-
export function RemotePanel({ t, state, copied, onClose, onStop, onRefresh, onCopy, onPickAddress, onPickPublic }) {
|
|
29
|
-
return (_jsxs("div", { className: css.panel, role: "dialog", "aria-modal": "true", "aria-label": t('title'), children: [_jsxs("div", { className: css.header, children: [_jsxs("div", { className: css.heading, children: [_jsx("h2", { className: css.title, children: t('title') }), _jsx("p", { className: css.subtitle, children: t('subtitle') })] }), _jsx("button", { type: "button", className: css.close, "aria-label": t('close.label'), onClick: onClose, children: _jsx(IconCloseOutline16, { size: 14 }) })] }), state.kind === 'lan-required' ? (_jsxs("div", { className: css.banner, role: "alert", children: [_jsx("p", { className: css.bannerTitle, children: t('status.lanRequired') }), _jsx("p", { className: css.bannerHint, children: t('status.lanRequiredHint') })] })) : state.kind === 'loopback-required' ? (_jsxs("div", { className: css.banner, role: "alert", children: [_jsx("p", { className: css.bannerTitle, children: t('status.loopbackRequired') }), _jsx("p", { className: css.bannerHint, children: t('status.loopbackRequiredHint') })] })) : state.kind === 'unreachable' ? (_jsxs("div", { className: css.banner, role: "alert", children: [_jsx("p", { className: css.bannerTitle, children: t('status.unreachable') }), _jsx("p", { className: css.bannerHint, children: t('status.unreachableHint') })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.card, children: [_jsxs("div", { className: css.cardHeader, children: [_jsx("span", { className: css.cardTitle, children: t('card.title') }), _jsxs("span", { className: css.badges, children: [state.public && _jsx("span", { className: clsx(css.badge, css.badgePublic), children: t('public.badge') }), _jsx("span", { className: clsx(css.badge, css[`badge-${statusOf(t, state).tone}`]), children: statusOf(t, state).text })] })] }), _jsx("div", { className: css.qrWrap, "data-testid": "remote-qr", children: _jsx(QRCodeSVG, { value: state.url, size: 184, level: "M", marginSize: 1, className: css.qr }) }), state.expired
|
|
29
|
+
export function RemotePanel({ t, state, copied, onClose, onStop, onRefresh, onCopy, onPickAddress, onPickPublic, onRevoke }) {
|
|
30
|
+
return (_jsxs("div", { className: css.panel, role: "dialog", "aria-modal": "true", "aria-label": t('title'), children: [_jsxs("div", { className: css.header, children: [_jsxs("div", { className: css.heading, children: [_jsx("h2", { className: css.title, children: t('title') }), _jsx("p", { className: css.subtitle, children: t('subtitle') })] }), _jsx("button", { type: "button", className: css.close, "aria-label": t('close.label'), onClick: onClose, children: _jsx(IconCloseOutline16, { size: 14 }) })] }), state.kind === 'lan-required' ? (_jsxs("div", { className: css.banner, role: "alert", children: [_jsx("p", { className: css.bannerTitle, children: t('status.lanRequired') }), _jsx("p", { className: css.bannerHint, children: t('status.lanRequiredHint') })] })) : state.kind === 'loopback-required' ? (_jsxs("div", { className: css.banner, role: "alert", children: [_jsx("p", { className: css.bannerTitle, children: t('status.loopbackRequired') }), _jsx("p", { className: css.bannerHint, children: t('status.loopbackRequiredHint') })] })) : state.kind === 'unreachable' ? (_jsxs("div", { className: css.banner, role: "alert", children: [_jsx("p", { className: css.bannerTitle, children: t('status.unreachable') }), _jsx("p", { className: css.bannerHint, children: t('status.unreachableHint') })] })) : (_jsxs(_Fragment, { children: [state.posture !== undefined && state.posture.hosts.some(host => host.exposed) && (_jsxs("div", { className: css.banner, role: "alert", children: [_jsx("p", { className: css.bannerTitle, children: t('posture.exposed') }), _jsx("p", { className: css.bannerHint, children: t('posture.exposedHint', { hosts: state.posture.hosts.filter(host => host.exposed).map(host => host.host).join(', ') }) })] })), _jsxs("div", { className: css.card, children: [_jsxs("div", { className: css.cardHeader, children: [_jsx("span", { className: css.cardTitle, children: t('card.title') }), _jsxs("span", { className: css.badges, children: [state.public && _jsx("span", { className: clsx(css.badge, css.badgePublic), children: t('public.badge') }), _jsx("span", { className: clsx(css.badge, css[`badge-${statusOf(t, state).tone}`]), children: statusOf(t, state).text })] })] }), _jsx("div", { className: css.qrWrap, "data-testid": "remote-qr", children: _jsx(QRCodeSVG, { value: state.url, size: 184, level: "M", marginSize: 1, className: css.qr }) }), state.expired
|
|
30
31
|
? _jsx("p", { className: css.expired, children: t('pair.expired') })
|
|
31
|
-
: _jsx("p", { className: css.expiry, children: t('pair.expires', { time: formatClock(state.expiresAt) }) })] }), _jsx("p", { className: css.hint, children: state.public ? t('pair.publicHint') : t('pair.hint') }), _jsx("
|
|
32
|
+
: _jsx("p", { className: css.expiry, children: t('pair.expires', { time: formatClock(state.expiresAt) }) })] }), _jsx("p", { className: css.hint, children: state.public ? t('pair.publicHint') : t('pair.hint') }), _jsxs("div", { className: css.pairLinks, children: [_jsxs("div", { className: css.pairLinkRow, children: [_jsxs("div", { className: css.pairLinkText, children: [_jsx("span", { className: css.pairLinkLabel, children: t('pair.phoneLabel') }), _jsx("code", { className: css.link, title: state.url, children: state.url })] }), _jsxs("button", { type: "button", className: css.copyLink, onClick: () => onCopy('phone', state.url), children: [_jsx(IconCopyOutline16, { size: 14 }), copied === 'phone' ? t('action.copied') : t('action.copyPhone')] })] }), _jsxs("div", { className: css.pairLinkRow, children: [_jsxs("div", { className: css.pairLinkText, children: [_jsx("span", { className: css.pairLinkLabel, children: t('pair.desktopLabel') }), _jsx("code", { className: css.link, title: desktopPairUrl(state.url), children: desktopPairUrl(state.url) })] }), _jsxs("button", { type: "button", className: css.copyLink, onClick: () => onCopy('desktop', desktopPairUrl(state.url)), children: [_jsx(IconCopyOutline16, { size: 14 }), copied === 'desktop' ? t('action.copied') : t('action.copyDesktop')] })] })] }), _jsx("p", { className: css.oneTimeHint, children: t('pair.oneTimeHint') }), state.phase === 'stopped' && _jsx("p", { className: css.stoppedHint, children: t('stopped.hint') }), state.tunnel !== undefined && state.tunnel.state !== 'running' && (_jsx("p", { className: state.tunnel.state === 'failed' ? css.tunnelFailed : css.tunnelNote, role: "status", children: state.tunnel.state === 'failed'
|
|
32
33
|
? t('tunnel.failed', { error: state.tunnel.error ?? t('tunnel.unknownError') })
|
|
33
|
-
: t('tunnel.starting') })), (state.publicBaseUrl !== undefined || state.lanAddresses.length > 1) && (_jsxs("fieldset", { className: css.addresses, children: [_jsx("legend", { children: t('address.label') }), state.publicBaseUrl !== undefined && (_jsxs("label", { className: css.address, children: [_jsx("input", { type: "radio", name: "lan-address", "aria-label": t('address.public'), checked: state.public, onChange: onPickPublic }), _jsx("span", { children: t('address.public') }), _jsx("code", { className: css.addressValue, children: state.publicBaseUrl })] }, "public")), state.lanAddresses.map(address => (_jsxs("label", { className: css.address, children: [_jsx("input", { type: "radio", name: "lan-address", "aria-label": address, checked: !state.public && address === state.address, onChange: () => onPickAddress(address) }), _jsx("span", { children: t('address.lan') }), _jsx("code", { className: css.addressValue, children: address })] }, address))), _jsx("p", { className: css.addressHint, children: t('address.hint') })] })), _jsxs("div", { className: css.actions, children: [_jsxs("button", { type: "button", className: css.action, onClick: onStop, children: [_jsx(IconStopFill16, { size: 14 }), t('action.stop')] }), _jsxs("button", { type: "button", className: css.action, onClick: onRefresh, children: [_jsx(IconRefreshOutline16, { size: 14 }), t('action.refresh')] }), _jsxs("
|
|
34
|
+
: t('tunnel.starting') })), (state.publicBaseUrl !== undefined || state.lanAddresses.length > 1) && (_jsxs("fieldset", { className: css.addresses, children: [_jsx("legend", { children: t('address.label') }), state.publicBaseUrl !== undefined && (_jsxs("label", { className: css.address, children: [_jsx("input", { type: "radio", name: "lan-address", "aria-label": t('address.public'), checked: state.public, onChange: onPickPublic }), _jsx("span", { children: t('address.public') }), _jsx("code", { className: css.addressValue, children: state.publicBaseUrl })] }, "public")), state.lanAddresses.map(address => (_jsxs("label", { className: css.address, children: [_jsx("input", { type: "radio", name: "lan-address", "aria-label": address, checked: !state.public && address === state.address, onChange: () => onPickAddress(address) }), _jsx("span", { children: t('address.lan') }), _jsx("code", { className: css.addressValue, children: address })] }, address))), _jsx("p", { className: css.addressHint, children: t('address.hint') })] })), _jsxs("div", { className: css.actions, children: [_jsxs("button", { type: "button", className: css.action, onClick: onStop, children: [_jsx(IconStopFill16, { size: 14 }), t('action.stop')] }), _jsxs("button", { type: "button", className: css.action, onClick: onRefresh, children: [_jsx(IconRefreshOutline16, { size: 14 }), t('action.refresh')] })] }), _jsxs("section", { className: css.devices, "data-testid": "remote-devices", "aria-label": t('devices.title'), children: [_jsx("h3", { className: css.devicesTitle, children: t('devices.title') }), state.devices.length === 0 ? (_jsx("p", { className: css.devicesEmpty, children: t('devices.empty') })) : (_jsx("ul", { className: css.deviceList, children: state.devices.map(device => (_jsxs("li", { className: css.deviceRow, children: [_jsxs("div", { className: css.deviceMeta, children: [_jsx("span", { className: css.deviceName, children: deviceNameFromUserAgent(device.userAgent) ?? t('devices.unknown') }), _jsx("span", { className: clsx(css.devicePresence, device.online ? css.deviceOnline : css.deviceOffline), children: device.online ? t('devices.online') : t('devices.offline') }), _jsx("span", { className: css.deviceSeen, children: t('devices.lastSeen', { time: formatLastSeen(device.lastSeenAt) }) })] }), _jsx("button", { type: "button", className: css.deviceRevoke, "aria-label": t('devices.revoke.label'), onClick: () => { onRevoke(device.id); }, children: t('devices.revoke') })] }, device.id))) }))] })] }))] }));
|
|
34
35
|
}
|
|
@@ -16,6 +16,8 @@ export interface RemoteSettings {
|
|
|
16
16
|
offlineAfterMs?: number;
|
|
17
17
|
/** Hard cap on paired device sessions (oldest evicted when full). */
|
|
18
18
|
maxDevices?: number;
|
|
19
|
+
/** Idle sessions older than this (ms) are deleted. */
|
|
20
|
+
idleExpireMs?: number;
|
|
19
21
|
/** Cookie name carrying the paired device id. */
|
|
20
22
|
cookieName?: string;
|
|
21
23
|
/** Fence flag: whether non-loopback /api requests must carry a live paired-device cookie. */
|
|
@@ -37,6 +39,8 @@ export interface RemoteSettingsCardState extends CardShell {
|
|
|
37
39
|
offlineAfterMs: CardFieldState;
|
|
38
40
|
/** Paired-device cap. */
|
|
39
41
|
maxDevices: CardFieldState;
|
|
42
|
+
/** Idle-expiry window. */
|
|
43
|
+
idleExpireMs: CardFieldState;
|
|
40
44
|
/** Device cookie name. */
|
|
41
45
|
cookieName: CardFieldState;
|
|
42
46
|
/** LAN fence flag. */
|
|
@@ -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,
|
|
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,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,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,YAAY,EAAE,cAAc,CAAA;IAC5B,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,EAc/C;IAED,OAAO,CAAC,UAAU;IAgBlB;;;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,+BAiIhE"}
|
|
@@ -12,6 +12,7 @@ export class RemoteSettingsCardController {
|
|
|
12
12
|
numberField('tokenTtlMs'),
|
|
13
13
|
numberField('offlineAfterMs'),
|
|
14
14
|
numberField('maxDevices'),
|
|
15
|
+
numberField('idleExpireMs'),
|
|
15
16
|
textField('cookieName'),
|
|
16
17
|
booleanField('requirePairingForLan'),
|
|
17
18
|
textField('publicBaseUrl'),
|
|
@@ -27,6 +28,7 @@ export class RemoteSettingsCardController {
|
|
|
27
28
|
tokenTtlMs: this.form.field('tokenTtlMs'),
|
|
28
29
|
offlineAfterMs: this.form.field('offlineAfterMs'),
|
|
29
30
|
maxDevices: this.form.field('maxDevices'),
|
|
31
|
+
idleExpireMs: this.form.field('idleExpireMs'),
|
|
30
32
|
cookieName: this.form.field('cookieName'),
|
|
31
33
|
requirePairingForLan: this.form.field('requirePairingForLan'),
|
|
32
34
|
publicBaseUrl: this.form.field('publicBaseUrl'),
|
|
@@ -64,5 +66,5 @@ export function RemoteSettingsCard(props) {
|
|
|
64
66
|
invalidLabel: t('settings.invalidNumber'),
|
|
65
67
|
disabled,
|
|
66
68
|
};
|
|
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'); } })] }));
|
|
69
|
+
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-idle-expire", label: t('settings.idleExpireMs'), hint: t('settings.idleExpireMsHint'), numeric: true, ...fieldProps, ...state.idleExpireMs, onEdit: (text) => { props.edit('idleExpireMs', text); }, onReset: () => { props.resetField('idleExpireMs'); } }), _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
70
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-name.d.ts","sourceRoot":"","sources":["../../../src/client/device-name.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,wBAAgB,uBAAuB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CA6B9E"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Derive a short, non-sensitive device label from a browser User-Agent. */
|
|
2
|
+
export function deviceNameFromUserAgent(userAgent) {
|
|
3
|
+
if (userAgent === undefined || userAgent.trim() === '')
|
|
4
|
+
return undefined;
|
|
5
|
+
const os = /Windows NT/i.test(userAgent)
|
|
6
|
+
? 'Windows'
|
|
7
|
+
: /Android/i.test(userAgent)
|
|
8
|
+
? 'Android'
|
|
9
|
+
: /iPhone|iPad|iPod/i.test(userAgent)
|
|
10
|
+
? 'iOS'
|
|
11
|
+
: /Macintosh|Mac OS X/i.test(userAgent)
|
|
12
|
+
? 'macOS'
|
|
13
|
+
: /Linux/i.test(userAgent)
|
|
14
|
+
? 'Linux'
|
|
15
|
+
: undefined;
|
|
16
|
+
const browser = /Edg(?:A|iOS)?\//i.test(userAgent)
|
|
17
|
+
? 'Edge'
|
|
18
|
+
: /(?:OPR|Opera)\//i.test(userAgent)
|
|
19
|
+
? 'Opera'
|
|
20
|
+
: /(?:Chrome|CriOS)\//i.test(userAgent)
|
|
21
|
+
? 'Chrome'
|
|
22
|
+
: /(?:Firefox|FxiOS)\//i.test(userAgent)
|
|
23
|
+
? 'Firefox'
|
|
24
|
+
: /Safari\//i.test(userAgent) && /Version\//i.test(userAgent)
|
|
25
|
+
? 'Safari'
|
|
26
|
+
: undefined;
|
|
27
|
+
if (os !== undefined && browser !== undefined)
|
|
28
|
+
return `${os} · ${browser}`;
|
|
29
|
+
return os ?? browser;
|
|
30
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAA;AAa7G,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAA;AAa7G,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAMrD,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACrE,YAAY,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AACnE,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAC7C,YAAY,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAC/F,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACzD,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAErE,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,0CAA0C;QAC1C,MAAM,EAAE,SAAS,CAAA;KAClB;IAED,UAAU,OAAO;QACf;;;;WAIG;QACH,gBAAgB,EAAE;YAAE,IAAI,EAAE,QAAQ,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,uBAAuB,CAAA;SAAE,CAAA;QACnF;;;;;WAKG;QACH,oBAAoB,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,4BAA4B,CAAA;SAAE,CAAA;KAC3F;CACF;AAED,4GAA4G;AAC5G,MAAM,WAAW,uBAAuB;IACtC,oEAAoE;IACpE,IAAI,EAAE,OAAO,CAAA;CACd;AAED,mEAAmE;AACnE,MAAM,WAAW,4BAA4B;IAC3C,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,KAAK,CAAA;CACjB;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAYD,wCAAwC;AACxC,eAAO,MAAM,MAAM,UAA+D,CAAA;AAElF;;;GAGG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CA4J9C"}
|
|
@@ -15,6 +15,8 @@ import { RemoteSettingsCard, RemoteSettingsCardController } from './RemoteSettin
|
|
|
15
15
|
import { en, zh } from './locales.js';
|
|
16
16
|
import { PAIR_FAILED_MARKER, runPairBootFlow } from './deep-link.js';
|
|
17
17
|
import { sendHeartbeat } from './pair-api.js';
|
|
18
|
+
import { installRemoteChannel, isLoopbackHostname } from './remote-channel.js';
|
|
19
|
+
import { FenceNotice } from './FenceNotice.js';
|
|
18
20
|
/** Dictionary namespace owned by this plugin. */
|
|
19
21
|
const NS = 'remote';
|
|
20
22
|
/** Settings namespace the remote-control card edits (the Host plugin registers it). */
|
|
@@ -122,6 +124,52 @@ export function apply(ctx) {
|
|
|
122
124
|
};
|
|
123
125
|
settingsScope.subscribe(syncRuntime);
|
|
124
126
|
syncRuntime();
|
|
127
|
+
// Remote desktop channel: on a non-loopback origin (LAN address or public
|
|
128
|
+
// tunnel) the connection plugin's /api fence refuses this desktop Web GUI,
|
|
129
|
+
// and pairing is the access control — so the SDK client's /api traffic is
|
|
130
|
+
// rewritten onto this plugin's gated /remote/api prefix (remote-channel.ts)
|
|
131
|
+
// while the fence setting demands it. Loopback origins are untouched.
|
|
132
|
+
let disposeChannel;
|
|
133
|
+
let fenceNotice;
|
|
134
|
+
const showFenceNotice = () => {
|
|
135
|
+
if (fenceNotice !== undefined)
|
|
136
|
+
return;
|
|
137
|
+
const node = document.createElement('div');
|
|
138
|
+
document.body.appendChild(node);
|
|
139
|
+
const root = createRoot(node);
|
|
140
|
+
root.render(createElement(FenceNotice, { t, onRetry: () => { window.location.reload(); } }));
|
|
141
|
+
fenceNotice = { unmount: () => { root.unmount(); node.remove(); }, node };
|
|
142
|
+
};
|
|
143
|
+
const hideFenceNotice = () => {
|
|
144
|
+
fenceNotice?.unmount();
|
|
145
|
+
fenceNotice = undefined;
|
|
146
|
+
};
|
|
147
|
+
const channelActive = () => {
|
|
148
|
+
if (isLoopbackHostname(window.location.hostname))
|
|
149
|
+
return false;
|
|
150
|
+
// A not-yet-loaded (or unavailable) settings snapshot falls back to the
|
|
151
|
+
// schema defaults — enabled and requirePairingForLan are both true, and
|
|
152
|
+
// on a remote origin the snapshot may never load (its own transport is
|
|
153
|
+
// what the channel gates), so waiting for 'ready' here would deadlock
|
|
154
|
+
// the channel off exactly where it is needed most.
|
|
155
|
+
const snapshot = settingsScope.getSnapshot();
|
|
156
|
+
const value = snapshot.status === 'ready' ? snapshot.value : undefined;
|
|
157
|
+
return (value?.enabled ?? true) && (value?.requirePairingForLan ?? true);
|
|
158
|
+
};
|
|
159
|
+
const syncChannel = () => {
|
|
160
|
+
if (channelActive() && disposeChannel === undefined) {
|
|
161
|
+
disposeChannel = ctx.effect(() => {
|
|
162
|
+
const restore = installRemoteChannel(window, { onUnpaired: showFenceNotice, onPaired: hideFenceNotice });
|
|
163
|
+
return restore;
|
|
164
|
+
}, 'remote-web-ui: remote desktop channel');
|
|
165
|
+
}
|
|
166
|
+
else if (!channelActive() && disposeChannel !== undefined) {
|
|
167
|
+
disposeChannel();
|
|
168
|
+
disposeChannel = undefined;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
settingsScope.subscribe(syncChannel);
|
|
172
|
+
syncChannel();
|
|
125
173
|
// One-time failed-pair toast. The accept result lands asynchronously, so
|
|
126
174
|
// the marker check is deferred past the accept round trip.
|
|
127
175
|
ctx.effect(() => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** `remote` namespace dictionaries: the
|
|
1
|
+
/** `remote` namespace dictionaries: the remote-access surface copy. */
|
|
2
2
|
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
3
3
|
export declare const zh: {
|
|
4
4
|
'entry.label': string;
|
|
@@ -24,12 +24,35 @@ export declare const zh: {
|
|
|
24
24
|
'address.hint': string;
|
|
25
25
|
'pair.expires': string;
|
|
26
26
|
'pair.expired': string;
|
|
27
|
+
'pair.phoneLabel': string;
|
|
28
|
+
'pair.desktopLabel': string;
|
|
29
|
+
'pair.oneTimeHint': string;
|
|
27
30
|
'pair.failed.title': string;
|
|
28
31
|
'pair.failed.detail': string;
|
|
32
|
+
'fence.unpaired.title': string;
|
|
33
|
+
'fence.unpaired.eyebrow': string;
|
|
34
|
+
'fence.unpaired.hint': string;
|
|
35
|
+
'fence.unpaired.stepDesktop': string;
|
|
36
|
+
'fence.unpaired.stepLink': string;
|
|
37
|
+
'fence.unpaired.stepOpen': string;
|
|
38
|
+
'fence.unpaired.retry': string;
|
|
39
|
+
'fence.unpaired.footnote': string;
|
|
40
|
+
'posture.exposed': string;
|
|
41
|
+
'posture.exposedHint': string;
|
|
29
42
|
'action.stop': string;
|
|
30
43
|
'action.refresh': string;
|
|
31
44
|
'action.copy': string;
|
|
45
|
+
'action.copyPhone': string;
|
|
46
|
+
'action.copyDesktop': string;
|
|
32
47
|
'action.copied': string;
|
|
48
|
+
'devices.title': string;
|
|
49
|
+
'devices.empty': string;
|
|
50
|
+
'devices.unknown': string;
|
|
51
|
+
'devices.online': string;
|
|
52
|
+
'devices.offline': string;
|
|
53
|
+
'devices.lastSeen': string;
|
|
54
|
+
'devices.revoke': string;
|
|
55
|
+
'devices.revoke.label': string;
|
|
33
56
|
'stopped.hint': string;
|
|
34
57
|
'tunnel.starting': string;
|
|
35
58
|
'tunnel.failed': string;
|
|
@@ -45,6 +68,8 @@ export declare const zh: {
|
|
|
45
68
|
'settings.offlineAfterMsHint': string;
|
|
46
69
|
'settings.maxDevices': string;
|
|
47
70
|
'settings.maxDevicesHint': string;
|
|
71
|
+
'settings.idleExpireMs': string;
|
|
72
|
+
'settings.idleExpireMsHint': string;
|
|
48
73
|
'settings.cookieName': string;
|
|
49
74
|
'settings.cookieNameHint': string;
|
|
50
75
|
'settings.requirePairingForLan': string;
|
|
@@ -135,12 +160,35 @@ export declare const en: {
|
|
|
135
160
|
'address.hint': string;
|
|
136
161
|
'pair.expires': string;
|
|
137
162
|
'pair.expired': string;
|
|
163
|
+
'pair.phoneLabel': string;
|
|
164
|
+
'pair.desktopLabel': string;
|
|
165
|
+
'pair.oneTimeHint': string;
|
|
138
166
|
'pair.failed.title': string;
|
|
139
167
|
'pair.failed.detail': string;
|
|
168
|
+
'fence.unpaired.title': string;
|
|
169
|
+
'fence.unpaired.eyebrow': string;
|
|
170
|
+
'fence.unpaired.hint': string;
|
|
171
|
+
'fence.unpaired.stepDesktop': string;
|
|
172
|
+
'fence.unpaired.stepLink': string;
|
|
173
|
+
'fence.unpaired.stepOpen': string;
|
|
174
|
+
'fence.unpaired.retry': string;
|
|
175
|
+
'fence.unpaired.footnote': string;
|
|
176
|
+
'posture.exposed': string;
|
|
177
|
+
'posture.exposedHint': string;
|
|
140
178
|
'action.stop': string;
|
|
141
179
|
'action.refresh': string;
|
|
142
180
|
'action.copy': string;
|
|
181
|
+
'action.copyPhone': string;
|
|
182
|
+
'action.copyDesktop': string;
|
|
143
183
|
'action.copied': string;
|
|
184
|
+
'devices.title': string;
|
|
185
|
+
'devices.empty': string;
|
|
186
|
+
'devices.unknown': string;
|
|
187
|
+
'devices.online': string;
|
|
188
|
+
'devices.offline': string;
|
|
189
|
+
'devices.lastSeen': string;
|
|
190
|
+
'devices.revoke': string;
|
|
191
|
+
'devices.revoke.label': string;
|
|
144
192
|
'stopped.hint': string;
|
|
145
193
|
'tunnel.starting': string;
|
|
146
194
|
'tunnel.failed': string;
|
|
@@ -156,6 +204,8 @@ export declare const en: {
|
|
|
156
204
|
'settings.offlineAfterMsHint': string;
|
|
157
205
|
'settings.maxDevices': string;
|
|
158
206
|
'settings.maxDevicesHint': string;
|
|
207
|
+
'settings.idleExpireMs': string;
|
|
208
|
+
'settings.idleExpireMsHint': string;
|
|
159
209
|
'settings.cookieName': string;
|
|
160
210
|
'settings.cookieNameHint': string;
|
|
161
211
|
'settings.requirePairingForLan': string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoImB,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoIsB,CAAA"}
|