dsh-mobile-plugin 0.1.100 → 0.2.1

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.
Files changed (59) hide show
  1. package/README.md +14 -5
  2. package/dsh-mobile-credentials/lib/credentials.js +405 -84
  3. package/lib/client.js +31 -8
  4. package/lib/http/routing.js +165 -9
  5. package/lib/impl.js +88 -9
  6. package/lib/index.js +3 -2
  7. package/lib/mirror/pusher.cjs +35 -4
  8. package/lib/mobile/approvals.js +62 -15
  9. package/lib/mobile/auth.js +46 -26
  10. package/lib/mobile/credential-upgrade.js +137 -0
  11. package/lib/mobile/entry-window.js +32 -0
  12. package/lib/mobile/events.js +14 -4
  13. package/lib/mobile/extensions.js +279 -0
  14. package/lib/mobile/history-derive.js +209 -11
  15. package/lib/mobile/host-bridge/event-synthesis.js +61 -10
  16. package/lib/mobile/pairing.js +150 -64
  17. package/lib/mobile/plugin-self.js +187 -0
  18. package/lib/mobile/push-notify-route.js +37 -0
  19. package/lib/mobile/push-notify.js +84 -0
  20. package/lib/mobile/questions.js +75 -10
  21. package/lib/mobile/relay-control.js +547 -0
  22. package/lib/mobile/session-cache.js +60 -2
  23. package/lib/mobile/session-file.js +133 -34
  24. package/lib/mobile/session-list-shortcut.js +38 -0
  25. package/lib/mobile/session-model-options.js +3 -1
  26. package/lib/mobile/session-models.js +15 -3
  27. package/lib/mobile/session-send.js +30 -27
  28. package/lib/mobile/session-snapshot.js +12 -12
  29. package/lib/mobile/sessions.js +20 -0
  30. package/lib/mobile/stats-collect.cjs +55 -12
  31. package/lib/mobile/stats-ledger.cjs +140 -21
  32. package/lib/mobile/stream-registry.js +73 -0
  33. package/lib/mobile/stream.js +382 -31
  34. package/lib/mobile/subagents.js +11 -2
  35. package/lib/mobile/uploads.js +91 -5
  36. package/lib/mobile/workspaces.js +39 -8
  37. package/lib/panel/index.html +11 -11
  38. package/lib/relay/core.js +28 -2
  39. package/lib/relay/lan-network.cjs +47 -0
  40. package/lib/relay/lan-proxy.js +283 -122
  41. package/lib/relay/protocol.js +4 -2
  42. package/lib/relay/tunnel.js +20 -14
  43. package/lib/routes/control-plane.js +139 -9
  44. package/lib/routes/mobile-api.js +92 -9
  45. package/lib/shared/e2ee-pair.js +91 -0
  46. package/lib/shared/e2ee-plain.js +79 -0
  47. package/lib/shared/e2ee-request.js +331 -0
  48. package/lib/shared/e2ee.js +173 -0
  49. package/lib/shared/http-util.js +27 -4
  50. package/lib/shared/list-signal.js +16 -0
  51. package/lib/shared/pair-throttle.js +150 -0
  52. package/lib/shared/sse-write.js +16 -5
  53. package/lib/shared/via-header.js +65 -0
  54. package/lib/shell/runtime.js +147 -42
  55. package/lib/shell/self-check-after-swap.js +93 -0
  56. package/lib/shell/updater.js +44 -3
  57. package/lib/sidecar-facade-worker.cjs +70 -6
  58. package/lib/update-install.cjs +13 -5
  59. package/package.json +3 -2
package/lib/relay/core.js CHANGED
@@ -27,9 +27,16 @@ const MOBILE_BEARER_TTL_SEC = 365 * 24 * 3600;
27
27
 
28
28
  function genInstanceId() { return crypto.randomBytes(4).toString('hex'); }
29
29
  function genPairingCode() { return String(crypto.randomInt(0, 1000000)).padStart(6, '0'); }
30
+ /**
31
+ * 二维码里那张一次性票据:32 字节随机数的 base64url(43 字符,无补位)。
32
+ *
33
+ * 它是**密钥材料**:手机拿它派生配对密钥,所以谁看到二维码谁就能配一次。它由宿主自己
34
+ * 生成、写进二维码、绝不经过中转 —— 中转连它长什么样都看不到。
35
+ */
36
+ function genPairingTicket() { return crypto.randomBytes(32).toString('base64url'); }
30
37
  function defaultState() {
31
38
  return {
32
- instanceId: genInstanceId(), pairingCode: genPairingCode(), relayUrl: '', name: os.hostname(),
39
+ instanceId: genInstanceId(), pairingCode: genPairingCode(), pairingTicket: genPairingTicket(), relayUrl: '', name: os.hostname(),
33
40
  lanEnabled: true, lanPort: 18787, lanSecret: crypto.randomBytes(32).toString('hex'), enabled: false,
34
41
  // 中转侧 TUNNEL_REGISTER_SECRET 的对偶值。为空时隧道注册会被中转以
35
42
  // close(1008, 'bad registration secret') 拒掉(除非中转根本没开校验)。
@@ -37,6 +44,9 @@ function defaultState() {
37
44
  // 手机优先用哪条地址(空 = 不指定)。它是 `RemoteControlSettingsSchema` 里的一项,
38
45
  // 写它不碰 sidecar(那份配置是 `impl.js` 的 `buildConfig` 逐字段拼的,不带它)。
39
46
  preferredLanAddress: '',
47
+ // 用户手填的直连地址(空 = 没填)。与上面那条同一条口径:也是 schema 里的一项、
48
+ // 也不进 sidecar 配置 —— 它只影响「报给手机的候选」与二维码,不影响隧道怎么连。
49
+ manualLanUrl: '',
40
50
  // 首次接入引导走完没有。**刻意不进 RemoteControlSettingsSchema**:它不是一项
41
51
  // 「远程控制设置」,改它不该触发 sidecar 重配(那条链上每加一个字段都要同步改
42
52
  // schema、worker 的 allowed 数组与 buildConfig,漏一处就抛 unknown field)。
@@ -55,6 +65,22 @@ function loadState() {
55
65
  return state;
56
66
  }
57
67
  }
68
+ /**
69
+ * 配对成功之后换掉配对码与票据(§5.1)。
70
+ *
71
+ * **两件必须同一个动作**:只换码不换票据,那张截图里的票据还能再配一次;只换票据不换码,
72
+ * 旧码在旧形状那条路上还能用。所以这里一次改两个字段、一次落盘。
73
+ *
74
+ * 放在这里而不是装配层:它只碰 state,是可单测的纯状态变更;装配层只负责在写完之后
75
+ * 让旁路重读配置。
76
+ */
77
+ function rotatePairing(state) {
78
+ state.pairingCode = genPairingCode();
79
+ state.pairingTicket = genPairingTicket();
80
+ saveState(state);
81
+ return { pairingCode: state.pairingCode, pairingTicket: state.pairingTicket };
82
+ }
83
+
58
84
  function saveState(state) {
59
85
  fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
60
86
  const tmp = `${STATE_FILE}.tmp`;
@@ -80,6 +106,6 @@ function signLanToken(secret, instanceId, ttlSec = MOBILE_TOKEN_TTL_SEC) { retur
80
106
  function verifyLanToken(secret, token, instanceId) { return sharedToken.verifyLanToken(secret, token, instanceId); }
81
107
 
82
108
  module.exports = {
83
- STATE_FILE, MOBILE_TOKEN_TTL_SEC, MOBILE_BEARER_TTL_SEC, loadState, saveState, genInstanceId, genPairingCode,
109
+ STATE_FILE, MOBILE_TOKEN_TTL_SEC, MOBILE_BEARER_TTL_SEC, loadState, saveState, rotatePairing, genInstanceId, genPairingCode, genPairingTicket,
84
110
  tunnelWsUrl, signLanToken, verifyLanToken,
85
111
  };
@@ -344,6 +344,39 @@ function advertisedCandidates(port, interfaces = os.networkInterfaces()) {
344
344
  }
345
345
  }
346
346
 
347
+ /**
348
+ * 用户**自己填的**直连地址(设置项 `manualLanUrl`,通常是内网穿透域名)→ 候选。
349
+ *
350
+ * 为什么需要它:网卡扫描只能产出这台机器身上的 IP,域名一条都扫不出来。手机在「网络」页
351
+ * 手填的地址从前只活在手机自己那份会话里 —— 重启、换网、重新配对之后全没了,而且只要同网
352
+ * 还有一条扫出来的 IP 更快,手填的那条还会被顶掉。放进宿主配置之后,候选的唯一真源就回到
353
+ * 宿主:手机每次核对连接信息都重新拿到它,二维码里也带着它。
354
+ *
355
+ * 只挡明显不能用的:空、非 http(s)、解析不出主机。不校验端口、不要求是 IP —— 穿透域名后面
356
+ * 接的是什么由反代决定,宿主无从判断也没必要判断。
357
+ */
358
+ function manualCandidate(value) {
359
+ const url = typeof value === 'string' ? value.trim().replace(/\/+$/, '') : ''
360
+ if (!url || !/^https?:\/\/[^\s/]+/i.test(url)) return null
361
+ let host = ''
362
+ try { host = new URL(url).hostname } catch { return null }
363
+ if (!host) return null
364
+ // `address` 与网卡候选同形(面板按裸地址显示、`orderCandidates` 按地址匹配)。
365
+ return { url, address: host, interface: 'manual', family: 'IPv4' }
366
+ }
367
+
368
+ /**
369
+ * 候选 = 用户填的那条(有的话)在前 + 其余照旧。按 url 去重,**不删**任何一条 NIC 候选:
370
+ * 穿透域名不通时手机还得能顺着网卡地址摸回这台机器。值不合法时原样返回 NIC 候选,
371
+ * 不让一条坏配置把整份候选清空(那正是彻底失联的形状)。
372
+ */
373
+ function withManualCandidate(list, value) {
374
+ const manual = manualCandidate(value)
375
+ const rest = Array.isArray(list) ? list : []
376
+ if (!manual) return rest
377
+ return [manual, ...rest.filter(item => item && item.url !== manual.url)]
378
+ }
379
+
347
380
  /**
348
381
  * 把用户选中的那条排到最前面(面板上的「手机优先用这条」,存在设置项 `preferredLanAddress`)。
349
382
  *
@@ -371,6 +404,17 @@ function orderCandidates(list, preferred) {
371
404
  return ordered
372
405
  }
373
406
 
407
+ /**
408
+ * 报给手机的直连候选 —— 二维码、配对响应与 `/connection-info` 三处**只许走这一份**。
409
+ *
410
+ * 两步顺序不能反:先让「手机优先用这条」调网卡候选的顺序,再把用户手填的地址盖到最前
411
+ * (那条是用户在手机上手打的,`preferredLanAddress` 只是面板上对网卡地址的选择,压不过它)。
412
+ * 三处各写一遍的结果就是「画面上说这条、手机去试另一条」。
413
+ */
414
+ function phoneCandidates(list, options = {}) {
415
+ return withManualCandidate(orderCandidates(list, options.preferred || ''), options.manual || '')
416
+ }
417
+
374
418
  module.exports = {
375
419
  ipv4Number,
376
420
  ipv6Number,
@@ -391,5 +435,8 @@ module.exports = {
391
435
  ipv6Urls,
392
436
  ipv6Candidates,
393
437
  advertisedCandidates,
438
+ manualCandidate,
439
+ withManualCandidate,
394
440
  orderCandidates,
441
+ phoneCandidates,
395
442
  }