@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,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The remote desktop channel — browser half. On a non-loopback origin (LAN
|
|
3
|
+
* address or public tunnel) fenced host routes refuse the request, and
|
|
4
|
+
* pairing is the real access control — so same-origin traffic the desktop
|
|
5
|
+
* issues is rewritten onto this plugin's gated `/remote` prefix (host half
|
|
6
|
+
* in src/remote-api.ts). The host then re-issues the call to 127.0.0.1 so
|
|
7
|
+
* plugin loopback fences pass.
|
|
8
|
+
*
|
|
9
|
+
* The rewrite is deliberately narrow:
|
|
10
|
+
* - loopback origins are untouched (the desktop at 127.0.0.1 keeps original paths);
|
|
11
|
+
* - the pairing routes (`/api/pair/*`) stay where they are — accept must
|
|
12
|
+
* work BEFORE a device is paired;
|
|
13
|
+
* - the update endpoints (`/api/update/*`) stay loopback-only;
|
|
14
|
+
* - desktop-launcher create/shutdown (`/api/dsh-desktop-launcher/*`) stay
|
|
15
|
+
* loopback-only (host-local files and process exit);
|
|
16
|
+
* - the family settings bridge (`/api/dsh-web-ui-settings/*`) stays
|
|
17
|
+
* loopback-only (same plane as SDK settings.*);
|
|
18
|
+
* - `/api/*` (SDK methods and `/api/<plugin>/...` plugin namespaces),
|
|
19
|
+
* `/sidebar/*`, `/git/*`, and `/pet/*` ride the channel;
|
|
20
|
+
* - fetch, EventSource, WebSocket, and img/script/iframe `src` are patched;
|
|
21
|
+
* everything else calls the original unchanged.
|
|
22
|
+
*
|
|
23
|
+
* Pure helpers are exported for unit tests; `installRemoteChannel` patches
|
|
24
|
+
* the given window and returns their restore.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** The gated mirror prefix (must match src/remote-methods.ts). */
|
|
28
|
+
export const REMOTE_PREFIX = '/remote'
|
|
29
|
+
|
|
30
|
+
/** Connection-plugin method prefix under the gated channel. */
|
|
31
|
+
export const REMOTE_API_PREFIX = `${REMOTE_PREFIX}/api`
|
|
32
|
+
|
|
33
|
+
const API_PREFIX = '/api/'
|
|
34
|
+
const PAIR_PREFIX = '/api/pair/'
|
|
35
|
+
const UPDATE_PREFIX = '/api/update/'
|
|
36
|
+
const DESKTOP_LAUNCHER_PREFIX = '/api/dsh-desktop-launcher'
|
|
37
|
+
const SETTINGS_BRIDGE_PREFIX = '/api/dsh-web-ui-settings'
|
|
38
|
+
const SIDEBAR_PREFIX = '/sidebar/'
|
|
39
|
+
const GIT_PREFIX = '/git/'
|
|
40
|
+
const PET_PREFIX = '/pet/'
|
|
41
|
+
const WS_PATHS = new Set([
|
|
42
|
+
'/api/events.mux',
|
|
43
|
+
'/api/events.host',
|
|
44
|
+
'/sidebar/ws/terminal',
|
|
45
|
+
'/sidebar/ws/agent-terminals',
|
|
46
|
+
'/api/dsh-ssh/terminal',
|
|
47
|
+
])
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Browser-safe loopback classification for the page origin (the SDK client
|
|
51
|
+
* exports its own; this copy keeps the module dependency-free).
|
|
52
|
+
* @param hostname - a location hostname (IPv6 without brackets).
|
|
53
|
+
* @returns true for localhost, IPv6 loopback, or any 127/8 literal.
|
|
54
|
+
*/
|
|
55
|
+
export function isLoopbackHostname(hostname: string): boolean {
|
|
56
|
+
if (hostname === 'localhost' || hostname === '::1') return true
|
|
57
|
+
const parts = hostname.split('.')
|
|
58
|
+
return parts.length === 4 && parts[0] === '127' && parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether one same-origin path must ride the gated channel (fetch, EventSource,
|
|
63
|
+
* img/script/iframe src).
|
|
64
|
+
* @param pathname - the request URL pathname.
|
|
65
|
+
*/
|
|
66
|
+
export function shouldRewriteFetchPath(pathname: string): boolean {
|
|
67
|
+
if (pathname.startsWith(PAIR_PREFIX)) return false
|
|
68
|
+
if (pathname.startsWith(UPDATE_PREFIX)) return false
|
|
69
|
+
if (pathname === DESKTOP_LAUNCHER_PREFIX || pathname.startsWith(`${DESKTOP_LAUNCHER_PREFIX}/`)) return false
|
|
70
|
+
if (pathname === SETTINGS_BRIDGE_PREFIX || pathname.startsWith(`${SETTINGS_BRIDGE_PREFIX}/`)) return false
|
|
71
|
+
if (pathname.startsWith(API_PREFIX)) return true
|
|
72
|
+
if (pathname.startsWith(SIDEBAR_PREFIX) || pathname === '/sidebar') return true
|
|
73
|
+
if (pathname.startsWith(GIT_PREFIX) || pathname === '/git') return true
|
|
74
|
+
if (pathname.startsWith(PET_PREFIX) || pathname === '/pet') return true
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Whether one WebSocket path must ride the gated channel.
|
|
80
|
+
* @param pathname - the WebSocket URL pathname.
|
|
81
|
+
*/
|
|
82
|
+
export function shouldRewriteWsPath(pathname: string): boolean {
|
|
83
|
+
return WS_PATHS.has(pathname)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The gated twin of one fenced path (`/remote` + original pathname). */
|
|
87
|
+
export function rewritePath(pathname: string): string {
|
|
88
|
+
return `${REMOTE_PREFIX}${pathname}`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Rewrite one raw URL string when it is same-origin and fenced. Relative
|
|
93
|
+
* inputs stay relative so resource loaders do not unexpectedly absolutize.
|
|
94
|
+
*/
|
|
95
|
+
export function rewriteRawUrl(raw: string, baseHref: string, origin: string): string {
|
|
96
|
+
let url: URL
|
|
97
|
+
try {
|
|
98
|
+
url = new URL(raw, baseHref)
|
|
99
|
+
} catch {
|
|
100
|
+
return raw
|
|
101
|
+
}
|
|
102
|
+
if (url.origin !== origin) return raw
|
|
103
|
+
if (!shouldRewriteFetchPath(url.pathname)) return raw
|
|
104
|
+
url.pathname = rewritePath(url.pathname)
|
|
105
|
+
if (raw.startsWith('/') && !raw.startsWith('//')) {
|
|
106
|
+
return `${url.pathname}${url.search}${url.hash}`
|
|
107
|
+
}
|
|
108
|
+
return url.href
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A constructor that exposes a configurable `src` on its prototype. */
|
|
112
|
+
interface SrcConstructor {
|
|
113
|
+
prototype: object
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The subset of window the channel needs (injectable for tests). */
|
|
117
|
+
export interface ChannelWindow {
|
|
118
|
+
fetch: typeof globalThis.fetch
|
|
119
|
+
WebSocket: typeof WebSocket
|
|
120
|
+
EventSource?: typeof EventSource
|
|
121
|
+
HTMLImageElement?: SrcConstructor
|
|
122
|
+
HTMLScriptElement?: SrcConstructor
|
|
123
|
+
HTMLIFrameElement?: SrcConstructor
|
|
124
|
+
location: { origin: string; href: string }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Options for {@link installRemoteChannel}. */
|
|
128
|
+
export interface RemoteChannelOptions {
|
|
129
|
+
/** Called when a gated call came back unpaired (code `unpaired`). */
|
|
130
|
+
onUnpaired?: () => void
|
|
131
|
+
/** Called when a gated call succeeded (an unpaired banner can retire). */
|
|
132
|
+
onPaired?: () => void
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Read an unpaired code from either the SDK envelope or a plugin JSON body. */
|
|
136
|
+
function unpairedCodeOf(value: unknown): string | undefined {
|
|
137
|
+
if (typeof value !== 'object' || value === null) return undefined
|
|
138
|
+
const record = value as { result?: unknown; error?: unknown }
|
|
139
|
+
const nested = record.result
|
|
140
|
+
if (typeof nested === 'object' && nested !== null) {
|
|
141
|
+
const error = (nested as { error?: unknown }).error
|
|
142
|
+
if (typeof error === 'object' && error !== null && typeof (error as { code?: unknown }).code === 'string') {
|
|
143
|
+
return (error as { code: string }).code
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const error = record.error
|
|
147
|
+
if (typeof error === 'object' && error !== null && typeof (error as { code?: unknown }).code === 'string') {
|
|
148
|
+
return (error as { code: string }).code
|
|
149
|
+
}
|
|
150
|
+
return undefined
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Whether a gated 403 is the unpaired-device fence (not a loopback-only
|
|
155
|
+
* method denial, which uses the same status with code `forbidden`).
|
|
156
|
+
*/
|
|
157
|
+
export async function isUnpairedDenied(response: Response): Promise<boolean> {
|
|
158
|
+
if (response.status !== 403) return false
|
|
159
|
+
try {
|
|
160
|
+
return unpairedCodeOf(await response.json()) === 'unpaired'
|
|
161
|
+
} catch {
|
|
162
|
+
return false
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Wrap a prototype `src` setter so fenced same-origin URLs ride `/remote`.
|
|
168
|
+
* No-ops when the constructor is missing or `src` is not configurable.
|
|
169
|
+
*/
|
|
170
|
+
function patchSrcAccessor(ctor: SrcConstructor | undefined, rewrite: (value: string) => string): () => void {
|
|
171
|
+
if (ctor === undefined) return () => {}
|
|
172
|
+
const descriptor = Object.getOwnPropertyDescriptor(ctor.prototype, 'src')
|
|
173
|
+
if (descriptor === undefined || descriptor.configurable === false) return () => {}
|
|
174
|
+
if (descriptor.set === undefined) return () => {}
|
|
175
|
+
const originalSet = descriptor.set
|
|
176
|
+
const originalGet = descriptor.get
|
|
177
|
+
Object.defineProperty(ctor.prototype, 'src', {
|
|
178
|
+
configurable: true,
|
|
179
|
+
enumerable: descriptor.enumerable ?? true,
|
|
180
|
+
get: originalGet,
|
|
181
|
+
set(this: unknown, value: unknown) {
|
|
182
|
+
originalSet.call(this, rewrite(String(value)))
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
return () => {
|
|
186
|
+
Object.defineProperty(ctor.prototype, 'src', descriptor)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Patch `fetch`, `EventSource`, `WebSocket`, and resource `src` accessors on
|
|
192
|
+
* one window to route fenced traffic through the gated channel.
|
|
193
|
+
* @param window - the browser window (or a test double).
|
|
194
|
+
* @param options - the unpaired callback.
|
|
195
|
+
* @returns a function restoring the originals.
|
|
196
|
+
*/
|
|
197
|
+
export function installRemoteChannel(window: ChannelWindow, options: RemoteChannelOptions = {}): () => void {
|
|
198
|
+
const originalFetch = window.fetch
|
|
199
|
+
const OriginalWebSocket = window.WebSocket
|
|
200
|
+
const OriginalEventSource = window.EventSource
|
|
201
|
+
|
|
202
|
+
const sameOrigin = (url: URL): boolean => url.origin === window.location.origin
|
|
203
|
+
const rewrite = (raw: string): string => rewriteRawUrl(raw, window.location.href, window.location.origin)
|
|
204
|
+
|
|
205
|
+
const patchedFetch: typeof globalThis.fetch = (input, init) => {
|
|
206
|
+
const url = new URL(
|
|
207
|
+
typeof input === 'string' || input instanceof URL ? input.toString() : input.url,
|
|
208
|
+
window.location.href,
|
|
209
|
+
)
|
|
210
|
+
if (sameOrigin(url) && shouldRewriteFetchPath(url.pathname)) {
|
|
211
|
+
const rewritten = new URL(url)
|
|
212
|
+
rewritten.pathname = rewritePath(url.pathname)
|
|
213
|
+
const next: RequestInfo | URL = typeof input === 'string' || input instanceof URL
|
|
214
|
+
? rewritten.toString()
|
|
215
|
+
: new Request(rewritten, input)
|
|
216
|
+
return Promise.resolve(originalFetch.call(window, next, init)).then(async (response) => {
|
|
217
|
+
if (await isUnpairedDenied(response.clone())) options.onUnpaired?.()
|
|
218
|
+
else options.onPaired?.()
|
|
219
|
+
return response
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
return originalFetch.call(window, input, init)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
class PatchedWebSocket extends OriginalWebSocket {
|
|
226
|
+
constructor(url: string | URL, protocols?: string | string[]) {
|
|
227
|
+
const parsed = new URL(url.toString(), window.location.href)
|
|
228
|
+
const wsOrigin = parsed.protocol === 'wss:' ? `https://${parsed.host}` : parsed.protocol === 'ws:' ? `http://${parsed.host}` : ''
|
|
229
|
+
if (wsOrigin !== '' && wsOrigin === window.location.origin && shouldRewriteWsPath(parsed.pathname)) {
|
|
230
|
+
const rewritten = new URL(parsed)
|
|
231
|
+
rewritten.pathname = rewritePath(parsed.pathname)
|
|
232
|
+
super(rewritten, protocols)
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
super(url, protocols)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const restoreSrc = [
|
|
240
|
+
patchSrcAccessor(window.HTMLImageElement, rewrite),
|
|
241
|
+
patchSrcAccessor(window.HTMLScriptElement, rewrite),
|
|
242
|
+
patchSrcAccessor(window.HTMLIFrameElement, rewrite),
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
window.fetch = patchedFetch
|
|
246
|
+
window.WebSocket = PatchedWebSocket as typeof WebSocket
|
|
247
|
+
if (OriginalEventSource !== undefined) {
|
|
248
|
+
class PatchedEventSource extends OriginalEventSource {
|
|
249
|
+
constructor(url: string | URL, eventSourceInitDict?: EventSourceInit) {
|
|
250
|
+
const parsed = new URL(url.toString(), window.location.href)
|
|
251
|
+
if (sameOrigin(parsed) && shouldRewriteFetchPath(parsed.pathname)) {
|
|
252
|
+
const rewritten = new URL(parsed)
|
|
253
|
+
rewritten.pathname = rewritePath(parsed.pathname)
|
|
254
|
+
super(rewritten, eventSourceInitDict)
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
super(url, eventSourceInitDict)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
window.EventSource = PatchedEventSource
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return () => {
|
|
264
|
+
window.fetch = originalFetch
|
|
265
|
+
window.WebSocket = OriginalWebSocket
|
|
266
|
+
if (OriginalEventSource !== undefined) window.EventSource = OriginalEventSource
|
|
267
|
+
for (const restore of restoreSrc) restore()
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -267,6 +267,7 @@
|
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
.link {
|
|
270
|
+
display: block;
|
|
270
271
|
margin: 0;
|
|
271
272
|
overflow: hidden;
|
|
272
273
|
text-overflow: ellipsis;
|
|
@@ -276,6 +277,55 @@
|
|
|
276
277
|
font-size: 12px;
|
|
277
278
|
}
|
|
278
279
|
|
|
280
|
+
.pairLinks {
|
|
281
|
+
display: flex;
|
|
282
|
+
flex-direction: column;
|
|
283
|
+
gap: 8px;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.pairLinkRow {
|
|
287
|
+
display: flex;
|
|
288
|
+
align-items: center;
|
|
289
|
+
gap: 10px;
|
|
290
|
+
min-width: 0;
|
|
291
|
+
padding: 10px 12px;
|
|
292
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
293
|
+
border-radius: 10px;
|
|
294
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.pairLinkText {
|
|
298
|
+
min-width: 0;
|
|
299
|
+
flex: 1;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.pairLinkLabel {
|
|
303
|
+
display: block;
|
|
304
|
+
margin-bottom: 3px;
|
|
305
|
+
color: var(--dsw-alias-label-secondary);
|
|
306
|
+
font-size: 12px;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.copyLink {
|
|
310
|
+
display: inline-flex;
|
|
311
|
+
flex: none;
|
|
312
|
+
align-items: center;
|
|
313
|
+
gap: 5px;
|
|
314
|
+
min-height: 30px;
|
|
315
|
+
padding: 0 10px;
|
|
316
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
317
|
+
border-radius: 8px;
|
|
318
|
+
background: var(--dsw-alias-button-elevated-fill);
|
|
319
|
+
color: var(--dsw-alias-label-primary);
|
|
320
|
+
cursor: pointer;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.oneTimeHint {
|
|
324
|
+
margin: 0;
|
|
325
|
+
color: var(--dsw-alias-label-caption);
|
|
326
|
+
font-size: 12px;
|
|
327
|
+
}
|
|
328
|
+
|
|
279
329
|
.stoppedHint {
|
|
280
330
|
margin: 0;
|
|
281
331
|
color: var(--dsw-alias-state-error-primary);
|
|
@@ -353,32 +403,110 @@
|
|
|
353
403
|
font-size: 13px;
|
|
354
404
|
}
|
|
355
405
|
|
|
356
|
-
/*
|
|
357
|
-
.
|
|
406
|
+
/* Unpaired remote desktop: opaque, blocking page above the shell. */
|
|
407
|
+
.fencePage {
|
|
358
408
|
position: fixed;
|
|
359
|
-
|
|
360
|
-
left: 50%;
|
|
361
|
-
transform: translateX(-50%);
|
|
409
|
+
inset: 0;
|
|
362
410
|
z-index: 2000;
|
|
363
|
-
|
|
411
|
+
display: flex;
|
|
412
|
+
flex-direction: column;
|
|
413
|
+
align-items: center;
|
|
414
|
+
justify-content: center;
|
|
415
|
+
overflow: auto;
|
|
364
416
|
box-sizing: border-box;
|
|
365
|
-
padding:
|
|
417
|
+
padding: 40px 24px;
|
|
418
|
+
background: var(--dsw-alias-bg-base);
|
|
419
|
+
text-align: center;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.fenceCard {
|
|
423
|
+
width: min(520px, 100%);
|
|
424
|
+
margin-inline: auto;
|
|
425
|
+
box-sizing: border-box;
|
|
426
|
+
padding: 36px 40px;
|
|
366
427
|
border: 1px solid var(--dsw-alias-border-l2);
|
|
367
|
-
border-radius:
|
|
368
|
-
background: var(--dsw-alias-bg-layer-
|
|
428
|
+
border-radius: 20px;
|
|
429
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
369
430
|
box-shadow: var(--dsw-shadow-lv3);
|
|
431
|
+
text-align: center;
|
|
370
432
|
}
|
|
371
433
|
|
|
372
|
-
.
|
|
373
|
-
|
|
434
|
+
.fenceMark {
|
|
435
|
+
display: grid;
|
|
436
|
+
place-items: center;
|
|
437
|
+
width: 44px;
|
|
438
|
+
height: 44px;
|
|
439
|
+
margin-inline: auto;
|
|
440
|
+
border-radius: 50%;
|
|
441
|
+
background: var(--dsw-alias-state-error-secondary);
|
|
374
442
|
color: var(--dsw-alias-state-error-primary);
|
|
375
|
-
font-
|
|
443
|
+
font-size: 24px;
|
|
444
|
+
line-height: 1;
|
|
376
445
|
}
|
|
377
446
|
|
|
378
|
-
.
|
|
379
|
-
margin:
|
|
380
|
-
color: var(--dsw-alias-
|
|
447
|
+
.fenceEyebrow {
|
|
448
|
+
margin: 22px 0 8px;
|
|
449
|
+
color: var(--dsw-alias-state-error-primary);
|
|
381
450
|
font-size: 13px;
|
|
451
|
+
font-weight: 600;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.fenceTitle {
|
|
455
|
+
margin: 0;
|
|
456
|
+
color: var(--dsw-alias-label-primary);
|
|
457
|
+
font-size: 24px;
|
|
458
|
+
line-height: 1.35;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.fenceDetail {
|
|
462
|
+
margin: 12px 0 0;
|
|
463
|
+
color: var(--dsw-alias-label-secondary);
|
|
464
|
+
font-size: 14px;
|
|
465
|
+
line-height: 1.65;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.fenceSteps {
|
|
469
|
+
margin: 24px auto 0;
|
|
470
|
+
padding: 20px 20px 20px 42px;
|
|
471
|
+
border-radius: 12px;
|
|
472
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
473
|
+
color: var(--dsw-alias-label-primary);
|
|
474
|
+
font-size: 14px;
|
|
475
|
+
line-height: 1.65;
|
|
476
|
+
text-align: left;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.fenceSteps li + li {
|
|
480
|
+
margin-top: 8px;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.fenceRetry {
|
|
484
|
+
width: 100%;
|
|
485
|
+
margin-top: 24px;
|
|
486
|
+
padding: 11px 16px;
|
|
487
|
+
border: 1px solid var(--dsw-alias-brand-primary);
|
|
488
|
+
border-radius: 10px;
|
|
489
|
+
background: var(--dsw-alias-brand-primary);
|
|
490
|
+
color: #fff;
|
|
491
|
+
font: inherit;
|
|
492
|
+
font-weight: 500;
|
|
493
|
+
cursor: pointer;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.fenceRetry:hover {
|
|
497
|
+
filter: brightness(0.96);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.fenceRetry:focus-visible {
|
|
501
|
+
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
502
|
+
outline-offset: 3px;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.fenceFootnote {
|
|
506
|
+
margin: 14px 0 0;
|
|
507
|
+
color: var(--dsw-alias-label-tertiary);
|
|
508
|
+
font-size: 12px;
|
|
509
|
+
line-height: 1.55;
|
|
382
510
|
}
|
|
383
511
|
|
|
384
512
|
/* Multi-homed address picker: one radio per constructible LAN literal. */
|
|
@@ -434,6 +562,101 @@
|
|
|
434
562
|
font-size: 12px;
|
|
435
563
|
}
|
|
436
564
|
|
|
565
|
+
.devices {
|
|
566
|
+
display: flex;
|
|
567
|
+
flex-direction: column;
|
|
568
|
+
gap: 8px;
|
|
569
|
+
padding: 12px 16px 14px;
|
|
570
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
571
|
+
border-radius: 16px;
|
|
572
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.devicesTitle {
|
|
576
|
+
margin: 0;
|
|
577
|
+
font-size: 13px;
|
|
578
|
+
font-weight: 500;
|
|
579
|
+
line-height: 20px;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.devicesEmpty {
|
|
583
|
+
margin: 0;
|
|
584
|
+
color: var(--dsw-alias-label-secondary);
|
|
585
|
+
font-size: 13px;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.deviceList {
|
|
589
|
+
margin: 0;
|
|
590
|
+
padding: 0;
|
|
591
|
+
list-style: none;
|
|
592
|
+
display: flex;
|
|
593
|
+
flex-direction: column;
|
|
594
|
+
gap: 8px;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.deviceRow {
|
|
598
|
+
display: flex;
|
|
599
|
+
align-items: flex-start;
|
|
600
|
+
justify-content: space-between;
|
|
601
|
+
gap: 12px;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.deviceMeta {
|
|
605
|
+
min-width: 0;
|
|
606
|
+
display: flex;
|
|
607
|
+
flex-direction: column;
|
|
608
|
+
gap: 2px;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.deviceName {
|
|
612
|
+
overflow: hidden;
|
|
613
|
+
text-overflow: ellipsis;
|
|
614
|
+
white-space: nowrap;
|
|
615
|
+
font-size: 13px;
|
|
616
|
+
font-weight: 500;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.devicePresence {
|
|
620
|
+
font-size: 12px;
|
|
621
|
+
line-height: 18px;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.deviceOnline {
|
|
625
|
+
color: var(--dsw-alias-state-success-primary);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
.deviceOffline {
|
|
629
|
+
color: var(--dsw-alias-label-secondary);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.deviceSeen {
|
|
633
|
+
color: var(--dsw-alias-label-tertiary);
|
|
634
|
+
font-size: 12px;
|
|
635
|
+
font-variant-numeric: tabular-nums;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.deviceRevoke {
|
|
639
|
+
flex: none;
|
|
640
|
+
border: none;
|
|
641
|
+
border-radius: 8px;
|
|
642
|
+
padding: 6px 10px;
|
|
643
|
+
background: transparent;
|
|
644
|
+
color: var(--dsw-alias-label-secondary);
|
|
645
|
+
font-size: 12px;
|
|
646
|
+
cursor: pointer;
|
|
647
|
+
transition: background-color 120ms ease, color 120ms ease;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.deviceRevoke:hover:not(:disabled) {
|
|
651
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
652
|
+
color: var(--dsw-alias-label-primary);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.deviceRevoke:focus-visible {
|
|
656
|
+
outline: none;
|
|
657
|
+
box-shadow: 0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);
|
|
658
|
+
}
|
|
659
|
+
|
|
437
660
|
/* Accessibility: collapse the interaction transitions for reduced-motion
|
|
438
661
|
users. The panel chrome is already static; this just disables the
|
|
439
662
|
hover/active/focus cross-fades added above. */
|
|
@@ -441,7 +664,8 @@
|
|
|
441
664
|
.trigger,
|
|
442
665
|
.close,
|
|
443
666
|
.action,
|
|
444
|
-
.address
|
|
667
|
+
.address,
|
|
668
|
+
.deviceRevoke {
|
|
445
669
|
transition: none;
|
|
446
670
|
}
|
|
447
671
|
}
|
package/src/dsh-home.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/host/dsh-home.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
/**
|
|
3
|
+
* DSH_HOME resolution shared by the plugin family's Host halves: the
|
|
4
|
+
* environment override wins, the platform home fallback follows. Mirrors
|
|
5
|
+
* what dsh-pet and dsh-liangshen each used to implement locally.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { homedir } from 'node:os'
|
|
9
|
+
import { isAbsolute, join } from 'node:path'
|
|
10
|
+
|
|
11
|
+
/** Expand a leading ~ (or ~user) in a path, platform-style. */
|
|
12
|
+
export function expandHome(path: string, home: string = homedir()): string {
|
|
13
|
+
if (path === '~') return home
|
|
14
|
+
if (path.startsWith('~/') || path.startsWith('~\\')) return join(home, path.slice(2))
|
|
15
|
+
return path
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the DSH home directory.
|
|
20
|
+
* @param env - process environment to read DSH_HOME from.
|
|
21
|
+
* @param home - platform home directory fallback (test seam).
|
|
22
|
+
* @returns the absolute DSH home path.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveDshHome(env: NodeJS.ProcessEnv = process.env, home: string = homedir()): string {
|
|
25
|
+
const raw = env.DSH_HOME
|
|
26
|
+
if (raw !== undefined && raw.trim() !== '') {
|
|
27
|
+
const expanded = expandHome(raw.trim(), home)
|
|
28
|
+
return isAbsolute(expanded) ? expanded : join(process.cwd(), expanded)
|
|
29
|
+
}
|
|
30
|
+
return join(home, '.dsh')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Resolve the DSH home directory from the live environment. */
|
|
34
|
+
export function dshHome(): string {
|
|
35
|
+
return resolveDshHome()
|
|
36
|
+
}
|
package/src/gate.ts
CHANGED
|
@@ -84,13 +84,16 @@ export function makeGateListener(
|
|
|
84
84
|
enabled: boolean | (() => boolean) = true,
|
|
85
85
|
): (request: IncomingMessage, method: string | undefined, next: () => boolean | Promise<boolean>) => boolean | Promise<boolean> {
|
|
86
86
|
return (request, _method, next) => {
|
|
87
|
+
// Loopback clients (desktop at 127.0.0.1) always pass without cookie check.
|
|
87
88
|
if (isLoopbackClient(request)) return next()
|
|
88
89
|
const active = typeof enabled === 'function' ? enabled() : enabled
|
|
89
90
|
if (!active) return false
|
|
90
91
|
const require = typeof requirePairingForLan === 'function' ? requirePairingForLan() : requirePairingForLan
|
|
92
|
+
// When pairing is not required for LAN, non-loopback requests pass too.
|
|
91
93
|
if (!require) return next()
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
+
// Non-loopback origin: verify paired-device cookie. Sibling plugins that
|
|
95
|
+
// own their own prefixes (/pet, /git, /sidebar, …) consult the same cookie
|
|
96
|
+
// through the remoteWebUiPairing service — this listener only covers /api.
|
|
94
97
|
return isPairedDeviceRequest(service, request) ? next() : false
|
|
95
98
|
}
|
|
96
99
|
}
|