@linxin666/dsh-remote-web-ui 0.3.6 → 0.3.9

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 (238) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +77 -468
  3. package/README.zh.md +83 -113
  4. package/lib/client.js +955 -203
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +1136 -1466
  7. package/lib/types/client/FooterRemoteEntry.d.ts +6 -7
  8. package/lib/types/client/FooterRemoteEntry.d.ts.map +1 -1
  9. package/lib/types/client/FooterRemoteEntry.js +1 -1
  10. package/lib/types/client/RemoteEntry.d.ts +7 -4
  11. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  12. package/lib/types/client/RemoteEntry.js +7 -10
  13. package/lib/types/client/RemotePanel.d.ts +2 -2
  14. package/lib/types/client/RemotePanel.d.ts.map +1 -1
  15. package/lib/types/client/RemotePanel.js +2 -2
  16. package/lib/types/client/RemoteSettingsCard.d.ts +9 -5
  17. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  18. package/lib/types/client/RemoteSettingsCard.js +56 -3
  19. package/lib/types/client/deep-link.d.ts +10 -13
  20. package/lib/types/client/deep-link.d.ts.map +1 -1
  21. package/lib/types/client/deep-link.js +14 -74
  22. package/lib/types/client/index.d.ts +2 -6
  23. package/lib/types/client/index.d.ts.map +1 -1
  24. package/lib/types/client/index.js +73 -39
  25. package/lib/types/client/locales.d.ts +26 -12
  26. package/lib/types/client/locales.d.ts.map +1 -1
  27. package/lib/types/client/locales.js +36 -22
  28. package/lib/types/client/mobile-adapt.d.ts +40 -0
  29. package/lib/types/client/mobile-adapt.d.ts.map +1 -0
  30. package/lib/types/client/mobile-adapt.js +967 -0
  31. package/lib/types/client/pair-api.d.ts +24 -6
  32. package/lib/types/client/pair-api.d.ts.map +1 -1
  33. package/lib/types/client/pair-api.js +9 -14
  34. package/lib/types/client/remote-channel.d.ts +3 -0
  35. package/lib/types/client/remote-channel.d.ts.map +1 -1
  36. package/lib/types/client/remote-channel.js +35 -3
  37. package/lib/types/client/settings-form.d.ts +35 -43
  38. package/lib/types/client/settings-form.d.ts.map +1 -1
  39. package/lib/types/client/settings-form.js +60 -53
  40. package/lib/types/firewall.d.ts +75 -0
  41. package/lib/types/firewall.d.ts.map +1 -0
  42. package/lib/types/firewall.js +184 -0
  43. package/lib/types/gate.d.ts +11 -5
  44. package/lib/types/gate.d.ts.map +1 -1
  45. package/lib/types/gate.js +11 -5
  46. package/lib/types/index.d.ts +39 -10
  47. package/lib/types/index.d.ts.map +1 -1
  48. package/lib/types/index.js +206 -58
  49. package/lib/types/inner-auth.d.ts +32 -0
  50. package/lib/types/inner-auth.d.ts.map +1 -0
  51. package/lib/types/inner-auth.js +72 -0
  52. package/lib/types/lan-bind-plan.d.ts +55 -0
  53. package/lib/types/lan-bind-plan.d.ts.map +1 -0
  54. package/lib/types/lan-bind-plan.js +60 -0
  55. package/lib/types/lan-bind.d.ts +69 -0
  56. package/lib/types/lan-bind.d.ts.map +1 -0
  57. package/lib/types/lan-bind.js +128 -0
  58. package/lib/types/loopback-proxy.d.ts +9 -2
  59. package/lib/types/loopback-proxy.d.ts.map +1 -1
  60. package/lib/types/loopback-proxy.js +24 -2
  61. package/lib/types/pairing.d.ts +15 -3
  62. package/lib/types/pairing.d.ts.map +1 -1
  63. package/lib/types/pairing.js +20 -6
  64. package/lib/types/remote-api.d.ts +30 -13
  65. package/lib/types/remote-api.d.ts.map +1 -1
  66. package/lib/types/remote-api.js +55 -42
  67. package/lib/types/remote-channel-boot.d.ts +21 -2
  68. package/lib/types/remote-channel-boot.d.ts.map +1 -1
  69. package/lib/types/remote-channel-boot.js +65 -3
  70. package/lib/types/remote-channel-rules.d.ts +8 -0
  71. package/lib/types/remote-channel-rules.d.ts.map +1 -1
  72. package/lib/types/remote-channel-rules.js +9 -2
  73. package/lib/types/remote-methods.d.ts +45 -11
  74. package/lib/types/remote-methods.d.ts.map +1 -1
  75. package/lib/types/remote-methods.js +56 -27
  76. package/lib/types/remote-presence-pet.d.ts +51 -0
  77. package/lib/types/remote-presence-pet.d.ts.map +1 -0
  78. package/lib/types/remote-presence-pet.js +101 -0
  79. package/lib/types/routes.d.ts +76 -4
  80. package/lib/types/routes.d.ts.map +1 -1
  81. package/lib/types/routes.js +336 -19
  82. package/lib/types/tunnel.d.ts.map +1 -1
  83. package/lib/types/tunnel.js +7 -1
  84. package/package.json +20 -18
  85. package/src/client/FooterRemoteEntry.tsx +9 -10
  86. package/src/client/RemoteEntry.tsx +13 -14
  87. package/src/client/RemotePanel.tsx +5 -16
  88. package/src/client/RemoteSettingsCard.tsx +72 -13
  89. package/src/client/deep-link.ts +14 -75
  90. package/src/client/index.ts +77 -39
  91. package/src/client/locales.ts +36 -22
  92. package/src/client/mobile-adapt.ts +913 -0
  93. package/src/client/pair-api.ts +26 -15
  94. package/src/client/remote-channel.ts +31 -3
  95. package/src/client/settings-form.ts +78 -92
  96. package/src/dsh-home.ts +1 -1
  97. package/src/firewall.ts +230 -0
  98. package/src/gate.ts +11 -5
  99. package/src/index.ts +235 -65
  100. package/src/inner-auth.ts +85 -0
  101. package/src/lan-bind-plan.ts +81 -0
  102. package/src/lan-bind.ts +145 -0
  103. package/src/loopback-proxy.ts +34 -0
  104. package/src/pairing.persist.test.ts +1 -1
  105. package/src/pairing.ts +20 -6
  106. package/src/remote-api.ts +67 -45
  107. package/src/remote-channel-boot.ts +70 -3
  108. package/src/remote-channel-rules.ts +17 -2
  109. package/src/remote-methods.ts +58 -27
  110. package/src/remote-presence-pet.ts +129 -0
  111. package/src/routes.ts +358 -19
  112. package/src/tunnel.ts +7 -1
  113. package/assets/apple-touch-icon.png +0 -0
  114. package/assets/mobile-icon-192.png +0 -0
  115. package/assets/mobile-icon-512.png +0 -0
  116. package/assets/mobile-service-worker.js +0 -63
  117. package/lib/mobile.js +0 -17658
  118. package/lib/mobile.js.map +0 -1
  119. package/lib/types/mobile/PairRequiredView.d.ts +0 -7
  120. package/lib/types/mobile/PairRequiredView.d.ts.map +0 -1
  121. package/lib/types/mobile/PairRequiredView.js +0 -28
  122. package/lib/types/mobile/PairRequiredView.test.d.ts +0 -2
  123. package/lib/types/mobile/PairRequiredView.test.d.ts.map +0 -1
  124. package/lib/types/mobile/PairRequiredView.test.js +0 -24
  125. package/lib/types/mobile/api.d.ts +0 -145
  126. package/lib/types/mobile/api.d.ts.map +0 -1
  127. package/lib/types/mobile/api.js +0 -103
  128. package/lib/types/mobile/index.d.ts +0 -10
  129. package/lib/types/mobile/index.d.ts.map +0 -1
  130. package/lib/types/mobile/index.js +0 -38
  131. package/lib/types/mobile/markdown.d.ts +0 -34
  132. package/lib/types/mobile/markdown.d.ts.map +0 -1
  133. package/lib/types/mobile/markdown.js +0 -285
  134. package/lib/types/mobile/markdown.test.d.ts +0 -2
  135. package/lib/types/mobile/markdown.test.d.ts.map +0 -1
  136. package/lib/types/mobile/markdown.test.js +0 -79
  137. package/lib/types/mobile/messages.d.ts +0 -149
  138. package/lib/types/mobile/messages.d.ts.map +0 -1
  139. package/lib/types/mobile/messages.js +0 -561
  140. package/lib/types/mobile/messages.test.d.ts +0 -2
  141. package/lib/types/mobile/messages.test.d.ts.map +0 -1
  142. package/lib/types/mobile/messages.test.js +0 -356
  143. package/lib/types/mobile/mobile-styles.d.ts +0 -6
  144. package/lib/types/mobile/mobile-styles.d.ts.map +0 -1
  145. package/lib/types/mobile/mobile-styles.js +0 -1438
  146. package/lib/types/mobile/mobile-theme.d.ts +0 -22
  147. package/lib/types/mobile/mobile-theme.d.ts.map +0 -1
  148. package/lib/types/mobile/mobile-theme.js +0 -70
  149. package/lib/types/mobile/mobile-theme.test.d.ts +0 -2
  150. package/lib/types/mobile/mobile-theme.test.d.ts.map +0 -1
  151. package/lib/types/mobile/mobile-theme.test.js +0 -71
  152. package/lib/types/mobile/mux.d.ts +0 -128
  153. package/lib/types/mobile/mux.d.ts.map +0 -1
  154. package/lib/types/mobile/mux.js +0 -305
  155. package/lib/types/mobile/mux.test.d.ts +0 -2
  156. package/lib/types/mobile/mux.test.d.ts.map +0 -1
  157. package/lib/types/mobile/mux.test.js +0 -267
  158. package/lib/types/mobile/pairing.d.ts +0 -31
  159. package/lib/types/mobile/pairing.d.ts.map +0 -1
  160. package/lib/types/mobile/pairing.js +0 -62
  161. package/lib/types/mobile/rpc.d.ts +0 -38
  162. package/lib/types/mobile/rpc.d.ts.map +0 -1
  163. package/lib/types/mobile/rpc.js +0 -83
  164. package/lib/types/mobile/theme-toggle.d.ts +0 -12
  165. package/lib/types/mobile/theme-toggle.d.ts.map +0 -1
  166. package/lib/types/mobile/theme-toggle.js +0 -26
  167. package/lib/types/mobile/views/App.d.ts +0 -57
  168. package/lib/types/mobile/views/App.d.ts.map +0 -1
  169. package/lib/types/mobile/views/App.js +0 -150
  170. package/lib/types/mobile/views/App.test.d.ts +0 -2
  171. package/lib/types/mobile/views/App.test.d.ts.map +0 -1
  172. package/lib/types/mobile/views/App.test.js +0 -52
  173. package/lib/types/mobile/views/ChatView.d.ts +0 -63
  174. package/lib/types/mobile/views/ChatView.d.ts.map +0 -1
  175. package/lib/types/mobile/views/ChatView.js +0 -821
  176. package/lib/types/mobile/views/ChatView.stream.test.d.ts +0 -2
  177. package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +0 -1
  178. package/lib/types/mobile/views/ChatView.stream.test.js +0 -236
  179. package/lib/types/mobile/views/ChatView.test.d.ts +0 -2
  180. package/lib/types/mobile/views/ChatView.test.d.ts.map +0 -1
  181. package/lib/types/mobile/views/ChatView.test.js +0 -718
  182. package/lib/types/mobile/views/SessionListView.d.ts +0 -27
  183. package/lib/types/mobile/views/SessionListView.d.ts.map +0 -1
  184. package/lib/types/mobile/views/SessionListView.js +0 -139
  185. package/lib/types/mobile/views/SessionListView.test.d.ts +0 -2
  186. package/lib/types/mobile/views/SessionListView.test.d.ts.map +0 -1
  187. package/lib/types/mobile/views/SessionListView.test.js +0 -167
  188. package/lib/types/mobile/views/WorkspaceView.d.ts +0 -21
  189. package/lib/types/mobile/views/WorkspaceView.d.ts.map +0 -1
  190. package/lib/types/mobile/views/WorkspaceView.js +0 -93
  191. package/lib/types/mobile/views/WorkspaceView.test.d.ts +0 -2
  192. package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +0 -1
  193. package/lib/types/mobile/views/WorkspaceView.test.js +0 -183
  194. package/lib/types/mobile-api.d.ts +0 -65
  195. package/lib/types/mobile-api.d.ts.map +0 -1
  196. package/lib/types/mobile-api.js +0 -404
  197. package/lib/types/mobile-pending.d.ts +0 -38
  198. package/lib/types/mobile-pending.d.ts.map +0 -1
  199. package/lib/types/mobile-pending.js +0 -59
  200. package/lib/types/mobile-pwa.d.ts +0 -6
  201. package/lib/types/mobile-pwa.d.ts.map +0 -1
  202. package/lib/types/mobile-pwa.js +0 -17
  203. package/lib/types/mobile-routes.d.ts +0 -13
  204. package/lib/types/mobile-routes.d.ts.map +0 -1
  205. package/lib/types/mobile-routes.js +0 -217
  206. package/lib/types/paired-model-catalog.d.ts +0 -17
  207. package/lib/types/paired-model-catalog.d.ts.map +0 -1
  208. package/lib/types/paired-model-catalog.js +0 -311
  209. package/src/mobile/PairRequiredView.test.tsx +0 -28
  210. package/src/mobile/PairRequiredView.tsx +0 -58
  211. package/src/mobile/api.ts +0 -229
  212. package/src/mobile/index.tsx +0 -41
  213. package/src/mobile/markdown.test.ts +0 -91
  214. package/src/mobile/markdown.ts +0 -291
  215. package/src/mobile/messages.test.ts +0 -402
  216. package/src/mobile/messages.ts +0 -685
  217. package/src/mobile/mobile-styles.ts +0 -1439
  218. package/src/mobile/mobile-theme.test.ts +0 -84
  219. package/src/mobile/mobile-theme.ts +0 -76
  220. package/src/mobile/mux.test.ts +0 -314
  221. package/src/mobile/mux.ts +0 -336
  222. package/src/mobile/pairing.ts +0 -77
  223. package/src/mobile/rpc.ts +0 -90
  224. package/src/mobile/theme-toggle.tsx +0 -47
  225. package/src/mobile/views/App.test.tsx +0 -62
  226. package/src/mobile/views/App.tsx +0 -213
  227. package/src/mobile/views/ChatView.stream.test.tsx +0 -268
  228. package/src/mobile/views/ChatView.test.tsx +0 -816
  229. package/src/mobile/views/ChatView.tsx +0 -1342
  230. package/src/mobile/views/SessionListView.test.tsx +0 -207
  231. package/src/mobile/views/SessionListView.tsx +0 -244
  232. package/src/mobile/views/WorkspaceView.test.tsx +0 -227
  233. package/src/mobile/views/WorkspaceView.tsx +0 -245
  234. package/src/mobile-api.ts +0 -418
  235. package/src/mobile-pending.ts +0 -88
  236. package/src/mobile-pwa.ts +0 -19
  237. package/src/mobile-routes.ts +0 -231
  238. package/src/paired-model-catalog.ts +0 -272
@@ -10,20 +10,20 @@
10
10
  import { createRequire } from 'node:module';
11
11
  import { join } from 'node:path';
12
12
  import { setInterval as nodeSetInterval, setTimeout as nodeSetTimeout } from 'node:timers';
13
- import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings';
14
13
  import z from 'schemastery';
15
14
  import { DEFAULT_IDLE_EXPIRE_MS, PairingService } from "./pairing.js";
16
15
  import { dshHome } from "./dsh-home.js";
17
16
  import { isPairedDeviceRequest, makeGateListener } from "./gate.js";
18
17
  import { RemoteWebUiPairing } from "./pairing-access.js";
19
18
  import { isTrustedApiRequest, makeRoutes } from "./routes.js";
20
- import { makeMobileRoutes } from "./mobile-routes.js";
21
- import { makeMobileApiRoutes } from "./mobile-api.js";
22
- import { PendingTracker } from "./mobile-pending.js";
23
- import { makePairedModelCatalogRoutes } from "./paired-model-catalog.js";
24
19
  import { makeRemoteApiRoutes, makeRemoteApiUpgradeRoutes } from "./remote-api.js";
20
+ import { startRemotePresencePet } from "./remote-presence-pet.js";
25
21
  import { claimPostureKey, postureTargets, probePosture, releasePostureKey } from "./posture.js";
26
22
  import { lanIPv4Addresses } from "./lan.js";
23
+ import { ensureFirewallRule, firewallSummary, removeFirewallRule } from "./firewall.js";
24
+ import { lanBindState, writeLanBind } from "./lan-bind.js";
25
+ import { desiredBindHost, desiredBindPort, firewallActionNeeded, pendingRestartOf } from "./lan-bind-plan.js";
26
+ import { createInnerAuth } from "./inner-auth.js";
27
27
  import { TunnelManager } from "./tunnel.js";
28
28
  import { checkUpdates, fetchGitHubReleaseNotes, fetchLatestVersion, RELEASE_NOTES_CACHE_TTL_MS, resolveAnchorManifest, resolveUpdateTarget, runUpdateVerified, } from "./update.js";
29
29
  import { makeUpdateRoutes } from "./update-routes.js";
@@ -33,13 +33,13 @@ import { UUID_POLYFILL_SCRIPT } from "./uuid-polyfill.js";
33
33
  /** Stable cordis plugin name. */
34
34
  export const name = 'remote-web-ui';
35
35
  /** Services required before the pairing surfaces can mount. */
36
- export const inject = ['webServer', 'apiProxy', 'commands', 'agents'];
36
+ export const inject = ['webServer', 'typertGateway', 'connection'];
37
37
  /**
38
38
  * Settings namespace of the remote-control capability — the section the web
39
39
  * settings surface edits. Spelled here rather than imported: the browser
40
40
  * half spells the same value and must not depend on a Host package.
41
41
  */
42
- export const REMOTE_WEB_UI_SETTINGS_NAMESPACE = settingsNamespace('remote-web-ui');
42
+ export const REMOTE_WEB_UI_SETTINGS_NAMESPACE = 'remote-web-ui';
43
43
  export const Config = z.object({
44
44
  tokenTtlMs: z.number().step(1).min(60_000).default(10 * 60_000),
45
45
  offlineAfterMs: z.number().step(1).min(5_000).default(25_000),
@@ -50,7 +50,8 @@ export const Config = z.object({
50
50
  publicBaseUrl: z.string(),
51
51
  devicesFile: z.string(),
52
52
  autoTunnel: z.boolean().default(false),
53
- mobileEnterToSend: z.boolean().default(true),
53
+ lanBind: z.boolean(),
54
+ profile: z.string().pattern(/^[A-Za-z0-9][A-Za-z0-9._-]*$/),
54
55
  enabled: z.boolean().default(true),
55
56
  });
56
57
  /** Presence sweep cadence (a stale device flips to disconnected within two sweeps). */
@@ -86,7 +87,8 @@ const DEFAULTS = {
86
87
  publicBaseUrl: undefined,
87
88
  devicesFile: defaultDevicesFile(),
88
89
  autoTunnel: false,
89
- mobileEnterToSend: true,
90
+ lanBind: undefined,
91
+ profile: process.env.DSH_PROFILE ?? 'web',
90
92
  enabled: true,
91
93
  };
92
94
  /**
@@ -106,12 +108,13 @@ function applyImpl(ctx, config) {
106
108
  publicBaseUrl: config?.publicBaseUrl,
107
109
  devicesFile: config?.devicesFile ?? DEFAULTS.devicesFile,
108
110
  autoTunnel: config?.autoTunnel ?? DEFAULTS.autoTunnel,
109
- mobileEnterToSend: config?.mobileEnterToSend ?? DEFAULTS.mobileEnterToSend,
111
+ lanBind: config?.lanBind,
112
+ profile: config?.profile ?? process.env.DSH_PROFILE ?? DEFAULTS.profile,
110
113
  enabled: config?.enabled ?? DEFAULTS.enabled,
111
114
  };
112
115
  // The live source the pairing service and the gate read: the settings
113
116
  // section once the web settings surface is served, the composition entry
114
- // otherwise (installSettingsSection swaps it when the namespace registers).
117
+ // otherwise (installSection swaps it when the namespace registers).
115
118
  let current = () => config ?? {};
116
119
  const resolve = () => {
117
120
  const value = current();
@@ -125,14 +128,15 @@ function applyImpl(ctx, config) {
125
128
  publicBaseUrl: value.publicBaseUrl,
126
129
  devicesFile: value.devicesFile ?? DEFAULTS.devicesFile,
127
130
  autoTunnel: value.autoTunnel ?? DEFAULTS.autoTunnel,
128
- mobileEnterToSend: value.mobileEnterToSend ?? DEFAULTS.mobileEnterToSend,
131
+ lanBind: value.lanBind,
132
+ profile: value.profile ?? process.env.DSH_PROFILE ?? DEFAULTS.profile,
129
133
  enabled: value.enabled ?? DEFAULTS.enabled,
130
134
  };
131
135
  };
132
136
  const service = new PairingService(pairingConfigOf(resolved));
133
137
  // ── auto tunnel ─────────────────────────────────────────────────────────
134
138
  // The minted public URL becomes the QR base (and the pairing fence's
135
- // trusted host). Phone /api traffic rides the plugin's own /m/api channel,
139
+ // trusted host). Phone /api traffic rides the plugin's own /remote channel,
136
140
  // which is NOT subject to the connection trust fence — so no fence
137
141
  // mutation is needed here (a distributable plugin must not change the
138
142
  // harness's connection plugin).
@@ -165,11 +169,18 @@ function applyImpl(ctx, config) {
165
169
  // sampling stance. The QR can only advertise addresses the fence accepts;
166
170
  // every interface gets its own base URL so a multi-homed machine can pick
167
171
  // the network the phone can actually reach.
168
- const lanBases = ctx.webServer.host === '0.0.0.0'
169
- ? lanIPv4Addresses().map(address => ({ address, base: `http://${address}:${String(ctx.webServer.port)}` }))
170
- : [];
171
- service.setLanBases(lanBases);
172
- const lanAddresses = lanBases.map(entry => entry.address);
172
+ // A recompose pass can hand the plugin a webServer whose facts have not
173
+ // settled yet (host unset, port undefined); deriving LAN bases from that
174
+ // would advertise :undefined links or clear a working set, so the bases
175
+ // only update when both facts are readable.
176
+ const bindKnown = (ctx.webServer.host === '0.0.0.0' || ctx.webServer.host === '127.0.0.1')
177
+ && Number.isFinite(ctx.webServer.port);
178
+ if (bindKnown) {
179
+ const lanBases = ctx.webServer.host === '0.0.0.0'
180
+ ? lanIPv4Addresses().map(address => ({ address, base: `http://${address}:${String(ctx.webServer.port)}` }))
181
+ : [];
182
+ service.setLanBases(lanBases);
183
+ }
173
184
  // Push a committed settings section into the service and gate. The service
174
185
  // config object is read per operation (token mint, touch, sweep), and the
175
186
  // gate re-reads its fence flag per request, so a live edit takes effect
@@ -179,12 +190,6 @@ function applyImpl(ctx, config) {
179
190
  // (now vetoing every non-loopback request) instead of opening the fence.
180
191
  let disposeRoutes;
181
192
  let disposeSweep;
182
- // The phone's data channel: pairing routes + the /m page + the /m/api
183
- // proxy (which needs the host ApiProxy service; the plugin injects it).
184
- const apiProxy = ctx.get('apiProxy');
185
- if (apiProxy === undefined) {
186
- console.warn('remote-web-ui: apiProxy service unavailable — the mobile data channel is disabled');
187
- }
188
193
  // ── remote update ────────────────────────────────────────────────────────
189
194
  // The dsh-web self-update surface: probe the npm registry for family
190
195
  // releases and run `pnpm update --latest` in the owning profile. Resolutions
@@ -262,31 +267,98 @@ function applyImpl(ctx, config) {
262
267
  });
263
268
  },
264
269
  });
270
+ // LAN-bind facts for the settings card, re-read per request so a hot
271
+ // rebind (the patch watcher recomposes the process) and a fresh toggle
272
+ // round are both reflected without a restart.
273
+ let lastKnownPort;
274
+ let lastFirewallApplied;
275
+ const lanBindStatus = () => {
276
+ const resolvedNow = resolve();
277
+ let state;
278
+ try {
279
+ state = lanBindState(resolvedNow.profile);
280
+ }
281
+ catch {
282
+ // An unsafe profile value must not take the whole status endpoint down.
283
+ state = { blockPresent: false };
284
+ }
285
+ const lanOn = state.host === '0.0.0.0';
286
+ const port = Number.isFinite(ctx.webServer.port) ? ctx.webServer.port : lastKnownPort;
287
+ if (Number.isFinite(ctx.webServer.port))
288
+ lastKnownPort = ctx.webServer.port;
289
+ const startup = ctx.get('webStartup');
290
+ const desiredHost = resolvedNow.lanBind === undefined
291
+ ? undefined
292
+ : desiredBindHost(resolvedNow.lanBind === true, startup?.host);
293
+ return {
294
+ profile: resolvedNow.profile,
295
+ setting: resolvedNow.lanBind ?? null,
296
+ blockHost: state.host ?? null,
297
+ bindHost: ctx.webServer.host,
298
+ port,
299
+ lanUrls: ctx.webServer.host === '0.0.0.0' && port !== undefined
300
+ ? lanIPv4Addresses().map(address => `http://${address}:${String(port)}`)
301
+ : [],
302
+ firewall: port !== undefined ? firewallSummary(port, lanOn) : { ok: true, managed: false },
303
+ platform: process.platform,
304
+ // The running bind does not follow the block on every deployment (the
305
+ // live patch watcher is profile-shape dependent): flag the divergence
306
+ // so the card can ask for a restart instead of looking broken. The
307
+ // comparison uses the effective desired host (a CLI --host wins over
308
+ // the toggle), not the raw toggle.
309
+ pendingRestart: pendingRestartOf(resolvedNow.lanBind, desiredHost, ctx.webServer.host),
310
+ };
311
+ };
312
+ // The process's inner browser credential: the proxied /api re-issues to
313
+ // 127.0.0.1, where the connection route enforces the harness browser-auth
314
+ // cookie (authority-bound; no loopback exemption on this cohort), so a
315
+ // device's own cookie can never satisfy the inner check. The plugin
316
+ // redeems its own launch token once and attaches the cookie to inner
317
+ // requests; the credential is only ever exercised behind the pairing gate
318
+ // in remote-api.ts.
319
+ const innerAuth = createInnerAuth(() => {
320
+ if (!Number.isFinite(ctx.webServer.port))
321
+ return undefined;
322
+ try {
323
+ return ctx.connection.authenticatedUrl?.(`http://127.0.0.1:${String(ctx.webServer.port)}/`);
324
+ }
325
+ catch {
326
+ return undefined;
327
+ }
328
+ });
329
+ // The official index document for the /pair-app landing, fetched from the
330
+ // inner loopback with the process credential and cached briefly (the shell
331
+ // is static; skins/injections settle right after boot).
332
+ const APP_SHELL_TTL_MS = 30_000;
333
+ let appShellCache;
334
+ const fetchAppShell = async () => {
335
+ if (!Number.isFinite(ctx.webServer.port))
336
+ return undefined;
337
+ if (appShellCache !== undefined && Date.now() - appShellCache.at < APP_SHELL_TTL_MS)
338
+ return appShellCache.html;
339
+ const cookie = await innerAuth.ready();
340
+ try {
341
+ const response = await fetch(`http://127.0.0.1:${String(ctx.webServer.port)}/`, {
342
+ headers: cookie !== undefined ? { cookie } : undefined,
343
+ });
344
+ if (!response.ok)
345
+ return undefined;
346
+ const html = await response.text();
347
+ appShellCache = { at: Date.now(), html };
348
+ return html;
349
+ }
350
+ catch {
351
+ return undefined;
352
+ }
353
+ };
265
354
  const routes = [
266
- ...makeRoutes({ service, lanAddresses, requirePairingForLan: () => resolve().requirePairingForLan }),
267
- ...makeMobileRoutes(),
268
- ...(apiProxy !== undefined
269
- ? makeMobileApiRoutes({
270
- service,
271
- apiProxy,
272
- pendingTracker: new PendingTracker(),
273
- mobileEnterToSend: () => resolve().mobileEnterToSend,
274
- commandDispatcher: ctx.commands !== undefined && ctx.agents !== undefined
275
- ? {
276
- async execute(sessionId, line, signal) {
277
- const agent = ctx.agents.get(sessionId);
278
- if (agent === undefined)
279
- return { error: 'session-not-found' };
280
- const execution = await ctx.commands.execute(agent, line, [], signal);
281
- if (execution === undefined)
282
- return { error: 'unknown-command' };
283
- return { ok: true, result: execution.result };
284
- },
285
- }
286
- : undefined,
287
- })
288
- : []),
289
- ...(apiProxy !== undefined ? makePairedModelCatalogRoutes({ service, apiProxy, lanAddresses }) : []),
355
+ ...makeRoutes({
356
+ service,
357
+ lanAddresses: service.lanAddresses,
358
+ requirePairingForLan: () => resolve().requirePairingForLan,
359
+ lanBindStatus,
360
+ indexDocument: fetchAppShell,
361
+ }),
290
362
  // The remote desktop channel: policy-gated `/remote` prefix that
291
363
  // re-issues fenced paths to loopback (see remote-api.ts). The live
292
364
  // requirePairingForLan is re-read per request, same as the gate listener
@@ -296,6 +368,7 @@ function applyImpl(ctx, config) {
296
368
  service,
297
369
  port: ctx.webServer.port,
298
370
  requirePairingForLan: () => resolve().requirePairingForLan,
371
+ auth: innerAuth,
299
372
  }),
300
373
  ...updateRoutes,
301
374
  ];
@@ -303,6 +376,7 @@ function applyImpl(ctx, config) {
303
376
  service,
304
377
  port: ctx.webServer.port,
305
378
  requirePairingForLan: () => resolve().requirePairingForLan,
379
+ auth: innerAuth,
306
380
  });
307
381
  const gate = makeGateListener(service, () => resolve().requirePairingForLan, () => resolve().enabled);
308
382
  ctx.effect(() => ctx.on('api/gate', gate), 'remote-web-ui: api gate');
@@ -350,20 +424,92 @@ function applyImpl(ctx, config) {
350
424
  const initialPostureTimer = nodeSetTimeout(() => { runPostureProbe(); }, 5_000);
351
425
  initialPostureTimer.unref();
352
426
  ctx.effect(() => () => { clearTimeout(initialPostureTimer); }, 'remote-web-ui: posture probe boot');
353
- // Sibling plugins (aionui-panel, …) look this up by name. Absent when this
427
+ // Sibling plugins (dsh-better-sidebar, …) look this up by name. Absent when this
354
428
  // plugin is not installed; stop() / enabled=false still refuse cookies.
355
429
  new RemoteWebUiPairing(ctx, (request) => {
356
430
  if (!resolve().enabled)
357
431
  return false;
358
432
  return isPairedDeviceRequest(service, request);
359
433
  });
360
- if (lanAddresses.length > 0) {
361
- const urls = lanAddresses.map(ip => `http://${ip}:${String(ctx.webServer.port)}/m/`).join(' , ');
362
- console.log(`remote-web-ui: mobile UI reachable on LAN at ${urls}`);
434
+ // Remote-presence to pet-visibility link: while a paired device is online
435
+ // (an active phone mirror), hide the host-global pet through the pet's OWN
436
+ // hide switch; when the last device has been offline for a grace window,
437
+ // show it again (user design; the pet plugin is optional, so the seam is
438
+ // resolved per transition and every failure degrades to a no-op).
439
+ const presencePet = startRemotePresencePet({
440
+ onState: listener => service.onState(listener),
441
+ pet: () => {
442
+ try {
443
+ return ctx.get('pet');
444
+ }
445
+ catch {
446
+ return undefined;
447
+ }
448
+ },
449
+ });
450
+ ctx.effect(() => presencePet, 'remote-web-ui: remote-presence pet visibility');
451
+ if (service.lanAddresses.length > 0) {
452
+ const urls = service.lanAddresses.map(ip => `http://${ip}:${String(ctx.webServer.port)}`).join(' , ');
453
+ console.log(`remote-web-ui: the paired Web GUI is reachable on LAN at ${urls}`);
454
+ }
455
+ // Cohort honesty (see the README security model): the pairing gate covers
456
+ // the plugin's own /remote channel; direct /api from LAN origins is
457
+ // governed by the harness fence + browser-auth cookie, and this cohort's
458
+ // api/gate seam has no emitter — so stop()/revoke() cannot invalidate a
459
+ // browser credential a device has already redeemed.
460
+ if (ctx.webServer.host === '0.0.0.0') {
461
+ console.warn('remote-web-ui: LAN-exposed bind — pairing gates the /remote channel; direct /api stays under the harness fence + browser auth (stop() does not revoke an already-redeemed browser credential)');
363
462
  }
364
463
  const sync = () => {
365
464
  const value = resolve();
366
465
  service.config = pairingConfigOf(value);
466
+ // LAN bind toggle: only write the managed patch block once the user has
467
+ // flipped it (undefined = untouched, never write). Desired host/port come
468
+ // from the CLI flags when given (flags win), else from the toggle and
469
+ // the currently bound port. The block takes effect on the next start, so
470
+ // the re-assert at every boot keeps it in sync with both the toggle and
471
+ // the flags.
472
+ if (value.lanBind !== undefined) {
473
+ const startup = ctx.get('webStartup');
474
+ const desiredHost = desiredBindHost(value.lanBind === true, startup?.host);
475
+ const desiredPort = desiredBindPort(startup?.port, Number.isFinite(ctx.webServer.port) ? ctx.webServer.port : undefined);
476
+ if (desiredPort === undefined) {
477
+ console.error('remote-web-ui: cannot assert the lan-bind block — the web server port is not known yet');
478
+ }
479
+ else {
480
+ try {
481
+ const current = lanBindState(value.profile);
482
+ if (current.host !== desiredHost || current.port !== desiredPort) {
483
+ writeLanBind(desiredHost, desiredPort, value.profile);
484
+ console.log(`remote-web-ui: lan-bind block written for profile ${value.profile} (${desiredHost}:${String(desiredPort)}); it takes effect when the profile next applies (the card reports pendingRestart until the running bind follows)`);
485
+ }
486
+ }
487
+ catch (error) {
488
+ console.error(`remote-web-ui: failed to write the lan-bind block: ${error instanceof Error ? error.message : String(error)}`);
489
+ }
490
+ }
491
+ // Keep the firewall rule aligned with the toggle (managed platforms
492
+ // only; see firewall.ts). The probes and rule rewrites are blocking
493
+ // subprocesses, so they run only when the desired state actually moved
494
+ // — not on every unrelated settings save. The port is the live bind
495
+ // port; without it there is nothing to align yet.
496
+ const livePort = Number.isFinite(ctx.webServer.port) ? ctx.webServer.port : undefined;
497
+ if (livePort === undefined) {
498
+ console.error('remote-web-ui: cannot align the host firewall rule — the web server port is not known yet');
499
+ }
500
+ else {
501
+ const nextFirewall = { enabled: value.lanBind === true, port: livePort };
502
+ if (firewallActionNeeded(lastFirewallApplied, nextFirewall)) {
503
+ const ruleOk = nextFirewall.enabled ? ensureFirewallRule(nextFirewall.port) : removeFirewallRule(nextFirewall.port);
504
+ if (ruleOk) {
505
+ lastFirewallApplied = nextFirewall;
506
+ }
507
+ else {
508
+ console.error('remote-web-ui: the host firewall rule could not be updated (admin rights required on managed platforms)');
509
+ }
510
+ }
511
+ }
512
+ }
367
513
  // The auto tunnel owns the public base while enabled: the minted URL
368
514
  // lands in the service through the tunnel's phase listener. The manual
369
515
  // publicBaseUrl applies only when the auto tunnel is off.
@@ -437,12 +583,14 @@ function applyImpl(ctx, config) {
437
583
  return;
438
584
  table.push({ kind: 'script', placement: 'head', text: REMOTE_CHANNEL_BOOT_SCRIPT });
439
585
  }), 'remote-web-ui: remote channel boot patch');
440
- installSettingsSection(ctx, REMOTE_WEB_UI_SETTINGS_NAMESPACE, Config, config ?? {}, {
441
- setSource: (source) => {
442
- current = source;
443
- sync();
444
- },
445
- onChange: sync,
586
+ ctx.inject(['settings'], (settingsCtx) => {
587
+ settingsCtx.settings.installSection(ctx, REMOTE_WEB_UI_SETTINGS_NAMESPACE, Config, config ?? {}, {
588
+ setSource: (source) => {
589
+ current = source;
590
+ sync();
591
+ },
592
+ onChange: sync,
593
+ });
446
594
  });
447
595
  sync();
448
596
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The inner browser credential for the remote desktop channel. The proxied
3
+ * /api re-issues requests to 127.0.0.1, where the connection plugin's route
4
+ * enforces the harness browser-auth cookie — an authority-bound signed cookie
5
+ * with no loopback exemption on this cohort line, so a cookie minted for the
6
+ * device's own authority can never satisfy the inner check. The process
7
+ * therefore redeems its own launch token (the same one-time exchange every
8
+ * paired device's first navigation performs) and attaches the resulting
9
+ * cookie to inner requests. The credential is only ever exercised behind the
10
+ * pairing gate: while requirePairingForLan is on, every proxied call already
11
+ * carried a live paired-device cookie before this module is consulted.
12
+ */
13
+ /** Browser-auth credential the proxy attaches to loopback-bound requests. */
14
+ export interface InnerAuth {
15
+ /**
16
+ * Resolve the `name=value` cookie pair for inner requests, or undefined
17
+ * when the launch token is unavailable (the proxy then sends without a
18
+ * credential and the inner route answers 401, exactly as before).
19
+ */
20
+ ready(): Promise<string | undefined>;
21
+ /** Drop the cached credential; the next ready() re-redeems. */
22
+ invalidate(): void;
23
+ }
24
+ /**
25
+ * Build the inner-auth handle.
26
+ * @param launchUrl - the launch-token URL for the inner loopback authority
27
+ * (the connection service's authenticatedUrl of `http://127.0.0.1:<port>/`),
28
+ * or undefined when the connection service or port is unavailable.
29
+ * @param fetchImpl - injectable fetch (tests).
30
+ */
31
+ export declare function createInnerAuth(launchUrl: () => string | undefined, fetchImpl?: typeof fetch): InnerAuth;
32
+ //# sourceMappingURL=inner-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inner-auth.d.ts","sourceRoot":"","sources":["../../src/inner-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,6EAA6E;AAC7E,MAAM,WAAW,SAAS;IACxB;;;;OAIG;IACH,KAAK,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IACpC,+DAA+D;IAC/D,UAAU,IAAI,IAAI,CAAA;CACnB;AAKD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,MAAM,GAAG,SAAS,EACnC,SAAS,GAAE,OAAO,KAAa,GAC9B,SAAS,CA8CX"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The inner browser credential for the remote desktop channel. The proxied
3
+ * /api re-issues requests to 127.0.0.1, where the connection plugin's route
4
+ * enforces the harness browser-auth cookie — an authority-bound signed cookie
5
+ * with no loopback exemption on this cohort line, so a cookie minted for the
6
+ * device's own authority can never satisfy the inner check. The process
7
+ * therefore redeems its own launch token (the same one-time exchange every
8
+ * paired device's first navigation performs) and attaches the resulting
9
+ * cookie to inner requests. The credential is only ever exercised behind the
10
+ * pairing gate: while requirePairingForLan is on, every proxied call already
11
+ * carried a live paired-device cookie before this module is consulted.
12
+ */
13
+ /** The harness browser-auth cookie name prefix (see dsh-client-connection). */
14
+ const BROWSER_AUTH_COOKIE_PREFIX = 'dsh-auth-';
15
+ /**
16
+ * Build the inner-auth handle.
17
+ * @param launchUrl - the launch-token URL for the inner loopback authority
18
+ * (the connection service's authenticatedUrl of `http://127.0.0.1:<port>/`),
19
+ * or undefined when the connection service or port is unavailable.
20
+ * @param fetchImpl - injectable fetch (tests).
21
+ */
22
+ export function createInnerAuth(launchUrl, fetchImpl = fetch) {
23
+ let cached;
24
+ let inflight;
25
+ const redeem = async () => {
26
+ const url = launchUrl();
27
+ if (url === undefined)
28
+ return undefined;
29
+ try {
30
+ // redirect: 'manual' keeps the 303's set-cookie readable instead of
31
+ // following it; the exchange itself is a plain GET with the token in
32
+ // the query, exactly what a first browser visit performs.
33
+ const response = await fetchImpl(url, { redirect: 'manual' });
34
+ const raw = response.headers.get('set-cookie');
35
+ if (raw === null)
36
+ return undefined;
37
+ // Generated cookie values are base64url and never contain commas, so a
38
+ // comma split followed by a prefix match finds the browser-auth entry
39
+ // even when the exchange sets several cookies.
40
+ const entry = raw
41
+ .split(',')
42
+ .map((part) => part.trim())
43
+ .find((part) => part.startsWith(BROWSER_AUTH_COOKIE_PREFIX));
44
+ if (entry === undefined)
45
+ return undefined;
46
+ const pair = entry.split(';')[0]?.trim();
47
+ return pair !== undefined && pair.includes('=') ? pair : undefined;
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ };
53
+ return {
54
+ ready() {
55
+ if (cached !== undefined)
56
+ return Promise.resolve(cached);
57
+ inflight ??= redeem().then((value) => {
58
+ if (value !== undefined)
59
+ cached = value;
60
+ inflight = undefined;
61
+ return value;
62
+ }, () => {
63
+ inflight = undefined;
64
+ return undefined;
65
+ });
66
+ return inflight;
67
+ },
68
+ invalidate() {
69
+ cached = undefined;
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Pure decision helpers shared by the lan-bind sync path (index.ts) and the
3
+ * status path (lanBindStatus). Extracted so the flags-win precedence, the
4
+ * pending-restart divergence check, and the firewall re-apply gate have one
5
+ * implementation and direct unit coverage.
6
+ */
7
+ /** The two bind hosts the managed block pins. */
8
+ export type LanBindPlanHost = '0.0.0.0' | '127.0.0.1';
9
+ /** webStartup facts the CLI hands the process (absent unless a flag was passed). */
10
+ export interface StartupFacts {
11
+ host?: string;
12
+ port?: number;
13
+ }
14
+ /**
15
+ * The bind host the managed block should pin: an explicit CLI --host wins,
16
+ * otherwise the toggle decides. CLI hosts outside the two managed literals
17
+ * (a pinned specific IP) are not a state the toggle manages — the toggle
18
+ * decides, and the card surfaces the divergence honestly.
19
+ * @param lanBind - the toggle value.
20
+ * @param startupHost - the CLI-provided bind host, when given.
21
+ * @returns the desired bind host literal.
22
+ */
23
+ export declare function desiredBindHost(lanBind: boolean, startupHost: string | undefined): LanBindPlanHost;
24
+ /**
25
+ * The port the managed block should pin: the CLI port when given, else the
26
+ * currently bound port. Undefined when neither is readable yet (the caller
27
+ * skips the write and reports why).
28
+ * @param startupPort - the CLI-provided port, when given.
29
+ * @param livePort - the currently bound port, when finite.
30
+ */
31
+ export declare function desiredBindPort(startupPort: number | undefined, livePort: number | undefined): number | undefined;
32
+ /**
33
+ * Whether the running bind has not caught up with what the toggle (plus a
34
+ * possibly overriding CLI host) will pin. Compared against the effective
35
+ * desired host — not the raw toggle — so a flag-managed bind is not reported
36
+ * as pending forever.
37
+ * @param lanBind - the toggle value (undefined = untouched, never pending).
38
+ * @param desiredHost - the effective desired host, or undefined when untouched.
39
+ * @param liveHost - the currently bound host.
40
+ */
41
+ export declare function pendingRestartOf(lanBind: boolean | undefined, desiredHost: LanBindPlanHost | undefined, liveHost: string | undefined): boolean;
42
+ /** The last firewall state this process applied. */
43
+ export interface AppliedFirewallState {
44
+ enabled: boolean;
45
+ port: number;
46
+ }
47
+ /**
48
+ * Whether the firewall rule must be (re)applied: only when the toggle or the
49
+ * bound port moved since the last application. Keeps unrelated settings
50
+ * saves from spawning netsh/firewall-cmd delete+add churn.
51
+ * @param previous - the state applied earlier by this process, if any.
52
+ * @param next - the state the settings now ask for, if derivable.
53
+ */
54
+ export declare function firewallActionNeeded(previous: AppliedFirewallState | undefined, next: AppliedFirewallState | undefined): boolean;
55
+ //# sourceMappingURL=lan-bind-plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lan-bind-plan.d.ts","sourceRoot":"","sources":["../../src/lan-bind-plan.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,iDAAiD;AACjD,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,WAAW,CAAA;AAErD,oFAAoF;AACpF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,eAAe,CAGlG;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAGjH;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,OAAO,GAAG,SAAS,EAC5B,WAAW,EAAE,eAAe,GAAG,SAAS,EACxC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,OAAO,CAGT;AAED,oDAAoD;AACpD,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,oBAAoB,GAAG,SAAS,EAC1C,IAAI,EAAE,oBAAoB,GAAG,SAAS,GACrC,OAAO,CAIT"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Pure decision helpers shared by the lan-bind sync path (index.ts) and the
3
+ * status path (lanBindStatus). Extracted so the flags-win precedence, the
4
+ * pending-restart divergence check, and the firewall re-apply gate have one
5
+ * implementation and direct unit coverage.
6
+ */
7
+ /**
8
+ * The bind host the managed block should pin: an explicit CLI --host wins,
9
+ * otherwise the toggle decides. CLI hosts outside the two managed literals
10
+ * (a pinned specific IP) are not a state the toggle manages — the toggle
11
+ * decides, and the card surfaces the divergence honestly.
12
+ * @param lanBind - the toggle value.
13
+ * @param startupHost - the CLI-provided bind host, when given.
14
+ * @returns the desired bind host literal.
15
+ */
16
+ export function desiredBindHost(lanBind, startupHost) {
17
+ if (startupHost === '0.0.0.0' || startupHost === '127.0.0.1')
18
+ return startupHost;
19
+ return lanBind ? '0.0.0.0' : '127.0.0.1';
20
+ }
21
+ /**
22
+ * The port the managed block should pin: the CLI port when given, else the
23
+ * currently bound port. Undefined when neither is readable yet (the caller
24
+ * skips the write and reports why).
25
+ * @param startupPort - the CLI-provided port, when given.
26
+ * @param livePort - the currently bound port, when finite.
27
+ */
28
+ export function desiredBindPort(startupPort, livePort) {
29
+ if (typeof startupPort === 'number' && Number.isFinite(startupPort))
30
+ return startupPort;
31
+ return typeof livePort === 'number' && Number.isFinite(livePort) ? livePort : undefined;
32
+ }
33
+ /**
34
+ * Whether the running bind has not caught up with what the toggle (plus a
35
+ * possibly overriding CLI host) will pin. Compared against the effective
36
+ * desired host — not the raw toggle — so a flag-managed bind is not reported
37
+ * as pending forever.
38
+ * @param lanBind - the toggle value (undefined = untouched, never pending).
39
+ * @param desiredHost - the effective desired host, or undefined when untouched.
40
+ * @param liveHost - the currently bound host.
41
+ */
42
+ export function pendingRestartOf(lanBind, desiredHost, liveHost) {
43
+ if (lanBind === undefined || desiredHost === undefined)
44
+ return false;
45
+ return liveHost !== desiredHost;
46
+ }
47
+ /**
48
+ * Whether the firewall rule must be (re)applied: only when the toggle or the
49
+ * bound port moved since the last application. Keeps unrelated settings
50
+ * saves from spawning netsh/firewall-cmd delete+add churn.
51
+ * @param previous - the state applied earlier by this process, if any.
52
+ * @param next - the state the settings now ask for, if derivable.
53
+ */
54
+ export function firewallActionNeeded(previous, next) {
55
+ if (next === undefined)
56
+ return false;
57
+ if (previous === undefined)
58
+ return true;
59
+ return previous.enabled !== next.enabled || previous.port !== next.port;
60
+ }