dsh-mobile-access 1.0.2 → 1.1.0

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/package.json CHANGED
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "dsh-mobile-access",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Independent DSH bundle that pairs iPhones and iPads with the official DeepSeek Harness Web GUI over the local network.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/abbaababa/dsh-mobile-access.git"
10
+ },
11
+ "homepage": "https://github.com/abbaababa/dsh-mobile-access#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/abbaababa/dsh-mobile-access/issues"
14
+ },
7
15
  "publishConfig": {
8
16
  "access": "public"
9
17
  },
@@ -20,6 +28,7 @@
20
28
  "index.js",
21
29
  "service.js",
22
30
  "routes.js",
31
+ "gateway.js",
23
32
  "html.js",
24
33
  "types.js",
25
34
  "client.js",
@@ -48,5 +57,9 @@
48
57
  "@deepseek-ai/dsh-client-ui-sidebar"
49
58
  ]
50
59
  }
60
+ },
61
+ "devDependencies": {
62
+ "@types/node": "^26.6.5",
63
+ "typescript": "6.0.3"
51
64
  }
52
65
  }
package/routes.js CHANGED
@@ -4,6 +4,7 @@
4
4
  * can reach it; every state and rotation call stays inside Connection's fence.
5
5
  */
6
6
  import { contentSecurityPolicy, renderMessagePage, renderPairingPage } from './html.js';
7
+ import { EMPTY_GATEWAY_STATUS } from './gateway.js';
7
8
  import { constantTimeEqual, isLoopbackHost, } from './service.js';
8
9
  const PAIR_PATH = '/m';
9
10
  const STATE_PATH = '/api/mobile-access/state';
@@ -182,27 +183,36 @@ function jsonResponse(payload, status = 200) {
182
183
  },
183
184
  });
184
185
  }
185
- function statePayload(ctx, service) {
186
+ function publicOriginOf(gateway) {
187
+ const status = gateway?.status();
188
+ return status?.running === true && status.publicOrigin !== null ? status.publicOrigin : null;
189
+ }
190
+ function statePayload(ctx, service, gateway) {
186
191
  const origin = originOf(ctx);
192
+ const gatewayStatus = gateway?.status() ?? EMPTY_GATEWAY_STATUS;
193
+ const publicOrigin = publicOriginOf(gateway) ?? origin;
187
194
  const snapshot = service.snapshot();
188
195
  return {
189
196
  ...snapshot,
190
197
  ok: true,
191
198
  version: 1,
192
199
  origin,
200
+ publicOrigin,
193
201
  pairPath: PAIR_PATH,
194
- pairUrl: new URL(PAIR_PATH, origin).href,
202
+ pairUrl: new URL(PAIR_PATH, publicOrigin).href,
195
203
  bindHost: ctx.webServer.host,
196
204
  protocol: protocolOf(ctx),
197
205
  port: ctx.webServer.port,
198
206
  loopback: isLoopbackHost(ctx.webServer.host),
207
+ gateway: gatewayStatus,
199
208
  };
200
209
  }
201
- function pairingPage(ctx, service, request, nonce, error) {
210
+ function pairingPage(ctx, service, request, nonce, gateway, error) {
202
211
  const snapshot = service.snapshot();
212
+ const publicOrigin = publicOriginOf(gateway);
203
213
  return renderPairingPage({
204
214
  ...(error === undefined ? {} : { error }),
205
- host: requestHost(ctx, request),
215
+ host: publicOrigin === null ? requestHost(ctx, request) : new URL(publicOrigin).host,
206
216
  expiresAt: snapshot.expiresAt,
207
217
  expired: snapshot.expired,
208
218
  locked: snapshot.locked,
@@ -211,10 +221,10 @@ function pairingPage(ctx, service, request, nonce, error) {
211
221
  nonce,
212
222
  });
213
223
  }
214
- function sendPairing(ctx, service, request, response, error, status = 200, extraHeaders = {}) {
224
+ function sendPairing(ctx, service, request, response, gateway, error, status = 200, extraHeaders = {}) {
215
225
  const nonce = pairingNonce();
216
226
  response.setHeader('set-cookie', pairingCookie(nonce));
217
- sendHtml(response, status, pairingPage(ctx, service, request, nonce, error), extraHeaders);
227
+ sendHtml(response, status, pairingPage(ctx, service, request, nonce, gateway, error), extraHeaders);
218
228
  }
219
229
  function refusalPage(title, message, detail) {
220
230
  return renderMessagePage({
@@ -296,7 +306,7 @@ function failureMessage(reason) {
296
306
  return '尝试过于频繁,请稍后再试。';
297
307
  }
298
308
  }
299
- async function handleGet(ctx, service, request, response) {
309
+ async function handleGet(ctx, service, request, response, gateway) {
300
310
  const captured = new CapturedResponse();
301
311
  const allowed = ctx.connection.authorizeIndex({
302
312
  method: request.method,
@@ -309,9 +319,9 @@ async function handleGet(ctx, service, request, response) {
309
319
  }
310
320
  if (replayCaptured(response, captured))
311
321
  return;
312
- sendPairing(ctx, service, request, response);
322
+ sendPairing(ctx, service, request, response, gateway);
313
323
  }
314
- async function handlePost(ctx, service, request, response) {
324
+ async function handlePost(ctx, service, request, response, gateway) {
315
325
  const contentType = (firstHeader(request.headers, 'content-type') ?? '').toLowerCase();
316
326
  if (!contentType.startsWith('application/x-www-form-urlencoded')) {
317
327
  sendHtml(response, 415, refusalPage('无法处理请求', '配对请求必须使用表单提交。'));
@@ -333,7 +343,7 @@ async function handlePost(ctx, service, request, response) {
333
343
  const nonce = params.get('nonce') ?? '';
334
344
  const expectedNonce = cookieValue(request.headers, PAIR_COOKIE) ?? '';
335
345
  if (nonce === '' || expectedNonce === '' || !constantTimeEqual(nonce, expectedNonce)) {
336
- sendPairing(ctx, service, request, response, '配对页已过期,请刷新后重试。', 403);
346
+ sendPairing(ctx, service, request, response, gateway, '配对页已过期,请刷新后重试。', 403);
337
347
  return;
338
348
  }
339
349
  const code = params.get('code') ?? '';
@@ -342,7 +352,7 @@ async function handlePost(ctx, service, request, response) {
342
352
  const extra = result.reason === 'rate'
343
353
  ? { 'retry-after': String(Math.max(1, Math.ceil((result.retryAfterMs ?? 1000) / 1000))) }
344
354
  : {};
345
- sendPairing(ctx, service, request, response, failureMessage(result.reason), result.reason === 'rate' ? 429 : 200, extra);
355
+ sendPairing(ctx, service, request, response, gateway, failureMessage(result.reason), result.reason === 'rate' ? 429 : 200, extra);
346
356
  return;
347
357
  }
348
358
  const minted = mintIndexCookie(ctx, request);
@@ -364,7 +374,7 @@ async function handlePost(ctx, service, request, response) {
364
374
  * Build the single exact `/m` handler. Exported for route-level tests, which do
365
375
  * not need a live DSH process.
366
376
  */
367
- export function createPairingHandler(ctx, service) {
377
+ export function createPairingHandler(ctx, service, gateway) {
368
378
  return async (request, response) => {
369
379
  if (!pairingHostAllowed(ctx, request)) {
370
380
  sendHtml(response, 403, refusalPage('请求被拒绝', '此请求未通过 Host 校验。'));
@@ -372,11 +382,11 @@ export function createPairingHandler(ctx, service) {
372
382
  }
373
383
  const method = (request.method ?? 'GET').toUpperCase();
374
384
  if (method === 'GET') {
375
- await handleGet(ctx, service, request, response);
385
+ await handleGet(ctx, service, request, response, gateway);
376
386
  return;
377
387
  }
378
388
  if (method === 'POST') {
379
- await handlePost(ctx, service, request, response);
389
+ await handlePost(ctx, service, request, response, gateway);
380
390
  return;
381
391
  }
382
392
  response.setHeader('allow', 'GET, POST');
@@ -387,7 +397,7 @@ export function createPairingHandler(ctx, service) {
387
397
  * Register all three routes. The returned disposer removes them in reverse
388
398
  * order; every registration is attempted only once.
389
399
  */
390
- export function registerMobileAccess(ctx, service) {
400
+ export function registerMobileAccess(ctx, service, gateway) {
391
401
  let pairing;
392
402
  let state;
393
403
  let rotate;
@@ -395,7 +405,7 @@ export function registerMobileAccess(ctx, service) {
395
405
  path: STATE_PATH,
396
406
  methods: ['GET'],
397
407
  requestBody: 'buffered',
398
- fetch: () => Promise.resolve(jsonResponse(statePayload(ctx, service))),
408
+ fetch: () => Promise.resolve(jsonResponse(statePayload(ctx, service, gateway))),
399
409
  };
400
410
  const rotateRoute = {
401
411
  path: ROTATE_PATH,
@@ -403,14 +413,14 @@ export function registerMobileAccess(ctx, service) {
403
413
  requestBody: 'buffered',
404
414
  fetch: () => {
405
415
  service.rotate();
406
- return Promise.resolve(jsonResponse(statePayload(ctx, service)));
416
+ return Promise.resolve(jsonResponse(statePayload(ctx, service, gateway)));
407
417
  },
408
418
  };
409
419
  try {
410
420
  pairing = ctx.webServer.register({
411
421
  kind: 'exact',
412
422
  path: PAIR_PATH,
413
- handler: createPairingHandler(ctx, service),
423
+ handler: createPairingHandler(ctx, service, gateway),
414
424
  });
415
425
  state = ctx.connection.fetch.register(stateRoute);
416
426
  rotate = ctx.connection.fetch.register(rotateRoute);