@linxin666/dsh-remote-web-ui 0.3.5 → 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.
- package/README.i18n.yaml +2 -2
- package/README.md +77 -468
- package/README.zh.md +83 -113
- package/lib/client.js +955 -203
- package/lib/client.js.map +1 -1
- package/lib/index.js +1136 -1466
- package/lib/types/client/FooterRemoteEntry.d.ts +6 -7
- package/lib/types/client/FooterRemoteEntry.d.ts.map +1 -1
- package/lib/types/client/FooterRemoteEntry.js +1 -1
- package/lib/types/client/RemoteEntry.d.ts +7 -4
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +7 -10
- package/lib/types/client/RemotePanel.d.ts +2 -2
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts +9 -5
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +56 -3
- package/lib/types/client/deep-link.d.ts +10 -13
- package/lib/types/client/deep-link.d.ts.map +1 -1
- package/lib/types/client/deep-link.js +14 -74
- package/lib/types/client/index.d.ts +2 -6
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +73 -39
- package/lib/types/client/locales.d.ts +26 -12
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +36 -22
- package/lib/types/client/mobile-adapt.d.ts +40 -0
- package/lib/types/client/mobile-adapt.d.ts.map +1 -0
- package/lib/types/client/mobile-adapt.js +967 -0
- package/lib/types/client/pair-api.d.ts +24 -6
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +9 -14
- package/lib/types/client/remote-channel.d.ts +3 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +35 -3
- package/lib/types/client/settings-form.d.ts +35 -43
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +60 -53
- package/lib/types/firewall.d.ts +75 -0
- package/lib/types/firewall.d.ts.map +1 -0
- package/lib/types/firewall.js +184 -0
- package/lib/types/gate.d.ts +11 -5
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +11 -5
- package/lib/types/index.d.ts +39 -10
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +206 -58
- package/lib/types/inner-auth.d.ts +32 -0
- package/lib/types/inner-auth.d.ts.map +1 -0
- package/lib/types/inner-auth.js +72 -0
- package/lib/types/lan-bind-plan.d.ts +55 -0
- package/lib/types/lan-bind-plan.d.ts.map +1 -0
- package/lib/types/lan-bind-plan.js +60 -0
- package/lib/types/lan-bind.d.ts +69 -0
- package/lib/types/lan-bind.d.ts.map +1 -0
- package/lib/types/lan-bind.js +128 -0
- package/lib/types/loopback-proxy.d.ts +9 -2
- package/lib/types/loopback-proxy.d.ts.map +1 -1
- package/lib/types/loopback-proxy.js +24 -2
- package/lib/types/pairing.d.ts +15 -3
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +20 -6
- package/lib/types/remote-api.d.ts +30 -13
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +55 -42
- package/lib/types/remote-channel-boot.d.ts +21 -2
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +65 -3
- package/lib/types/remote-channel-rules.d.ts +8 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +9 -2
- package/lib/types/remote-methods.d.ts +45 -11
- package/lib/types/remote-methods.d.ts.map +1 -1
- package/lib/types/remote-methods.js +56 -27
- package/lib/types/remote-presence-pet.d.ts +51 -0
- package/lib/types/remote-presence-pet.d.ts.map +1 -0
- package/lib/types/remote-presence-pet.js +101 -0
- package/lib/types/routes.d.ts +76 -4
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +336 -19
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +7 -1
- package/package.json +20 -18
- package/src/client/FooterRemoteEntry.tsx +9 -10
- package/src/client/RemoteEntry.tsx +13 -14
- package/src/client/RemotePanel.tsx +5 -16
- package/src/client/RemoteSettingsCard.tsx +72 -13
- package/src/client/deep-link.ts +14 -75
- package/src/client/index.ts +77 -39
- package/src/client/locales.ts +36 -22
- package/src/client/mobile-adapt.ts +913 -0
- package/src/client/pair-api.ts +26 -15
- package/src/client/remote-channel.ts +31 -3
- package/src/client/settings-form.ts +78 -92
- package/src/dsh-home.ts +1 -1
- package/src/firewall.ts +230 -0
- package/src/gate.ts +11 -5
- package/src/index.ts +235 -65
- package/src/inner-auth.ts +85 -0
- package/src/lan-bind-plan.ts +81 -0
- package/src/lan-bind.ts +145 -0
- package/src/loopback-proxy.ts +34 -0
- package/src/pairing.persist.test.ts +1 -1
- package/src/pairing.ts +20 -6
- package/src/remote-api.ts +67 -45
- package/src/remote-channel-boot.ts +70 -3
- package/src/remote-channel-rules.ts +17 -2
- package/src/remote-methods.ts +58 -27
- package/src/remote-presence-pet.ts +129 -0
- package/src/routes.ts +358 -19
- package/src/tunnel.ts +7 -1
- package/assets/apple-touch-icon.png +0 -0
- package/assets/mobile-icon-192.png +0 -0
- package/assets/mobile-icon-512.png +0 -0
- package/assets/mobile-service-worker.js +0 -63
- package/lib/mobile.js +0 -17621
- package/lib/mobile.js.map +0 -1
- package/lib/types/mobile/PairRequiredView.d.ts +0 -7
- package/lib/types/mobile/PairRequiredView.d.ts.map +0 -1
- package/lib/types/mobile/PairRequiredView.js +0 -28
- package/lib/types/mobile/PairRequiredView.test.d.ts +0 -2
- package/lib/types/mobile/PairRequiredView.test.d.ts.map +0 -1
- package/lib/types/mobile/PairRequiredView.test.js +0 -24
- package/lib/types/mobile/api.d.ts +0 -145
- package/lib/types/mobile/api.d.ts.map +0 -1
- package/lib/types/mobile/api.js +0 -103
- package/lib/types/mobile/index.d.ts +0 -10
- package/lib/types/mobile/index.d.ts.map +0 -1
- package/lib/types/mobile/index.js +0 -38
- package/lib/types/mobile/markdown.d.ts +0 -34
- package/lib/types/mobile/markdown.d.ts.map +0 -1
- package/lib/types/mobile/markdown.js +0 -285
- package/lib/types/mobile/markdown.test.d.ts +0 -2
- package/lib/types/mobile/markdown.test.d.ts.map +0 -1
- package/lib/types/mobile/markdown.test.js +0 -79
- package/lib/types/mobile/messages.d.ts +0 -141
- package/lib/types/mobile/messages.d.ts.map +0 -1
- package/lib/types/mobile/messages.js +0 -553
- package/lib/types/mobile/messages.test.d.ts +0 -2
- package/lib/types/mobile/messages.test.d.ts.map +0 -1
- package/lib/types/mobile/messages.test.js +0 -356
- package/lib/types/mobile/mobile-styles.d.ts +0 -6
- package/lib/types/mobile/mobile-styles.d.ts.map +0 -1
- package/lib/types/mobile/mobile-styles.js +0 -1438
- package/lib/types/mobile/mobile-theme.d.ts +0 -22
- package/lib/types/mobile/mobile-theme.d.ts.map +0 -1
- package/lib/types/mobile/mobile-theme.js +0 -70
- package/lib/types/mobile/mobile-theme.test.d.ts +0 -2
- package/lib/types/mobile/mobile-theme.test.d.ts.map +0 -1
- package/lib/types/mobile/mobile-theme.test.js +0 -71
- package/lib/types/mobile/mux.d.ts +0 -128
- package/lib/types/mobile/mux.d.ts.map +0 -1
- package/lib/types/mobile/mux.js +0 -305
- package/lib/types/mobile/mux.test.d.ts +0 -2
- package/lib/types/mobile/mux.test.d.ts.map +0 -1
- package/lib/types/mobile/mux.test.js +0 -267
- package/lib/types/mobile/pairing.d.ts +0 -31
- package/lib/types/mobile/pairing.d.ts.map +0 -1
- package/lib/types/mobile/pairing.js +0 -62
- package/lib/types/mobile/rpc.d.ts +0 -38
- package/lib/types/mobile/rpc.d.ts.map +0 -1
- package/lib/types/mobile/rpc.js +0 -83
- package/lib/types/mobile/theme-toggle.d.ts +0 -12
- package/lib/types/mobile/theme-toggle.d.ts.map +0 -1
- package/lib/types/mobile/theme-toggle.js +0 -26
- package/lib/types/mobile/views/App.d.ts +0 -57
- package/lib/types/mobile/views/App.d.ts.map +0 -1
- package/lib/types/mobile/views/App.js +0 -150
- package/lib/types/mobile/views/App.test.d.ts +0 -2
- package/lib/types/mobile/views/App.test.d.ts.map +0 -1
- package/lib/types/mobile/views/App.test.js +0 -52
- package/lib/types/mobile/views/ChatView.d.ts +0 -56
- package/lib/types/mobile/views/ChatView.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.js +0 -773
- package/lib/types/mobile/views/ChatView.stream.test.d.ts +0 -2
- package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.stream.test.js +0 -231
- package/lib/types/mobile/views/ChatView.test.d.ts +0 -2
- package/lib/types/mobile/views/ChatView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.test.js +0 -718
- package/lib/types/mobile/views/SessionListView.d.ts +0 -27
- package/lib/types/mobile/views/SessionListView.d.ts.map +0 -1
- package/lib/types/mobile/views/SessionListView.js +0 -139
- package/lib/types/mobile/views/SessionListView.test.d.ts +0 -2
- package/lib/types/mobile/views/SessionListView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/SessionListView.test.js +0 -167
- package/lib/types/mobile/views/WorkspaceView.d.ts +0 -21
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +0 -1
- package/lib/types/mobile/views/WorkspaceView.js +0 -93
- package/lib/types/mobile/views/WorkspaceView.test.d.ts +0 -2
- package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/WorkspaceView.test.js +0 -183
- package/lib/types/mobile-api.d.ts +0 -65
- package/lib/types/mobile-api.d.ts.map +0 -1
- package/lib/types/mobile-api.js +0 -404
- package/lib/types/mobile-pending.d.ts +0 -38
- package/lib/types/mobile-pending.d.ts.map +0 -1
- package/lib/types/mobile-pending.js +0 -59
- package/lib/types/mobile-pwa.d.ts +0 -6
- package/lib/types/mobile-pwa.d.ts.map +0 -1
- package/lib/types/mobile-pwa.js +0 -17
- package/lib/types/mobile-routes.d.ts +0 -13
- package/lib/types/mobile-routes.d.ts.map +0 -1
- package/lib/types/mobile-routes.js +0 -217
- package/lib/types/paired-model-catalog.d.ts +0 -17
- package/lib/types/paired-model-catalog.d.ts.map +0 -1
- package/lib/types/paired-model-catalog.js +0 -311
- package/src/mobile/PairRequiredView.test.tsx +0 -28
- package/src/mobile/PairRequiredView.tsx +0 -58
- package/src/mobile/api.ts +0 -229
- package/src/mobile/index.tsx +0 -41
- package/src/mobile/markdown.test.ts +0 -91
- package/src/mobile/markdown.ts +0 -291
- package/src/mobile/messages.test.ts +0 -402
- package/src/mobile/messages.ts +0 -677
- package/src/mobile/mobile-styles.ts +0 -1439
- package/src/mobile/mobile-theme.test.ts +0 -84
- package/src/mobile/mobile-theme.ts +0 -76
- package/src/mobile/mux.test.ts +0 -314
- package/src/mobile/mux.ts +0 -336
- package/src/mobile/pairing.ts +0 -77
- package/src/mobile/rpc.ts +0 -90
- package/src/mobile/theme-toggle.tsx +0 -47
- package/src/mobile/views/App.test.tsx +0 -62
- package/src/mobile/views/App.tsx +0 -213
- package/src/mobile/views/ChatView.stream.test.tsx +0 -263
- package/src/mobile/views/ChatView.test.tsx +0 -816
- package/src/mobile/views/ChatView.tsx +0 -1295
- package/src/mobile/views/SessionListView.test.tsx +0 -207
- package/src/mobile/views/SessionListView.tsx +0 -244
- package/src/mobile/views/WorkspaceView.test.tsx +0 -227
- package/src/mobile/views/WorkspaceView.tsx +0 -245
- package/src/mobile-api.ts +0 -418
- package/src/mobile-pending.ts +0 -88
- package/src/mobile-pwa.ts +0 -19
- package/src/mobile-routes.ts +0 -231
- package/src/paired-model-catalog.ts +0 -272
package/lib/types/index.js
CHANGED
|
@@ -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', '
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 /
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
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({
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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 (
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
+
}
|