@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/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
- }, 15_000)
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 workspaceId = body === undefined || typeof body.workspaceId !== 'string' || body.workspaceId === ''
229
- ? undefined
230
- : body.workspaceId
231
- const address = body === undefined || typeof body.address !== 'string' || body.address === ''
232
- ? undefined
233
- : body.address
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 token = typeof body?.token === 'string' ? body.token : ''
277
- const result = service.accept(token)
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' })