@linxin666/dsh-remote-web-ui 0.2.3 → 0.2.5
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 +610 -74
- package/lib/client.js.map +1 -1
- package/lib/index.js +676 -35
- package/lib/mobile.js +10 -7
- 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 +83 -33
- 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/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +9 -6
- 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/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +10 -2
- package/package.json +13 -13
- 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 +83 -33
- 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-form.ts +8 -6
- 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
- package/src/update.ts +14 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The remote desktop data channel: `/remote` is this plugin's own prefix, so
|
|
3
|
+
* the paired-device cookie is the access control (exactly like `/m/api`).
|
|
4
|
+
* After that gate, every fenced same-origin path the browser rewrote here is
|
|
5
|
+
* re-issued to 127.0.0.1 as a loopback-shaped request so sibling plugin
|
|
6
|
+
* fences (and the connection plugin's `/api`) accept it — no `--trusted-host`
|
|
7
|
+
* and no per-plugin pairing consult.
|
|
8
|
+
*
|
|
9
|
+
* Security model:
|
|
10
|
+
* - Every request must carry a live paired-device cookie, enforced before
|
|
11
|
+
* any bytes are forwarded and before any host call.
|
|
12
|
+
* - The SDK's loopback-only privileged methods (native dialogs, the settings
|
|
13
|
+
* plane, credentials — the `PRIVILEGED_METHODS` set of client-connection)
|
|
14
|
+
* are denied here. The set is pinned by tests/remote-contract.spec.ts.
|
|
15
|
+
* - `/api/pair/*`, `/api/update/*`, `/api/plugin-manager/*`,
|
|
16
|
+
* `/api/dsh-desktop-launcher/*` and `/api/dsh-web-ui-settings/*` stay physically local.
|
|
17
|
+
* - Everything else is HTTP- or WebSocket-proxied to the local port with
|
|
18
|
+
* Host rewritten, Origin and cookies dropped, and a synthetic same-origin
|
|
19
|
+
* browser marker added after authentication. Plugin loopback fences then
|
|
20
|
+
* pass. The pairing cookie never leaves this process.
|
|
21
|
+
*/
|
|
22
|
+
import { readCookie } from './gate.js';
|
|
23
|
+
import { writeJson } from './http.js';
|
|
24
|
+
import { proxyLoopbackHttp, proxyLoopbackUpgrade } from './loopback-proxy.js';
|
|
25
|
+
import { DESKTOP_LAUNCHER_PATH, LOOPBACK_ONLY_METHODS, PLUGIN_MANAGER_PATH, REMOTE_PREFIX, REMOTE_UPGRADE_PATHS, WEB_UI_SETTINGS_BRIDGE_PATH, } from './remote-methods.js';
|
|
26
|
+
export { DESKTOP_LAUNCHER_PATH, LOOPBACK_ONLY_METHODS, PLUGIN_MANAGER_PATH, REMOTE_API_PATHS, REMOTE_PREFIX, REMOTE_UPGRADE_PATHS, WEB_UI_SETTINGS_BRIDGE_PATH, } from './remote-methods.js';
|
|
27
|
+
export { REMOTE_API_PREFIX } from './remote-methods.js';
|
|
28
|
+
const ALLOWED_METHODS = new Set(['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']);
|
|
29
|
+
/** Reject traversal and empty segments; allow plugin file-path characters. */
|
|
30
|
+
function isSafeSegment(segment) {
|
|
31
|
+
if (segment === '')
|
|
32
|
+
return false;
|
|
33
|
+
let decoded;
|
|
34
|
+
try {
|
|
35
|
+
decoded = decodeURIComponent(segment);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return decoded !== '.' && decoded !== '..' && !decoded.includes('/') && !decoded.includes('\\') && !decoded.includes('\0');
|
|
41
|
+
}
|
|
42
|
+
/** One SDK-shaped error envelope (keeps the desktop client's parse path intact). */
|
|
43
|
+
function envelopeError(res, status, rpcId, code, message) {
|
|
44
|
+
writeJson(res, status, {
|
|
45
|
+
type: 'server-response',
|
|
46
|
+
rpcId,
|
|
47
|
+
result: { ok: false, error: { code, message, details: { issues: [] } } },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Map `/remote/...` to the inner path, or undefined when the outer path is
|
|
52
|
+
* not a safe rewrite target.
|
|
53
|
+
*/
|
|
54
|
+
export function innerPathOf(pathname) {
|
|
55
|
+
if (pathname === REMOTE_PREFIX || pathname === `${REMOTE_PREFIX}/`)
|
|
56
|
+
return undefined;
|
|
57
|
+
if (!pathname.startsWith(`${REMOTE_PREFIX}/`))
|
|
58
|
+
return undefined;
|
|
59
|
+
const rest = pathname.slice(REMOTE_PREFIX.length);
|
|
60
|
+
if (!rest.startsWith('/'))
|
|
61
|
+
return undefined;
|
|
62
|
+
const segments = rest.slice(1).split('/');
|
|
63
|
+
if (segments.length === 0 || segments.some(segment => !isSafeSegment(segment))) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
return rest;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Whether a paired inner path must stay physically local.
|
|
70
|
+
* @returns a denial message, or undefined when the path may be proxied.
|
|
71
|
+
*/
|
|
72
|
+
export function loopbackOnlyDenial(innerPath) {
|
|
73
|
+
if (innerPath === '/api/pair' || innerPath.startsWith('/api/pair/')) {
|
|
74
|
+
return 'pairing endpoints stay loopback-only and stay unreachable from a paired remote desktop';
|
|
75
|
+
}
|
|
76
|
+
if (innerPath === '/api/update' || innerPath.startsWith('/api/update/')) {
|
|
77
|
+
return 'update endpoints stay loopback-only and stay unreachable from a paired remote desktop';
|
|
78
|
+
}
|
|
79
|
+
if (innerPath === PLUGIN_MANAGER_PATH || innerPath.startsWith(`${PLUGIN_MANAGER_PATH}/`)) {
|
|
80
|
+
return 'plugin-manager stays loopback-only and stays unreachable from a paired remote desktop';
|
|
81
|
+
}
|
|
82
|
+
if (innerPath === DESKTOP_LAUNCHER_PATH || innerPath.startsWith(`${DESKTOP_LAUNCHER_PATH}/`)) {
|
|
83
|
+
return 'desktop-launcher endpoints stay loopback-only and stay unreachable from a paired remote desktop';
|
|
84
|
+
}
|
|
85
|
+
if (innerPath === WEB_UI_SETTINGS_BRIDGE_PATH || innerPath.startsWith(`${WEB_UI_SETTINGS_BRIDGE_PATH}/`)) {
|
|
86
|
+
return 'settings-bridge endpoints stay loopback-only and stay unreachable from a paired remote desktop';
|
|
87
|
+
}
|
|
88
|
+
if (!innerPath.startsWith('/api/'))
|
|
89
|
+
return undefined;
|
|
90
|
+
const method = innerPath.slice('/api/'.length);
|
|
91
|
+
if (method !== '' && !method.includes('/') && LOOPBACK_ONLY_METHODS.has(method)) {
|
|
92
|
+
return `${method} is loopback-only and stays unreachable from a paired remote desktop`;
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Build the remote desktop channel HTTP routes.
|
|
98
|
+
* @param deps - pairing service + local port.
|
|
99
|
+
* @returns the routes to register on webServer.
|
|
100
|
+
*/
|
|
101
|
+
export function makeRemoteApiRoutes(deps) {
|
|
102
|
+
const { service, port } = deps;
|
|
103
|
+
const handler = (req, res) => {
|
|
104
|
+
// Cookie gate first — same order as /m/api. Do not buffer an unpaired body.
|
|
105
|
+
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
106
|
+
const paired = deviceId !== undefined && service.touchDevice(deviceId);
|
|
107
|
+
if (!paired) {
|
|
108
|
+
req.resume();
|
|
109
|
+
envelopeError(res, 403, 'invalid-request', 'unpaired', 'this device is not paired with the desktop');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const method = req.method ?? 'GET';
|
|
113
|
+
if (!ALLOWED_METHODS.has(method)) {
|
|
114
|
+
req.resume();
|
|
115
|
+
res.writeHead(405).end();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
119
|
+
const inner = innerPathOf(url.pathname);
|
|
120
|
+
if (inner === undefined) {
|
|
121
|
+
req.resume();
|
|
122
|
+
res.writeHead(404).end();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const denied = loopbackOnlyDenial(inner);
|
|
126
|
+
if (denied !== undefined) {
|
|
127
|
+
req.resume();
|
|
128
|
+
envelopeError(res, 403, 'invalid-request', 'forbidden', denied);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
proxyLoopbackHttp(req, res, port, `${inner}${url.search}`);
|
|
132
|
+
};
|
|
133
|
+
return [{ kind: 'prefix', path: REMOTE_PREFIX, handler }];
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Map one outer upgrade URL onto the loopback path (query string included).
|
|
137
|
+
*/
|
|
138
|
+
export function upgradeInnerPath(reqUrl, fallbackPath) {
|
|
139
|
+
if (reqUrl === undefined || reqUrl === '')
|
|
140
|
+
return fallbackPath;
|
|
141
|
+
let url;
|
|
142
|
+
try {
|
|
143
|
+
url = new URL(reqUrl, 'http://127.0.0.1');
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return fallbackPath;
|
|
147
|
+
}
|
|
148
|
+
const inner = innerPathOf(url.pathname);
|
|
149
|
+
if (inner === undefined)
|
|
150
|
+
return fallbackPath;
|
|
151
|
+
return `${inner}${url.search}`;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Build the WebSocket upgrade routes for the event streams and known plugin
|
|
155
|
+
* sockets. webServer matches upgrades by exact path.
|
|
156
|
+
* @param deps - pairing service + local port.
|
|
157
|
+
* @returns the upgrade routes to register on webServer.
|
|
158
|
+
*/
|
|
159
|
+
export function makeRemoteApiUpgradeRoutes(deps) {
|
|
160
|
+
const { service, port } = deps;
|
|
161
|
+
const handlerFor = (fallbackPath) => (req, socket, head) => {
|
|
162
|
+
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
163
|
+
if (deviceId === undefined || !service.touchDevice(deviceId)) {
|
|
164
|
+
socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
|
|
165
|
+
socket.destroy();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const inner = upgradeInnerPath(req.url, fallbackPath);
|
|
169
|
+
const denied = loopbackOnlyDenial(inner.split('?')[0] ?? inner);
|
|
170
|
+
if (denied !== undefined) {
|
|
171
|
+
socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
|
|
172
|
+
socket.destroy();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
proxyLoopbackUpgrade(req, socket, head, port, inner);
|
|
176
|
+
};
|
|
177
|
+
return REMOTE_UPGRADE_PATHS.map((path) => ({
|
|
178
|
+
path,
|
|
179
|
+
handler: handlerFor(path.slice(REMOTE_PREFIX.length)),
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote desktop channel constants — SDK-independent so tests and the
|
|
3
|
+
* client half can pin them without importing the host SDK graph.
|
|
4
|
+
*/
|
|
5
|
+
/** Gated mirror of same-origin fenced paths (`/remote` + original pathname). */
|
|
6
|
+
export declare const REMOTE_PREFIX = "/remote";
|
|
7
|
+
/** Connection-plugin method prefix under the gated channel. */
|
|
8
|
+
export declare const REMOTE_API_PREFIX = "/remote/api";
|
|
9
|
+
/** WebSocket event-stream paths served by the channel (client rewrites to these). */
|
|
10
|
+
export declare const REMOTE_API_PATHS: {
|
|
11
|
+
readonly mux: "/remote/api/events.mux";
|
|
12
|
+
readonly host: "/remote/api/events.host";
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Exact upgrade paths registered on webServer (the SDK matches upgrades by
|
|
16
|
+
* exact path, not prefix). Query strings ride on the request URL.
|
|
17
|
+
*/
|
|
18
|
+
export declare const REMOTE_UPGRADE_PATHS: readonly ["/remote/api/events.mux", "/remote/api/events.host", "/remote/sidebar/ws/terminal", "/remote/sidebar/ws/agent-terminals", "/remote/api/dsh-ssh/terminal"];
|
|
19
|
+
/** Plugin-manager HTTP prefix: install/remove stay physically local. */
|
|
20
|
+
export declare const PLUGIN_MANAGER_PATH = "/api/plugin-manager";
|
|
21
|
+
/** Desktop-launcher HTTP prefix: shortcut create and host shutdown stay physically local. */
|
|
22
|
+
export declare const DESKTOP_LAUNCHER_PATH = "/api/dsh-desktop-launcher";
|
|
23
|
+
/** Family settings-bridge HTTP prefix: describe/mutate stay physically local. */
|
|
24
|
+
export declare const WEB_UI_SETTINGS_BRIDGE_PATH = "/api/dsh-web-ui-settings";
|
|
25
|
+
/**
|
|
26
|
+
* Loopback-only methods of the host API surface, mirrored from
|
|
27
|
+
* client-connection's `PRIVILEGED_METHODS` (pinned by
|
|
28
|
+
* tests/remote-contract.spec.ts against the installed SDK). They stay
|
|
29
|
+
* unreachable from a paired remote desktop, matching the SDK's own stance
|
|
30
|
+
* that the configuration plane is loopback-same-origin only.
|
|
31
|
+
*/
|
|
32
|
+
export declare const LOOPBACK_ONLY_METHODS: Set<string>;
|
|
33
|
+
//# sourceMappingURL=remote-methods.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-methods.d.ts","sourceRoot":"","sources":["../../src/remote-methods.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,gFAAgF;AAChF,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,gBAAyB,CAAA;AAEvD,qFAAqF;AACrF,eAAO,MAAM,gBAAgB;aAC3B,GAAG;aACH,IAAI;CACI,CAAA;AAEV;;;GAGG;AACH,eAAO,MAAM,oBAAoB,qKAMvB,CAAA;AAEV,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,wBAAwB,CAAA;AAExD,6FAA6F;AAC7F,eAAO,MAAM,qBAAqB,8BAA8B,CAAA;AAEhE,iFAAiF;AACjF,eAAO,MAAM,2BAA2B,6BAA6B,CAAA;AAErE;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,aAgBhC,CAAA"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote desktop channel constants — SDK-independent so tests and the
|
|
3
|
+
* client half can pin them without importing the host SDK graph.
|
|
4
|
+
*/
|
|
5
|
+
/** Gated mirror of same-origin fenced paths (`/remote` + original pathname). */
|
|
6
|
+
export const REMOTE_PREFIX = '/remote';
|
|
7
|
+
/** Connection-plugin method prefix under the gated channel. */
|
|
8
|
+
export const REMOTE_API_PREFIX = `${REMOTE_PREFIX}/api`;
|
|
9
|
+
/** WebSocket event-stream paths served by the channel (client rewrites to these). */
|
|
10
|
+
export const REMOTE_API_PATHS = {
|
|
11
|
+
mux: `${REMOTE_API_PREFIX}/events.mux`,
|
|
12
|
+
host: `${REMOTE_API_PREFIX}/events.host`,
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Exact upgrade paths registered on webServer (the SDK matches upgrades by
|
|
16
|
+
* exact path, not prefix). Query strings ride on the request URL.
|
|
17
|
+
*/
|
|
18
|
+
export const REMOTE_UPGRADE_PATHS = [
|
|
19
|
+
REMOTE_API_PATHS.mux,
|
|
20
|
+
REMOTE_API_PATHS.host,
|
|
21
|
+
`${REMOTE_PREFIX}/sidebar/ws/terminal`,
|
|
22
|
+
`${REMOTE_PREFIX}/sidebar/ws/agent-terminals`,
|
|
23
|
+
`${REMOTE_API_PREFIX}/dsh-ssh/terminal`,
|
|
24
|
+
];
|
|
25
|
+
/** Plugin-manager HTTP prefix: install/remove stay physically local. */
|
|
26
|
+
export const PLUGIN_MANAGER_PATH = '/api/plugin-manager';
|
|
27
|
+
/** Desktop-launcher HTTP prefix: shortcut create and host shutdown stay physically local. */
|
|
28
|
+
export const DESKTOP_LAUNCHER_PATH = '/api/dsh-desktop-launcher';
|
|
29
|
+
/** Family settings-bridge HTTP prefix: describe/mutate stay physically local. */
|
|
30
|
+
export const WEB_UI_SETTINGS_BRIDGE_PATH = '/api/dsh-web-ui-settings';
|
|
31
|
+
/**
|
|
32
|
+
* Loopback-only methods of the host API surface, mirrored from
|
|
33
|
+
* client-connection's `PRIVILEGED_METHODS` (pinned by
|
|
34
|
+
* tests/remote-contract.spec.ts against the installed SDK). They stay
|
|
35
|
+
* unreachable from a paired remote desktop, matching the SDK's own stance
|
|
36
|
+
* that the configuration plane is loopback-same-origin only.
|
|
37
|
+
*/
|
|
38
|
+
export const LOOPBACK_ONLY_METHODS = new Set([
|
|
39
|
+
'agentPreset.read',
|
|
40
|
+
'agentPreset.copy',
|
|
41
|
+
'agentPreset.openDocument',
|
|
42
|
+
'agentPreset.remove',
|
|
43
|
+
'host.pickDirectory',
|
|
44
|
+
'host.openPath',
|
|
45
|
+
'settings.describe',
|
|
46
|
+
'settings.openDocument',
|
|
47
|
+
'settings.update',
|
|
48
|
+
'settings.replace',
|
|
49
|
+
'settings.mutate',
|
|
50
|
+
'credentials.describe',
|
|
51
|
+
'credentials.set',
|
|
52
|
+
'credentials.unset',
|
|
53
|
+
'llm.discoverModels',
|
|
54
|
+
]);
|
package/lib/types/routes.d.ts
CHANGED
|
@@ -40,6 +40,7 @@ export declare const PAIR_PATHS: {
|
|
|
40
40
|
readonly issue: '/api/pair/issue';
|
|
41
41
|
readonly accept: '/api/pair/accept';
|
|
42
42
|
readonly stop: '/api/pair/stop';
|
|
43
|
+
readonly revoke: '/api/pair/revoke';
|
|
43
44
|
readonly heartbeat: '/api/pair/heartbeat';
|
|
44
45
|
readonly status: '/api/pair/status';
|
|
45
46
|
readonly events: '/api/pair/events';
|
|
@@ -59,6 +60,9 @@ export declare const issuePayloadSchema: z.ZodObject<{
|
|
|
59
60
|
export declare const acceptPayloadSchema: z.ZodObject<{
|
|
60
61
|
token: z.ZodDefault<z.ZodString>;
|
|
61
62
|
}, z.core.$strip>;
|
|
63
|
+
export declare const revokePayloadSchema: z.ZodObject<{
|
|
64
|
+
deviceId: z.ZodString;
|
|
65
|
+
}, z.core.$strip>;
|
|
62
66
|
export declare const pairActionPayloadSchema: z.ZodObject<{}, z.core.$loose>;
|
|
63
67
|
/** The SSE fan-out for desktop panel status. */
|
|
64
68
|
export declare class PairingEventsStream {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,EAAE,CAAC,EAAgB,MAAM,KAAK,CAAA;AACrC,OAAO,EAA0B,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAIhG;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA6BtG;AAKD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOxE;AAKD,8CAA8C;AAC9C,eAAO,MAAM,UAAU;aACrB,KAAK,EAAE,iBAAiB;aACxB,MAAM,EAAE,kBAAkB;aAC1B,IAAI,EAAE,gBAAgB;aACtB,SAAS,EAAE,qBAAqB;aAChC,MAAM,EAAE,kBAAkB;aAC1B,MAAM,EAAE,kBAAkB;CAClB,CAAA;AAEV;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB;;;iBAG7B,CAAA;AACF,eAAO,MAAM,mBAAmB;;iBAE9B,CAAA;AACF,eAAO,MAAM,uBAAuB,gCAA6B,CAAA;AA6BjE,gDAAgD;AAChD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAElD;;OAEG;IACH,YAAY,OAAO,EAAE,cAAc,EAElC;IAED,4DAA4D;IAC5D,IAAI,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,CAepD;IAED,kEAAkE;IAClE,IAAI,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAUpC;IAED,wCAAwC;IACxC,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAED,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,OAAO,EAAE,cAAc,CAAA;IACvB,0EAA0E;IAC1E,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;CAChC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,QAAQ,EAAE,
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,EAAE,CAAC,EAAgB,MAAM,KAAK,CAAA;AACrC,OAAO,EAA0B,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAIhG;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA6BtG;AAKD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOxE;AAKD,8CAA8C;AAC9C,eAAO,MAAM,UAAU;aACrB,KAAK,EAAE,iBAAiB;aACxB,MAAM,EAAE,kBAAkB;aAC1B,IAAI,EAAE,gBAAgB;aACtB,MAAM,EAAE,kBAAkB;aAC1B,SAAS,EAAE,qBAAqB;aAChC,MAAM,EAAE,kBAAkB;aAC1B,MAAM,EAAE,kBAAkB;CAClB,CAAA;AAEV;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB;;;iBAG7B,CAAA;AACF,eAAO,MAAM,mBAAmB;;iBAE9B,CAAA;AACF,eAAO,MAAM,mBAAmB;;iBAE9B,CAAA;AACF,eAAO,MAAM,uBAAuB,gCAA6B,CAAA;AA6BjE,gDAAgD;AAChD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAElD;;OAEG;IACH,YAAY,OAAO,EAAE,cAAc,EAElC;IAED,4DAA4D;IAC5D,IAAI,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,CAepD;IAED,kEAAkE;IAClE,IAAI,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAUpC;IAED,wCAAwC;IACxC,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAED,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,OAAO,EAAE,cAAc,CAAA;IACvB,0EAA0E;IAC1E,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;CAChC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,QAAQ,EAAE,CAiO3D"}
|
package/lib/types/routes.js
CHANGED
|
@@ -88,6 +88,7 @@ export const PAIR_PATHS = {
|
|
|
88
88
|
issue: '/api/pair/issue',
|
|
89
89
|
accept: '/api/pair/accept',
|
|
90
90
|
stop: '/api/pair/stop',
|
|
91
|
+
revoke: '/api/pair/revoke',
|
|
91
92
|
heartbeat: '/api/pair/heartbeat',
|
|
92
93
|
status: '/api/pair/status',
|
|
93
94
|
events: '/api/pair/events',
|
|
@@ -107,6 +108,9 @@ export const issuePayloadSchema = z.object({
|
|
|
107
108
|
export const acceptPayloadSchema = z.object({
|
|
108
109
|
token: z.string().default(''),
|
|
109
110
|
});
|
|
111
|
+
export const revokePayloadSchema = z.object({
|
|
112
|
+
deviceId: z.string().min(1),
|
|
113
|
+
});
|
|
110
114
|
export const pairActionPayloadSchema = z.object({}).passthrough();
|
|
111
115
|
/**
|
|
112
116
|
* Parse a pair request body through schema. A missing/empty or non-object
|
|
@@ -291,7 +295,8 @@ export function makeRoutes(deps) {
|
|
|
291
295
|
writeJson(res, 400, { ok: false, code: 'bad-payload' });
|
|
292
296
|
return;
|
|
293
297
|
}
|
|
294
|
-
const
|
|
298
|
+
const ua = req.headers['user-agent'];
|
|
299
|
+
const result = service.accept(payload.token, typeof ua === 'string' ? ua : undefined);
|
|
295
300
|
if (!result.ok) {
|
|
296
301
|
writeJson(res, result.code === 'used' ? 409 : 404, { ok: false, code: result.code });
|
|
297
302
|
return;
|
|
@@ -322,6 +327,26 @@ export function makeRoutes(deps) {
|
|
|
322
327
|
service.stop();
|
|
323
328
|
writeJson(res, 200, { ok: true });
|
|
324
329
|
};
|
|
330
|
+
const handleRevoke = async (req, res) => {
|
|
331
|
+
if (!requireMethod(req, res, 'POST'))
|
|
332
|
+
return;
|
|
333
|
+
if (!loopbackFence(req)) {
|
|
334
|
+
writeJson(res, 403, { ok: false, code: 'forbidden' });
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const body = await readJsonBody(req);
|
|
338
|
+
const payload = parsePairPayload(revokePayloadSchema, body);
|
|
339
|
+
if (payload === undefined) {
|
|
340
|
+
writeJson(res, 400, { ok: false, code: 'bad-payload' });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const revoked = service.revoke(payload.deviceId);
|
|
344
|
+
if (!revoked) {
|
|
345
|
+
writeJson(res, 404, { ok: false, code: 'unknown-device' });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
writeJson(res, 200, { ok: true });
|
|
349
|
+
};
|
|
325
350
|
const handleHeartbeat = async (req, res) => {
|
|
326
351
|
if (!requireMethod(req, res, 'POST'))
|
|
327
352
|
return;
|
|
@@ -352,10 +377,13 @@ export function makeRoutes(deps) {
|
|
|
352
377
|
const paired = deviceId !== undefined && service.hasDevice(deviceId);
|
|
353
378
|
const snapshot = service.snapshot();
|
|
354
379
|
// Unpaired LAN/tunnel clients get only the pairing-relevant fields; the
|
|
355
|
-
// token expiry,
|
|
356
|
-
//
|
|
380
|
+
// token expiry, public tunnel URL, and counts are an oracle for targeting
|
|
381
|
+
// and stay behind a live device cookie. The per-device roster (ids are
|
|
382
|
+
// session credentials) is never returned here — only the loopback events
|
|
383
|
+
// stream carries it to the desktop panel.
|
|
384
|
+
const { devices: _devices, ...rest } = snapshot;
|
|
357
385
|
const visible = paired
|
|
358
|
-
?
|
|
386
|
+
? rest
|
|
359
387
|
: { phase: snapshot.phase, lanAvailable: snapshot.lanAvailable, lanAddresses: snapshot.lanAddresses };
|
|
360
388
|
writeJson(res, 200, { ok: true, paired, ...visible });
|
|
361
389
|
};
|
|
@@ -374,6 +402,7 @@ export function makeRoutes(deps) {
|
|
|
374
402
|
{ kind: 'exact', path: PAIR_PATHS.issue, handler: handleIssue },
|
|
375
403
|
{ kind: 'exact', path: PAIR_PATHS.accept, handler: handleAccept },
|
|
376
404
|
{ kind: 'exact', path: PAIR_PATHS.stop, handler: handleStop },
|
|
405
|
+
{ kind: 'exact', path: PAIR_PATHS.revoke, handler: handleRevoke },
|
|
377
406
|
{ kind: 'exact', path: PAIR_PATHS.heartbeat, handler: handleHeartbeat },
|
|
378
407
|
{ kind: 'exact', path: PAIR_PATHS.status, handler: handleStatus },
|
|
379
408
|
{ kind: 'exact', path: PAIR_PATHS.events, handler: handleEvents },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAE1C,iDAAiD;AACjD,eAAO,MAAM,YAAY,+BAA+B,CAAA;AAExD,oEAAoE;AACpE,eAAO,MAAM,YAAY,gBAAgB,CAAA;AAEzC,sEAAsE;AACtE,eAAO,MAAM,iBAAiB,8BAA8B,CAAA;AAE5D,+EAA+E;AAC/E,eAAO,MAAM,YAAY,iCAAiC,CAAA;AAQ1D,0EAA0E;AAC1E,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,2DAA2D;IAC3D,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CASlE;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CA2B5D;AAYD,yDAAyD;AACzD,MAAM,WAAW,YAAY;IAC3B,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAA;CACZ;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,kBAAkB,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAahF;AAED,6DAA6D;AAC7D,KAAK,cAAc,GAAG,MAAM,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAA;AAE9D,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAG1D;
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"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"}
|
package/lib/types/update.js
CHANGED
|
@@ -128,6 +128,13 @@ export function isLinkedSpec(spec) {
|
|
|
128
128
|
return false;
|
|
129
129
|
return /^(?:link|file):|^\.{1,2}(?:[/\\]|$)/.test(spec);
|
|
130
130
|
}
|
|
131
|
+
/** Whether a direct local dependency overrides one of the aggregate's children. */
|
|
132
|
+
function hasLinkedFamilyOverride(anchorManifest, profileManifest) {
|
|
133
|
+
const dependencies = profileManifest?.dependencies;
|
|
134
|
+
if (typeof dependencies !== 'object' || dependencies === null)
|
|
135
|
+
return false;
|
|
136
|
+
return familyChildren(anchorManifest).some(name => isLinkedSpec(dependencies[name]));
|
|
137
|
+
}
|
|
131
138
|
/**
|
|
132
139
|
* Resolve the anchor package's manifest path. The aggregate package is the
|
|
133
140
|
* canonical entry point; this plugin's own package is the fallback. Both a
|
|
@@ -171,7 +178,7 @@ export function resolveUpdateTarget(deps) {
|
|
|
171
178
|
return { error: 'link' };
|
|
172
179
|
const profileManifest = readManifest(join(profile.dir, 'package.json'));
|
|
173
180
|
const spec = profileManifest?.dependencies?.[anchor];
|
|
174
|
-
if (isLinkedSpec(spec))
|
|
181
|
+
if (isLinkedSpec(spec) || hasLinkedFamilyOverride(manifest, profileManifest))
|
|
175
182
|
return { error: 'link' };
|
|
176
183
|
// Standalone installs carry no aggregate: the anchor's own dependency
|
|
177
184
|
// list misses every sibling @linxin666/* plugin installed directly into
|
|
@@ -283,7 +290,8 @@ export async function checkUpdates(deps) {
|
|
|
283
290
|
const profile = findProfile(manifestPath);
|
|
284
291
|
const profileManifest = profile === undefined ? undefined : readManifest(join(profile.dir, "package.json"));
|
|
285
292
|
const linked = profile === undefined
|
|
286
|
-
|| isLinkedSpec(profileManifest?.dependencies?.[anchor])
|
|
293
|
+
|| isLinkedSpec(profileManifest?.dependencies?.[anchor])
|
|
294
|
+
|| hasLinkedFamilyOverride(manifest, profileManifest);
|
|
287
295
|
if (profile === undefined) {
|
|
288
296
|
return { mode: 'link', packages: [], outdated: false };
|
|
289
297
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-remote-web-ui",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "0.2.
|
|
3
|
+
"description": "Scan-to-pair mobile and PC remote control for the dsh web GUI: QR entry beside the settings button, one-time pairing tokens, a gated /remote/api desktop channel, live device status, and revocable sessions",
|
|
4
|
+
"version": "0.2.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -61,17 +61,17 @@
|
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
64
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.
|
|
65
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.
|
|
66
|
-
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.
|
|
67
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.
|
|
68
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.
|
|
69
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "^0.1.0-rc.
|
|
70
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.
|
|
71
|
-
"@deepseek-ai/dsh-host-apiproxy": "^0.1.0-rc.
|
|
72
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.
|
|
73
|
-
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.
|
|
74
|
-
"@deepseek-ai/dsh-settings": "^0.1.0-rc.
|
|
64
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8",
|
|
65
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8",
|
|
66
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.8",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8",
|
|
69
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "^0.1.0-rc.8",
|
|
70
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.8",
|
|
71
|
+
"@deepseek-ai/dsh-host-apiproxy": "^0.1.0-rc.8",
|
|
72
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.8",
|
|
73
|
+
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
|
|
74
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.8",
|
|
75
75
|
"@testing-library/dom": "^10.4.1",
|
|
76
76
|
"@testing-library/react": "^16.3.2",
|
|
77
77
|
"@types/node": "^22.20.0",
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
import css from './remote.module.css'
|
|
10
|
+
|
|
11
|
+
/** Notice props: localized copy. */
|
|
12
|
+
export interface FenceNoticeProps {
|
|
13
|
+
t: TranslateNS<'remote'>
|
|
14
|
+
/** Retry after the user has opened a freshly issued computer pairing link. */
|
|
15
|
+
onRetry: () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Render the unpaired blocking page.
|
|
20
|
+
* @param props - localized copy.
|
|
21
|
+
* @returns the notice element.
|
|
22
|
+
*/
|
|
23
|
+
export function FenceNotice({ t, onRetry }: FenceNoticeProps) {
|
|
24
|
+
return (
|
|
25
|
+
<div className={css.fencePage} role="dialog" aria-modal="true" aria-labelledby="remote-fence-title">
|
|
26
|
+
<main className={css.fenceCard} data-dsh-plugin="remote-web-ui">
|
|
27
|
+
<div className={css.fenceMark} aria-hidden="true">×</div>
|
|
28
|
+
<p className={css.fenceEyebrow}>{t('fence.unpaired.eyebrow')}</p>
|
|
29
|
+
<h1 id="remote-fence-title" className={css.fenceTitle}>{t('fence.unpaired.title')}</h1>
|
|
30
|
+
<p className={css.fenceDetail}>{t('fence.unpaired.hint')}</p>
|
|
31
|
+
<ol className={css.fenceSteps}>
|
|
32
|
+
<li>{t('fence.unpaired.stepDesktop')}</li>
|
|
33
|
+
<li>{t('fence.unpaired.stepLink')}</li>
|
|
34
|
+
<li>{t('fence.unpaired.stepOpen')}</li>
|
|
35
|
+
</ol>
|
|
36
|
+
<button className={css.fenceRetry} type="button" onClick={onRetry}>
|
|
37
|
+
{t('fence.unpaired.retry')}
|
|
38
|
+
</button>
|
|
39
|
+
<p className={css.fenceFootnote}>{t('fence.unpaired.footnote')}</p>
|
|
40
|
+
</main>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -12,7 +12,7 @@ import { createPortal } from 'react-dom'
|
|
|
12
12
|
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
13
13
|
import type { PairingPhase } from '../pairing.ts'
|
|
14
14
|
import { RemotePanel, type PanelState } from './RemotePanel.tsx'
|
|
15
|
-
import { copyText, issuePair, stopPair, type IssueResponse, type PairStateFrame, type TunnelStatusFrame } from './pair-api.ts'
|
|
15
|
+
import { copyText, issuePair, revokePair, stopPair, type DeviceFrame, type IssueResponse, type PairStateFrame, type TunnelStatusFrame } from './pair-api.ts'
|
|
16
16
|
import { PhoneIcon } from './PhoneIcon.tsx'
|
|
17
17
|
import { UpdateEntry } from './UpdateEntry.tsx'
|
|
18
18
|
import css from './remote.module.css'
|
|
@@ -38,7 +38,9 @@ function mergeFrame(state: PanelState, frame: PairStateFrame): PanelState {
|
|
|
38
38
|
phase: frame.phase,
|
|
39
39
|
deviceCount: frame.deviceCount,
|
|
40
40
|
onlineCount: frame.onlineCount,
|
|
41
|
+
devices: frame.devices ?? [],
|
|
41
42
|
...(frame.tunnel !== undefined ? { tunnel: frame.tunnel as TunnelStatusFrame } : {}),
|
|
43
|
+
...(frame.posture !== undefined ? { posture: frame.posture } : {}),
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
|
|
@@ -55,7 +57,7 @@ export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
|
|
|
55
57
|
// pure), so mint decisions read this ref instead.
|
|
56
58
|
const stateRef = useRef(state)
|
|
57
59
|
useEffect(() => { stateRef.current = state }, [state])
|
|
58
|
-
const [copied, setCopied] = useState(
|
|
60
|
+
const [copied, setCopied] = useState<'phone' | 'desktop' | undefined>(undefined)
|
|
59
61
|
const eventSource = useRef<EventSource | undefined>(undefined)
|
|
60
62
|
// Generation counter for the open flow: closing (or re-opening) the panel
|
|
61
63
|
// bumps it, so an in-flight issue() that resolves after a close does not
|
|
@@ -97,6 +99,7 @@ export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
|
|
|
97
99
|
phase: 'waiting',
|
|
98
100
|
deviceCount: 0,
|
|
99
101
|
onlineCount: 0,
|
|
102
|
+
devices: [] as DeviceFrame[],
|
|
100
103
|
// Whether this QR is built on the configured public (tunneled) base.
|
|
101
104
|
public: publicBaseUrl !== undefined && result.url.startsWith(publicBaseUrl),
|
|
102
105
|
...(publicBaseUrl !== undefined ? { publicBaseUrl } : {}),
|
|
@@ -180,7 +183,14 @@ export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
|
|
|
180
183
|
// keeps the UI honest, and the status stream confirms the stopped phase.
|
|
181
184
|
void stopPair().catch(() => {})
|
|
182
185
|
// Optimistic fallback; the status stream confirms with the stopped phase.
|
|
183
|
-
setState(previous => previous.kind === 'ready' ? { ...previous, phase: 'stopped' as PairingPhase } : previous)
|
|
186
|
+
setState(previous => previous.kind === 'ready' ? { ...previous, phase: 'stopped' as PairingPhase, devices: [] } : previous)
|
|
187
|
+
}, [])
|
|
188
|
+
|
|
189
|
+
const handleRevoke = useCallback((deviceId: string) => {
|
|
190
|
+
void revokePair(deviceId).catch(() => {})
|
|
191
|
+
setState(previous => previous.kind === 'ready'
|
|
192
|
+
? { ...previous, devices: previous.devices.filter(device => device.id !== deviceId) }
|
|
193
|
+
: previous)
|
|
184
194
|
}, [])
|
|
185
195
|
|
|
186
196
|
const handleRefresh = useCallback(() => {
|
|
@@ -197,14 +207,13 @@ export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
|
|
|
197
207
|
void mint().then(setState)
|
|
198
208
|
}, [mint])
|
|
199
209
|
|
|
200
|
-
const handleCopy = useCallback(() => {
|
|
201
|
-
|
|
202
|
-
void copyText(state.url).then((ok) => {
|
|
210
|
+
const handleCopy = useCallback((target: 'phone' | 'desktop', url: string) => {
|
|
211
|
+
void copyText(url).then((ok) => {
|
|
203
212
|
if (!ok) return
|
|
204
|
-
setCopied(
|
|
205
|
-
window.setTimeout(() => { setCopied(
|
|
213
|
+
setCopied(target)
|
|
214
|
+
window.setTimeout(() => { setCopied(undefined) }, 1500)
|
|
206
215
|
})
|
|
207
|
-
}, [
|
|
216
|
+
}, [])
|
|
208
217
|
|
|
209
218
|
return (
|
|
210
219
|
<>
|
|
@@ -225,6 +234,7 @@ export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
|
|
|
225
234
|
onCopy={handleCopy}
|
|
226
235
|
onPickAddress={handlePickAddress}
|
|
227
236
|
onPickPublic={handlePickPublic}
|
|
237
|
+
onRevoke={handleRevoke}
|
|
228
238
|
/>
|
|
229
239
|
</div>
|
|
230
240
|
), document.body)}
|