@linxin666/dsh-remote-web-ui 0.1.14 → 0.1.15
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/lib/client.js +28 -15
- package/lib/client.js.map +1 -1
- package/lib/index.js +63 -7
- package/lib/mobile.js +44 -25
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +26 -11
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +27 -7
- package/lib/types/client/settings-form.d.ts +14 -5
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +38 -10
- package/lib/types/mobile/mux.d.ts +13 -4
- package/lib/types/mobile/mux.d.ts.map +1 -1
- package/lib/types/mobile/mux.js +50 -30
- package/lib/types/mobile/mux.test.js +75 -6
- package/lib/types/mobile-api.d.ts +2 -0
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +4 -1
- package/lib/types/routes.d.ts +17 -0
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +49 -10
- package/package.json +1 -1
- package/src/client/PluginSettingsCard.tsx +83 -17
- package/src/client/settings-card.module.css +6 -0
- package/src/client/settings-form.ts +41 -9
- package/src/mobile/mux.test.ts +84 -6
- package/src/mobile/mux.ts +46 -25
- package/src/mobile-api.ts +6 -1
- package/src/routes.ts +51 -10
package/src/mobile-api.ts
CHANGED
|
@@ -49,6 +49,8 @@ const MOBILE_PREFERENCES_METHOD = 'mobile.preferences'
|
|
|
49
49
|
|
|
50
50
|
/** One session.list page (thin phones load incrementally). */
|
|
51
51
|
const SESSION_PAGE_SIZE = 20
|
|
52
|
+
/** SSE keep-alive ping cadence for the live mux stream (single connection). */
|
|
53
|
+
const DEFAULT_EVENTS_HEARTBEAT_MS = 15_000
|
|
52
54
|
|
|
53
55
|
/** Encode one list position as an opaque continuation cursor. */
|
|
54
56
|
function sessionListCursor(updatedAt: number, sessionId: string): string {
|
|
@@ -78,6 +80,8 @@ export interface MobileApiDeps {
|
|
|
78
80
|
apiProxy: ApiProxy
|
|
79
81
|
/** The resolved mobile composer preference (live per request). */
|
|
80
82
|
mobileEnterToSend: () => boolean
|
|
83
|
+
/** SSE keep-alive ping cadence for the mux stream (default 15000 ms; test seam). */
|
|
84
|
+
eventsHeartbeatMs?: number
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
/** Mobile API route paths. */
|
|
@@ -97,6 +101,7 @@ const MOBILE_API_METHOD_PREFIX = `${MOBILE_API_PREFIX}/`
|
|
|
97
101
|
*/
|
|
98
102
|
export function makeMobileApiRoutes(deps: MobileApiDeps): WebRoute[] {
|
|
99
103
|
const { service, apiProxy, mobileEnterToSend } = deps
|
|
104
|
+
const eventsHeartbeatMs = deps.eventsHeartbeatMs ?? DEFAULT_EVENTS_HEARTBEAT_MS
|
|
100
105
|
|
|
101
106
|
/** The phone gate: a live paired-device cookie, or nothing else proceeds. */
|
|
102
107
|
const gateOk = (req: IncomingMessage): boolean => {
|
|
@@ -190,7 +195,7 @@ export function makeMobileApiRoutes(deps: MobileApiDeps): WebRoute[] {
|
|
|
190
195
|
} catch {
|
|
191
196
|
// The write failed; the close handler tears the subscription down.
|
|
192
197
|
}
|
|
193
|
-
},
|
|
198
|
+
}, eventsHeartbeatMs)
|
|
194
199
|
const onClose = (): void => {
|
|
195
200
|
if (closed) return
|
|
196
201
|
closed = true
|
package/src/routes.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
12
12
|
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
|
|
13
|
+
import { z, type ZodType } from 'zod'
|
|
13
14
|
import { UnknownLanAddressError, type PairingService, type PairingSnapshot } from './pairing.ts'
|
|
14
15
|
import { isLoopbackClient, readCookie } from './gate.ts'
|
|
15
16
|
|
|
@@ -91,6 +92,34 @@ export const PAIR_PATHS = {
|
|
|
91
92
|
events: '/api/pair/events',
|
|
92
93
|
} as const
|
|
93
94
|
|
|
95
|
+
/**
|
|
96
|
+
* /api/pair request payload contracts. Each POST endpoint validates its body
|
|
97
|
+
* against one of these instead of reaching into a hand-parsed object: the
|
|
98
|
+
* control-plane endpoints that carry no meaningful payload use the permissive
|
|
99
|
+
* pairActionPayloadSchema so their smoke calls keep working unchanged, while
|
|
100
|
+
* issue/accept enforce their optional/required fields. Unknown (extra) keys
|
|
101
|
+
* are tolerated exactly as the previous manual reads ignored them.
|
|
102
|
+
*/
|
|
103
|
+
export const issuePayloadSchema = z.object({
|
|
104
|
+
workspaceId: z.string().min(1).optional(),
|
|
105
|
+
address: z.string().min(1).optional(),
|
|
106
|
+
})
|
|
107
|
+
export const acceptPayloadSchema = z.object({
|
|
108
|
+
token: z.string().default(''),
|
|
109
|
+
})
|
|
110
|
+
export const pairActionPayloadSchema = z.object({}).passthrough()
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Parse a pair request body through schema. A missing/empty or non-object
|
|
114
|
+
* body is treated as an empty object (the same way the previous manual reads
|
|
115
|
+
* yielded absent fields), and a value that fails the schema returns
|
|
116
|
+
* `undefined` so the caller can answer with the existing error shape.
|
|
117
|
+
*/
|
|
118
|
+
function parsePairPayload<T>(schema: ZodType<T>, body: Record<string, unknown> | undefined): T | undefined {
|
|
119
|
+
const result = schema.safeParse(body ?? {})
|
|
120
|
+
return result.success ? result.data : undefined
|
|
121
|
+
}
|
|
122
|
+
|
|
94
123
|
/** One JSON response. */
|
|
95
124
|
function writeJson(res: ServerResponse, status: number, body: unknown): void {
|
|
96
125
|
const payload = JSON.stringify(body)
|
|
@@ -225,12 +254,12 @@ export function makeRoutes(deps: PairRoutesDeps): WebRoute[] {
|
|
|
225
254
|
return
|
|
226
255
|
}
|
|
227
256
|
const body = await readJsonBody(req)
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
:
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
257
|
+
const payload = parsePairPayload(issuePayloadSchema, body)
|
|
258
|
+
if (payload === undefined) {
|
|
259
|
+
writeJson(res, 400, { ok: false, code: 'bad-payload' })
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
const { workspaceId, address } = payload
|
|
234
263
|
try {
|
|
235
264
|
const { token, expiresAt } = service.issue(workspaceId, address)
|
|
236
265
|
// The default base is the public (tunneled) URL when configured — a
|
|
@@ -273,8 +302,12 @@ export function makeRoutes(deps: PairRoutesDeps): WebRoute[] {
|
|
|
273
302
|
return
|
|
274
303
|
}
|
|
275
304
|
const body = await readJsonBody(req)
|
|
276
|
-
const
|
|
277
|
-
|
|
305
|
+
const payload = parsePairPayload(acceptPayloadSchema, body)
|
|
306
|
+
if (payload === undefined) {
|
|
307
|
+
writeJson(res, 400, { ok: false, code: 'bad-payload' })
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
const result = service.accept(payload.token)
|
|
278
311
|
if (!result.ok) {
|
|
279
312
|
writeJson(res, result.code === 'used' ? 409 : 404, { ok: false, code: result.code })
|
|
280
313
|
return
|
|
@@ -294,7 +327,11 @@ export function makeRoutes(deps: PairRoutesDeps): WebRoute[] {
|
|
|
294
327
|
writeJson(res, 403, { ok: false, code: 'forbidden' })
|
|
295
328
|
return
|
|
296
329
|
}
|
|
297
|
-
await readJsonBody(req)
|
|
330
|
+
const body = await readJsonBody(req)
|
|
331
|
+
if (parsePairPayload(pairActionPayloadSchema, body) === undefined) {
|
|
332
|
+
writeJson(res, 400, { ok: false, code: 'bad-payload' })
|
|
333
|
+
return
|
|
334
|
+
}
|
|
298
335
|
service.stop()
|
|
299
336
|
writeJson(res, 200, { ok: true })
|
|
300
337
|
}
|
|
@@ -305,7 +342,11 @@ export function makeRoutes(deps: PairRoutesDeps): WebRoute[] {
|
|
|
305
342
|
writeJson(res, 403, { ok: false, code: 'forbidden' })
|
|
306
343
|
return
|
|
307
344
|
}
|
|
308
|
-
await readJsonBody(req)
|
|
345
|
+
const body = await readJsonBody(req)
|
|
346
|
+
if (parsePairPayload(pairActionPayloadSchema, body) === undefined) {
|
|
347
|
+
writeJson(res, 400, { ok: false, code: 'bad-payload' })
|
|
348
|
+
return
|
|
349
|
+
}
|
|
309
350
|
const deviceId = readCookie(req.headers.cookie, service.config.cookieName)
|
|
310
351
|
if (deviceId === undefined || !service.heartbeat(deviceId)) {
|
|
311
352
|
writeJson(res, 401, { ok: false, code: 'unpaired' })
|