@linxin666/dsh-remote-web-ui 0.4.2 → 0.4.3
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 +8 -6
- package/README.zh.md +8 -6
- package/lib/client.js +154 -594
- package/lib/client.js.map +1 -1
- package/lib/index.js +323 -908
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +8 -9
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +1 -1
- package/lib/types/client/index.d.ts +32 -3
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +51 -8
- package/lib/types/client/locales.d.ts +0 -84
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +0 -84
- package/lib/types/client/settings-entry-form.d.ts +50 -0
- package/lib/types/client/settings-entry-form.d.ts.map +1 -0
- package/lib/types/client/settings-entry-form.js +98 -0
- package/lib/types/index.d.ts +99 -7
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +104 -137
- package/lib/types/lan-bind-plan.d.ts +14 -0
- package/lib/types/lan-bind-plan.d.ts.map +1 -1
- package/lib/types/lan-bind-plan.js +16 -0
- package/lib/types/lan-bind.d.ts +1 -1
- package/lib/types/lan-bind.js +1 -1
- package/lib/types/mount-once.d.ts +38 -7
- package/lib/types/mount-once.d.ts.map +1 -1
- package/lib/types/mount-once.js +96 -12
- package/lib/types/tunnel.d.ts +60 -3
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +136 -3
- package/package.json +2 -2
- package/src/client/RemoteEntry.tsx +7 -9
- package/src/client/RemoteSettingsCard.tsx +0 -2
- package/src/client/index.ts +53 -9
- package/src/client/locales.ts +0 -84
- package/src/client/remote.module.css +50 -200
- package/src/client/settings-entry-form.ts +142 -0
- package/src/index.ts +156 -152
- package/src/lan-bind-plan.ts +22 -0
- package/src/lan-bind.ts +1 -1
- package/src/mount-once.ts +107 -13
- package/src/tunnel.ts +142 -3
- package/lib/types/client/UpdateEntry.d.ts +0 -14
- package/lib/types/client/UpdateEntry.d.ts.map +0 -1
- package/lib/types/client/UpdateEntry.js +0 -112
- package/lib/types/client/UpdatePanel.d.ts +0 -42
- package/lib/types/client/UpdatePanel.d.ts.map +0 -1
- package/lib/types/client/UpdatePanel.js +0 -101
- package/lib/types/client/update-api.d.ts +0 -29
- package/lib/types/client/update-api.d.ts.map +0 -1
- package/lib/types/client/update-api.js +0 -47
- package/lib/types/console-output.d.ts +0 -41
- package/lib/types/console-output.d.ts.map +0 -1
- package/lib/types/console-output.js +0 -89
- package/lib/types/update-routes.d.ts +0 -30
- package/lib/types/update-routes.d.ts.map +0 -1
- package/lib/types/update-routes.js +0 -47
- package/lib/types/update.d.ts +0 -282
- package/lib/types/update.d.ts.map +0 -1
- package/lib/types/update.js +0 -773
- package/src/client/UpdateEntry.tsx +0 -144
- package/src/client/UpdatePanel.tsx +0 -256
- package/src/client/update-api.ts +0 -49
- package/src/console-output.ts +0 -97
- package/src/update-routes.ts +0 -65
- package/src/update.ts +0 -915
package/lib/types/index.d.ts
CHANGED
|
@@ -10,10 +10,19 @@
|
|
|
10
10
|
import type { IncomingMessage } from 'node:http';
|
|
11
11
|
import type { Context } from '@deepseek-ai/cordis';
|
|
12
12
|
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
13
|
-
import z from 'schemastery';
|
|
13
|
+
import z from '@deepseek-ai/schemastery';
|
|
14
14
|
import { type PairingConfig } from './pairing.ts';
|
|
15
15
|
declare module '@deepseek-ai/cordis' {
|
|
16
16
|
interface Events {
|
|
17
|
+
/**
|
|
18
|
+
* Volatile config values were committed into the running fiber without a
|
|
19
|
+
* remount; dispatched to the owning fiber only. Spelled here because the
|
|
20
|
+
* Loader package is not a dependency of this plugin, with the Loader's own
|
|
21
|
+
* shape so the two declarations merge when a Host program carries both.
|
|
22
|
+
* @param paths - changed config paths as key arrays; every value is committed before dispatch.
|
|
23
|
+
* @mode emit
|
|
24
|
+
*/
|
|
25
|
+
'loader/volatile-update'(paths: readonly (readonly string[])[]): void;
|
|
17
26
|
/**
|
|
18
27
|
* Waterfall seam on the /api transport fence: the connection plugin
|
|
19
28
|
* fires this per /api request before bridging to the API proxy on
|
|
@@ -147,16 +156,71 @@ export interface Config {
|
|
|
147
156
|
*/
|
|
148
157
|
lanBind?: boolean;
|
|
149
158
|
/**
|
|
150
|
-
* The profile whose cordis.patch.yml the LAN bind toggle manages.
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
159
|
+
* The profile whose cordis.patch.yml the LAN bind toggle manages. Defaults
|
|
160
|
+
* to the profile the Host booted (its `profileContext`), then to the
|
|
161
|
+
* DSH_PROFILE environment variable, then "web" — the Desktop client boots
|
|
162
|
+
* "desktop" without exporting DSH_PROFILE, so the environment alone names
|
|
163
|
+
* the wrong profile there. Must be a single safe path segment (the runtime
|
|
164
|
+
* and environment fallbacks bypass this schema, so the path builder asserts
|
|
165
|
+
* containment independently).
|
|
154
166
|
*/
|
|
155
167
|
profile?: string;
|
|
156
168
|
/** Master switch for the plugin (browser half + host pairing surfaces). */
|
|
157
169
|
enabled?: boolean;
|
|
158
170
|
}
|
|
159
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Plugin config schema. Under the 0.1.7 settings model this schema IS the
|
|
173
|
+
* entry's settings page: the Host derives one form per profile entry from it
|
|
174
|
+
* and serves that form only when at least one field is `volatile()`. The marker
|
|
175
|
+
* is also what admits a write and what keeps the edit on the live path — the
|
|
176
|
+
* Loader commits the new value into the field's reference and announces
|
|
177
|
+
* `loader/volatile-update` on this fiber instead of remounting the row, so the
|
|
178
|
+
* pairing service, its device sessions, the tunnel and the route registrations
|
|
179
|
+
* survive a settings save (see {@link applyImpl}'s sync).
|
|
180
|
+
*
|
|
181
|
+
* The schema is left to inference rather than annotated with `z<Config>`: a
|
|
182
|
+
* volatile field's parsed output is a live reference while its accepted input
|
|
183
|
+
* stays the plain value, so the two sides no longer share one shape and the
|
|
184
|
+
* annotation would reject the schema the Host must be given.
|
|
185
|
+
*
|
|
186
|
+
* The deployment-level fields (`trustedHosts`, `devicesFile`, `profile`) are
|
|
187
|
+
* deliberately NOT volatile: they belong in the profile patch, so the form
|
|
188
|
+
* leaves them to the operator instead of offering a card control that a
|
|
189
|
+
* document write could not honor.
|
|
190
|
+
*/
|
|
191
|
+
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
192
|
+
tokenTtlMs: z<number, number, "volatile-defined">;
|
|
193
|
+
offlineAfterMs: z<number, number, "volatile-defined">;
|
|
194
|
+
maxDevices: z<number, number, "volatile-defined">;
|
|
195
|
+
idleExpireMs: z<number, number, "volatile-defined">;
|
|
196
|
+
cookieName: z<string, string, "volatile-defined">;
|
|
197
|
+
requirePairingForLan: z<boolean, boolean, "volatile-defined">;
|
|
198
|
+
publicBaseUrl: z<string, string, "volatile">;
|
|
199
|
+
trustedHosts: z<string[], string[], "plain">;
|
|
200
|
+
devicesFile: z<string, string, "plain">;
|
|
201
|
+
autoTunnel: z<boolean, boolean, "volatile-defined">;
|
|
202
|
+
tunnelToken: z<string, string, "volatile">;
|
|
203
|
+
relay: z<boolean, boolean, "volatile-defined">;
|
|
204
|
+
lanBind: z<boolean, boolean, "volatile">;
|
|
205
|
+
profile: z<string, string, "plain">;
|
|
206
|
+
enabled: z<boolean, boolean, "volatile-defined">;
|
|
207
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
208
|
+
tokenTtlMs: z<number, number, "volatile-defined">;
|
|
209
|
+
offlineAfterMs: z<number, number, "volatile-defined">;
|
|
210
|
+
maxDevices: z<number, number, "volatile-defined">;
|
|
211
|
+
idleExpireMs: z<number, number, "volatile-defined">;
|
|
212
|
+
cookieName: z<string, string, "volatile-defined">;
|
|
213
|
+
requirePairingForLan: z<boolean, boolean, "volatile-defined">;
|
|
214
|
+
publicBaseUrl: z<string, string, "volatile">;
|
|
215
|
+
trustedHosts: z<string[], string[], "plain">;
|
|
216
|
+
devicesFile: z<string, string, "plain">;
|
|
217
|
+
autoTunnel: z<boolean, boolean, "volatile-defined">;
|
|
218
|
+
tunnelToken: z<string, string, "volatile">;
|
|
219
|
+
relay: z<boolean, boolean, "volatile-defined">;
|
|
220
|
+
lanBind: z<boolean, boolean, "volatile">;
|
|
221
|
+
profile: z<string, string, "plain">;
|
|
222
|
+
enabled: z<boolean, boolean, "volatile-defined">;
|
|
223
|
+
}>>, "plain">;
|
|
160
224
|
/**
|
|
161
225
|
* Fully resolved config: every field non-optional except `publicBaseUrl`,
|
|
162
226
|
* which legitimately resolves to `undefined` when unset (the schema keeps it
|
|
@@ -171,6 +235,34 @@ type ResolvedConfig = Required<Omit<Config, 'publicBaseUrl' | 'trustedHosts' | '
|
|
|
171
235
|
tunnelToken: string | undefined;
|
|
172
236
|
profile: string;
|
|
173
237
|
};
|
|
238
|
+
/**
|
|
239
|
+
* The stable reference a `volatile()` config field resolves to; its owner
|
|
240
|
+
* updates it in place when the user saves.
|
|
241
|
+
*/
|
|
242
|
+
interface ConfigRef<T> {
|
|
243
|
+
/** @returns the value currently committed for the running instance. */
|
|
244
|
+
get(): T;
|
|
245
|
+
}
|
|
246
|
+
/** One resolved config field: a live reference, or a plain value from a hand-built context. */
|
|
247
|
+
type ConfigField<T> = ConfigRef<T> | T;
|
|
248
|
+
/** The config the Host hands {@link applyImpl} — the runtime face of {@link Config}. */
|
|
249
|
+
export interface ResolvedConfigFields {
|
|
250
|
+
tokenTtlMs?: ConfigField<number>;
|
|
251
|
+
offlineAfterMs?: ConfigField<number>;
|
|
252
|
+
maxDevices?: ConfigField<number>;
|
|
253
|
+
idleExpireMs?: ConfigField<number>;
|
|
254
|
+
cookieName?: ConfigField<string>;
|
|
255
|
+
requirePairingForLan?: ConfigField<boolean>;
|
|
256
|
+
publicBaseUrl?: ConfigField<string>;
|
|
257
|
+
trustedHosts?: ConfigField<string[]>;
|
|
258
|
+
devicesFile?: ConfigField<string>;
|
|
259
|
+
autoTunnel?: ConfigField<boolean>;
|
|
260
|
+
tunnelToken?: ConfigField<string>;
|
|
261
|
+
relay?: ConfigField<boolean>;
|
|
262
|
+
lanBind?: ConfigField<boolean>;
|
|
263
|
+
profile?: ConfigField<string>;
|
|
264
|
+
enabled?: ConfigField<boolean>;
|
|
265
|
+
}
|
|
174
266
|
/**
|
|
175
267
|
* The single mapping from resolved plugin config to the pairing service
|
|
176
268
|
* config. Both the constructed service and every later apply of this row
|
|
@@ -185,6 +277,6 @@ export declare function defaultDevicesFile(home?: string): string;
|
|
|
185
277
|
* @param config - resolved plugin config (schema defaults applied by the loader).
|
|
186
278
|
*/
|
|
187
279
|
export declare const apply: typeof applyImpl;
|
|
188
|
-
declare function applyImpl(ctx: Context, config?:
|
|
280
|
+
declare function applyImpl(ctx: Context, config?: ResolvedConfigFields): void;
|
|
189
281
|
export {};
|
|
190
282
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
package/lib/types/index.js
CHANGED
|
@@ -7,15 +7,14 @@
|
|
|
7
7
|
* `./client` entry) renders the sidebar entry, the pairing panel, and the
|
|
8
8
|
* phone-side pair/accept + deep-link flow.
|
|
9
9
|
*/
|
|
10
|
-
import { createRequire } from 'node:module';
|
|
11
10
|
import { join } from 'node:path';
|
|
12
11
|
import { setInterval as nodeSetInterval, setTimeout as nodeSetTimeout } from 'node:timers';
|
|
13
|
-
import z from 'schemastery';
|
|
12
|
+
import z from '@deepseek-ai/schemastery';
|
|
14
13
|
import { DEFAULT_IDLE_EXPIRE_MS, PairingService } from "./pairing.js";
|
|
15
14
|
import { dshHome } from "./dsh-home.js";
|
|
16
15
|
import { isPairedDeviceRequest, makeGateListener } from "./gate.js";
|
|
17
16
|
import { RemoteWebUiPairing } from "./pairing-access.js";
|
|
18
|
-
import {
|
|
17
|
+
import { makeRoutes } from "./routes.js";
|
|
19
18
|
import { makeRemoteApiRoutes, makeRemoteApiUpgradeRoutes } from "./remote-api.js";
|
|
20
19
|
import { startRemotePresencePet } from "./remote-presence-pet.js";
|
|
21
20
|
import { claimPostureKey, postureTargets, probePosture, releasePostureKey } from "./posture.js";
|
|
@@ -24,13 +23,11 @@ import { ensureFirewallRule, firewallSummary, removeFirewallRule } from "./firew
|
|
|
24
23
|
import { lanBindState, writeLanBind } from "./lan-bind.js";
|
|
25
24
|
import { isHttpUrl, tunnelPlanOf } from "./tunnel-plan.js";
|
|
26
25
|
import { loadRelayIdentity, RelayRegistrar } from "./relay-registry.js";
|
|
27
|
-
import { desiredBindHost, desiredBindPort, firewallActionNeeded, pendingRestartOf } from "./lan-bind-plan.js";
|
|
26
|
+
import { desiredBindHost, desiredBindPort, firewallActionNeeded, pendingRestartOf, resolveManagedProfile } from "./lan-bind-plan.js";
|
|
28
27
|
import { createInnerAuth } from "./inner-auth.js";
|
|
29
28
|
import { withIdentityEncoding } from "./http.js";
|
|
30
29
|
import { TunnelManager } from "./tunnel.js";
|
|
31
30
|
import { PublicBaseKeeper } from "./public-base.js";
|
|
32
|
-
import { checkUpdates, fetchGitHubReleaseNotes, fetchLatestVersion, RELEASE_NOTES_CACHE_TTL_MS, resolveAnchorManifest, resolveUpdateTarget, runUpdateVerified, } from "./update.js";
|
|
33
|
-
import { makeUpdateRoutes } from "./update-routes.js";
|
|
34
31
|
import { mountOnce } from "./mount-once.js";
|
|
35
32
|
import { REMOTE_CHANNEL_BOOT_SCRIPT } from "./remote-channel-boot.js";
|
|
36
33
|
import { UUID_POLYFILL_SCRIPT } from "./uuid-polyfill.js";
|
|
@@ -47,31 +44,68 @@ export const inject = ['webServer', 'typertGateway', 'connection'];
|
|
|
47
44
|
* Host package.
|
|
48
45
|
*/
|
|
49
46
|
export const REMOTE_WEB_UI_SETTINGS_NAMESPACE = 'remote-web-ui';
|
|
47
|
+
/**
|
|
48
|
+
* Plugin config schema. Under the 0.1.7 settings model this schema IS the
|
|
49
|
+
* entry's settings page: the Host derives one form per profile entry from it
|
|
50
|
+
* and serves that form only when at least one field is `volatile()`. The marker
|
|
51
|
+
* is also what admits a write and what keeps the edit on the live path — the
|
|
52
|
+
* Loader commits the new value into the field's reference and announces
|
|
53
|
+
* `loader/volatile-update` on this fiber instead of remounting the row, so the
|
|
54
|
+
* pairing service, its device sessions, the tunnel and the route registrations
|
|
55
|
+
* survive a settings save (see {@link applyImpl}'s sync).
|
|
56
|
+
*
|
|
57
|
+
* The schema is left to inference rather than annotated with `z<Config>`: a
|
|
58
|
+
* volatile field's parsed output is a live reference while its accepted input
|
|
59
|
+
* stays the plain value, so the two sides no longer share one shape and the
|
|
60
|
+
* annotation would reject the schema the Host must be given.
|
|
61
|
+
*
|
|
62
|
+
* The deployment-level fields (`trustedHosts`, `devicesFile`, `profile`) are
|
|
63
|
+
* deliberately NOT volatile: they belong in the profile patch, so the form
|
|
64
|
+
* leaves them to the operator instead of offering a card control that a
|
|
65
|
+
* document write could not honor.
|
|
66
|
+
*/
|
|
50
67
|
export const Config = z.object({
|
|
51
|
-
tokenTtlMs: z.number().step(1).min(60_000).default(10 * 60_000),
|
|
52
|
-
offlineAfterMs: z.number().step(1).min(5_000).default(25_000),
|
|
53
|
-
maxDevices: z.number().step(1).min(1).max(64).default(4),
|
|
54
|
-
idleExpireMs: z.number().step(1).min(60_000).default(DEFAULT_IDLE_EXPIRE_MS),
|
|
55
|
-
cookieName: z.string().min(1).default('dsh_pair'),
|
|
56
|
-
requirePairingForLan: z.boolean().default(true),
|
|
57
|
-
publicBaseUrl: z.string(),
|
|
68
|
+
tokenTtlMs: z.number().step(1).min(60_000).default(10 * 60_000).volatile(),
|
|
69
|
+
offlineAfterMs: z.number().step(1).min(5_000).default(25_000).volatile(),
|
|
70
|
+
maxDevices: z.number().step(1).min(1).max(64).default(4).volatile(),
|
|
71
|
+
idleExpireMs: z.number().step(1).min(60_000).default(DEFAULT_IDLE_EXPIRE_MS).volatile(),
|
|
72
|
+
cookieName: z.string().min(1).default('dsh_pair').volatile(),
|
|
73
|
+
requirePairingForLan: z.boolean().default(true).volatile(),
|
|
74
|
+
publicBaseUrl: z.string().volatile(),
|
|
58
75
|
trustedHosts: z.array(z.string()),
|
|
59
76
|
devicesFile: z.string(),
|
|
60
|
-
autoTunnel: z.boolean().default(false),
|
|
61
|
-
tunnelToken: z.string().role('secret'),
|
|
62
|
-
relay: z.boolean().default(true),
|
|
63
|
-
lanBind: z.boolean(),
|
|
77
|
+
autoTunnel: z.boolean().default(false).volatile(),
|
|
78
|
+
tunnelToken: z.string().role('secret').volatile(),
|
|
79
|
+
relay: z.boolean().default(true).volatile(),
|
|
80
|
+
lanBind: z.boolean().volatile(),
|
|
64
81
|
profile: z.string().pattern(/^[A-Za-z0-9][A-Za-z0-9._-]*$/),
|
|
65
|
-
enabled: z.boolean().default(true),
|
|
82
|
+
enabled: z.boolean().default(true).volatile(),
|
|
66
83
|
});
|
|
67
84
|
/** Presence sweep cadence (a stale device flips to disconnected within two sweeps). */
|
|
68
85
|
const SWEEP_INTERVAL_MS = 10_000;
|
|
86
|
+
/** Read one resolved config field, following the live reference the schema produces. */
|
|
87
|
+
function readConfigField(field, fallback) {
|
|
88
|
+
if (field === undefined)
|
|
89
|
+
return fallback;
|
|
90
|
+
if (typeof field === 'object' && field !== null && typeof field.get === 'function') {
|
|
91
|
+
const value = field.get();
|
|
92
|
+
return value === undefined ? fallback : value;
|
|
93
|
+
}
|
|
94
|
+
return field;
|
|
95
|
+
}
|
|
69
96
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
97
|
+
* Read one optional resolved config field. A volatile reference is read at
|
|
98
|
+
* call time, so an unset field stays `undefined` rather than falling back to a
|
|
99
|
+
* schema default (the distinction the LAN toggle and the tunnel plan depend on).
|
|
73
100
|
*/
|
|
74
|
-
|
|
101
|
+
function readOptionalConfigField(field) {
|
|
102
|
+
if (field === undefined)
|
|
103
|
+
return undefined;
|
|
104
|
+
if (typeof field === 'object' && field !== null && typeof field.get === 'function') {
|
|
105
|
+
return field.get();
|
|
106
|
+
}
|
|
107
|
+
return field;
|
|
108
|
+
}
|
|
75
109
|
/**
|
|
76
110
|
* The single mapping from resolved plugin config to the pairing service
|
|
77
111
|
* config. Both the constructed service and every later apply of this row
|
|
@@ -106,9 +140,22 @@ const DEFAULTS = {
|
|
|
106
140
|
tunnelToken: undefined,
|
|
107
141
|
relay: true,
|
|
108
142
|
lanBind: undefined,
|
|
109
|
-
profile: process.env.DSH_PROFILE
|
|
143
|
+
profile: resolveManagedProfile(undefined, undefined, process.env.DSH_PROFILE),
|
|
110
144
|
enabled: true,
|
|
111
145
|
};
|
|
146
|
+
/**
|
|
147
|
+
* The launched profile the Host publishes on its `profileContext` service
|
|
148
|
+
* (`{ name, dir, patchPath, … }`). The Desktop client boots the "desktop"
|
|
149
|
+
* profile without exporting DSH_PROFILE, so the environment alone resolves
|
|
150
|
+
* the wrong profile there and the LAN bind toggle would edit a profile that
|
|
151
|
+
* is not running.
|
|
152
|
+
* @param ctx - host plugin context.
|
|
153
|
+
* @returns the profile name when the Host publishes one; undefined otherwise.
|
|
154
|
+
*/
|
|
155
|
+
function launchedProfileName(ctx) {
|
|
156
|
+
const fact = ctx.get('profileContext');
|
|
157
|
+
return typeof fact?.name === 'string' && fact.name.length > 0 ? fact.name : undefined;
|
|
158
|
+
}
|
|
112
159
|
/**
|
|
113
160
|
* Mount the pairing service, routes, gate listener, and presence sweep.
|
|
114
161
|
* @param ctx - host plugin context carrying webServer.
|
|
@@ -117,30 +164,29 @@ const DEFAULTS = {
|
|
|
117
164
|
export const apply = mountOnce('@linxin666/dsh-remote-web-ui', applyImpl);
|
|
118
165
|
function applyImpl(ctx, config) {
|
|
119
166
|
const envPublicBase = process.env.DSH_REMOTE_PUBLIC_BASE_URL?.trim() || undefined;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
const service = new PairingService(pairingConfigOf(resolved));
|
|
167
|
+
// The settings are read through the volatile references at every call rather
|
|
168
|
+
// than captured once: a settings save commits into those references and
|
|
169
|
+
// announces `loader/volatile-update` (see the listener below), so resolving
|
|
170
|
+
// them here is what makes a live edit reach the running instance without a
|
|
171
|
+
// remount — and keeps the device sessions, the tunnel and the routes alive.
|
|
172
|
+
const resolve = () => ({
|
|
173
|
+
tokenTtlMs: readConfigField(config?.tokenTtlMs, DEFAULTS.tokenTtlMs),
|
|
174
|
+
offlineAfterMs: readConfigField(config?.offlineAfterMs, DEFAULTS.offlineAfterMs),
|
|
175
|
+
maxDevices: readConfigField(config?.maxDevices, DEFAULTS.maxDevices),
|
|
176
|
+
idleExpireMs: readConfigField(config?.idleExpireMs, DEFAULTS.idleExpireMs),
|
|
177
|
+
cookieName: readConfigField(config?.cookieName, DEFAULTS.cookieName),
|
|
178
|
+
requirePairingForLan: readConfigField(config?.requirePairingForLan, DEFAULTS.requirePairingForLan),
|
|
179
|
+
publicBaseUrl: readOptionalConfigField(config?.publicBaseUrl) ?? envPublicBase,
|
|
180
|
+
trustedHosts: readOptionalConfigField(config?.trustedHosts),
|
|
181
|
+
devicesFile: readConfigField(config?.devicesFile, DEFAULTS.devicesFile),
|
|
182
|
+
autoTunnel: readConfigField(config?.autoTunnel, DEFAULTS.autoTunnel),
|
|
183
|
+
tunnelToken: readOptionalConfigField(config?.tunnelToken),
|
|
184
|
+
relay: readConfigField(config?.relay, DEFAULTS.relay),
|
|
185
|
+
lanBind: readOptionalConfigField(config?.lanBind),
|
|
186
|
+
profile: resolveManagedProfile(readOptionalConfigField(config?.profile), launchedProfileName(ctx), process.env.DSH_PROFILE),
|
|
187
|
+
enabled: readConfigField(config?.enabled, DEFAULTS.enabled),
|
|
188
|
+
});
|
|
189
|
+
const service = new PairingService(pairingConfigOf(resolve()));
|
|
144
190
|
// ── auto tunnel ─────────────────────────────────────────────────────────
|
|
145
191
|
// The minted public URL becomes the QR base (and the pairing fence's
|
|
146
192
|
// trusted host). Phone /api traffic rides the plugin's own /remote channel,
|
|
@@ -213,7 +259,7 @@ function applyImpl(ctx, config) {
|
|
|
213
259
|
};
|
|
214
260
|
// 'off' until a sync pass turns a mode on; the phase listener only feeds
|
|
215
261
|
// the public base while a plugin-managed tunnel (quick or named) runs.
|
|
216
|
-
let tunnelMode =
|
|
262
|
+
let tunnelMode = resolve().autoTunnel ? 'quick' : 'off';
|
|
217
263
|
tunnel.onPhase((info) => {
|
|
218
264
|
if (tunnelMode === 'off')
|
|
219
265
|
return;
|
|
@@ -272,92 +318,6 @@ function applyImpl(ctx, config) {
|
|
|
272
318
|
// request) instead of opening the fence.
|
|
273
319
|
let disposeRoutes;
|
|
274
320
|
let disposeSweep;
|
|
275
|
-
// ── remote update ────────────────────────────────────────────────────────
|
|
276
|
-
// The dsh-web self-update surface: probe the npm registry for family
|
|
277
|
-
// releases and run `pnpm update --latest` in the owning profile. Resolutions
|
|
278
|
-
// anchor on the host process's own module graph, so the update always
|
|
279
|
-
// targets the profile the running web GUI was booted from. The anchor path
|
|
280
|
-
// is re-resolved per operation: pnpm removes the old version's .pnpm
|
|
281
|
-
// directory on update, so a boot-time captured path would fail to read
|
|
282
|
-
// after a successful update; versions are re-read from disk per check.
|
|
283
|
-
const requireFromHost = createRequire(import.meta.url);
|
|
284
|
-
/** Host-process resolve that degrades to "not installed" (undefined) instead of throwing. */
|
|
285
|
-
const hostResolve = (specifier) => {
|
|
286
|
-
try {
|
|
287
|
-
return requireFromHost.resolve(specifier);
|
|
288
|
-
}
|
|
289
|
-
catch {
|
|
290
|
-
return undefined;
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
const resolveAnchorPath = () => resolveAnchorManifest(hostResolve);
|
|
294
|
-
const releaseNotesCache = new Map();
|
|
295
|
-
const fetchReleaseNotesCached = async (version) => {
|
|
296
|
-
const cached = releaseNotesCache.get(version);
|
|
297
|
-
if (cached !== undefined && Date.now() - cached.at < RELEASE_NOTES_CACHE_TTL_MS)
|
|
298
|
-
return cached.notes;
|
|
299
|
-
const notes = await fetchGitHubReleaseNotes(version, fetch);
|
|
300
|
-
releaseNotesCache.set(version, { at: Date.now(), notes });
|
|
301
|
-
return notes;
|
|
302
|
-
};
|
|
303
|
-
// The status probe fans out one registry GET per family package (plus a
|
|
304
|
-
// GitHub release-notes call), and the sidebar update entry asks for it on
|
|
305
|
-
// every GUI page load — so the answer is memoized briefly and concurrent
|
|
306
|
-
// callers share one probe. A completed update invalidates it.
|
|
307
|
-
let updateStatusCache;
|
|
308
|
-
let updateStatusInFlight;
|
|
309
|
-
const checkStatusCached = () => {
|
|
310
|
-
const cached = updateStatusCache;
|
|
311
|
-
if (cached !== undefined && Date.now() - cached.at < UPDATE_STATUS_TTL_MS)
|
|
312
|
-
return Promise.resolve(cached.value);
|
|
313
|
-
if (updateStatusInFlight !== undefined)
|
|
314
|
-
return updateStatusInFlight;
|
|
315
|
-
updateStatusInFlight = checkUpdates({
|
|
316
|
-
anchorManifestPath: resolveAnchorPath(),
|
|
317
|
-
resolve: hostResolve,
|
|
318
|
-
fetchLatest: name => fetchLatestVersion(name, fetch),
|
|
319
|
-
fetchReleaseNotes: fetchReleaseNotesCached,
|
|
320
|
-
}).then((value) => {
|
|
321
|
-
updateStatusCache = { at: Date.now(), value };
|
|
322
|
-
return value;
|
|
323
|
-
}).finally(() => { updateStatusInFlight = undefined; });
|
|
324
|
-
return updateStatusInFlight;
|
|
325
|
-
};
|
|
326
|
-
const updateRoutes = makeUpdateRoutes({
|
|
327
|
-
// Control endpoints are host-surface only: a LAN/phone origin must never
|
|
328
|
-
// trigger a real install on this machine.
|
|
329
|
-
fence: request => isTrustedApiRequest(request, []),
|
|
330
|
-
check: () => checkStatusCached(),
|
|
331
|
-
run: async () => {
|
|
332
|
-
const target = resolveUpdateTarget({ anchorManifestPath: resolveAnchorPath() });
|
|
333
|
-
if ('error' in target) {
|
|
334
|
-
const code = target.error;
|
|
335
|
-
return {
|
|
336
|
-
ok: false,
|
|
337
|
-
exitCode: null,
|
|
338
|
-
output: '',
|
|
339
|
-
error: code === 'not-found' ? 'dsh-web aggregate not installed' : 'local link install — update unavailable',
|
|
340
|
-
errorCode: code,
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
// Verify the versions actually moved after a green pnpm exit: the pnpm
|
|
344
|
-
// 11 minimumReleaseAge gate can silently keep the installed versions
|
|
345
|
-
// (same-day releases), which a plain exit-0 check would report as
|
|
346
|
-
// success — the user then restarts and nothing changed.
|
|
347
|
-
const result = await runUpdateVerified({
|
|
348
|
-
run: { profileDir: target.profileDir, packages: target.packages },
|
|
349
|
-
check: {
|
|
350
|
-
anchorManifestPath: resolveAnchorPath(),
|
|
351
|
-
resolve: hostResolve,
|
|
352
|
-
fetchLatest: name => fetchLatestVersion(name, fetch),
|
|
353
|
-
fetchReleaseNotes: fetchReleaseNotesCached,
|
|
354
|
-
},
|
|
355
|
-
});
|
|
356
|
-
// The install moved (or failed) the versions the cached status reported.
|
|
357
|
-
updateStatusCache = undefined;
|
|
358
|
-
return result;
|
|
359
|
-
},
|
|
360
|
-
});
|
|
361
321
|
// LAN-bind facts for the settings card, re-read per request so a hot
|
|
362
322
|
// rebind (the patch watcher recomposes the process) and a fresh toggle
|
|
363
323
|
// round are both reflected without a restart.
|
|
@@ -488,7 +448,6 @@ function applyImpl(ctx, config) {
|
|
|
488
448
|
port: ctx.webServer.port,
|
|
489
449
|
auth: innerAuth,
|
|
490
450
|
}),
|
|
491
|
-
...updateRoutes,
|
|
492
451
|
];
|
|
493
452
|
const upgrades = makeRemoteApiUpgradeRoutes({
|
|
494
453
|
service,
|
|
@@ -760,8 +719,16 @@ function applyImpl(ctx, config) {
|
|
|
760
719
|
return;
|
|
761
720
|
table.push({ kind: 'script', placement: 'head', text: REMOTE_CHANNEL_BOOT_SCRIPT });
|
|
762
721
|
}), 'remote-web-ui: remote channel boot patch');
|
|
722
|
+
// The settings write path. The Host commits the new values into this row's
|
|
723
|
+
// volatile references and announces it here instead of remounting the row, so
|
|
724
|
+
// `sync` is what re-applies them: it re-reads every field through those
|
|
725
|
+
// references (see `resolve`) and is idempotent, so a save that changes
|
|
726
|
+
// nothing observable is a no-op while a disable, a bind flip or a tunnel
|
|
727
|
+
// change lands on the live instance without dropping the pairing service,
|
|
728
|
+
// its device sessions or the route registrations.
|
|
729
|
+
ctx.on('loader/volatile-update', () => { sync(); });
|
|
763
730
|
// No settings registration: the Host derives this plugin's settings page
|
|
764
|
-
// from the exported `Config` schema
|
|
765
|
-
//
|
|
731
|
+
// from the exported `Config` schema, whose volatile fields are what make the
|
|
732
|
+
// page writable and what route a save through the live path above.
|
|
766
733
|
sync();
|
|
767
734
|
}
|
|
@@ -44,6 +44,20 @@ export interface AppliedFirewallState {
|
|
|
44
44
|
enabled: boolean;
|
|
45
45
|
port: number;
|
|
46
46
|
}
|
|
47
|
+
/** The managed profile when neither the config nor the runtime names one. */
|
|
48
|
+
export declare const DEFAULT_MANAGED_PROFILE = "web";
|
|
49
|
+
/**
|
|
50
|
+
* The profile whose `cordis.patch.yml` the LAN bind toggle manages. An
|
|
51
|
+
* explicit `profile` config wins; otherwise the launched profile the Host
|
|
52
|
+
* publishes is authoritative, because the DSH Desktop client boots the
|
|
53
|
+
* `desktop` profile without exporting `DSH_PROFILE` — an environment-only
|
|
54
|
+
* fallback edited `profiles/web` there, so the toggle looked applied while
|
|
55
|
+
* the running host never followed the block.
|
|
56
|
+
* @param configured - the explicit `profile` plugin config, when set.
|
|
57
|
+
* @param launched - the launched profile name the Host published, when available.
|
|
58
|
+
* @param env - the DSH_PROFILE environment value, when set.
|
|
59
|
+
*/
|
|
60
|
+
export declare function resolveManagedProfile(configured: string | undefined, launched: string | undefined, env: string | undefined): string;
|
|
47
61
|
/**
|
|
48
62
|
* Whether the firewall rule must be (re)applied: only when the toggle or the
|
|
49
63
|
* bound port moved since the last application. Keeps unrelated settings
|
|
@@ -1 +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"}
|
|
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,6EAA6E;AAC7E,eAAO,MAAM,uBAAuB,QAAQ,CAAA;AAE5C;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,GAAG,EAAE,MAAM,GAAG,SAAS,GACtB,MAAM,CAER;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,oBAAoB,GAAG,SAAS,EAC1C,IAAI,EAAE,oBAAoB,GAAG,SAAS,GACrC,OAAO,CAIT"}
|
|
@@ -44,6 +44,22 @@ export function pendingRestartOf(lanBind, desiredHost, liveHost) {
|
|
|
44
44
|
return false;
|
|
45
45
|
return liveHost !== desiredHost;
|
|
46
46
|
}
|
|
47
|
+
/** The managed profile when neither the config nor the runtime names one. */
|
|
48
|
+
export const DEFAULT_MANAGED_PROFILE = 'web';
|
|
49
|
+
/**
|
|
50
|
+
* The profile whose `cordis.patch.yml` the LAN bind toggle manages. An
|
|
51
|
+
* explicit `profile` config wins; otherwise the launched profile the Host
|
|
52
|
+
* publishes is authoritative, because the DSH Desktop client boots the
|
|
53
|
+
* `desktop` profile without exporting `DSH_PROFILE` — an environment-only
|
|
54
|
+
* fallback edited `profiles/web` there, so the toggle looked applied while
|
|
55
|
+
* the running host never followed the block.
|
|
56
|
+
* @param configured - the explicit `profile` plugin config, when set.
|
|
57
|
+
* @param launched - the launched profile name the Host published, when available.
|
|
58
|
+
* @param env - the DSH_PROFILE environment value, when set.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveManagedProfile(configured, launched, env) {
|
|
61
|
+
return configured ?? launched ?? env ?? DEFAULT_MANAGED_PROFILE;
|
|
62
|
+
}
|
|
47
63
|
/**
|
|
48
64
|
* Whether the firewall rule must be (re)applied: only when the toggle or the
|
|
49
65
|
* bound port moved since the last application. Keeps unrelated settings
|
package/lib/types/lan-bind.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export declare const LAN_BIND_BLOCK_END = "# --- end remote-web-ui lan-bind bloc
|
|
|
24
24
|
export type LanBindHost = '0.0.0.0' | '127.0.0.1';
|
|
25
25
|
/**
|
|
26
26
|
* The absolute path of the profile patch file this toggle manages. The value
|
|
27
|
-
* is config-controlled (and the
|
|
27
|
+
* is config-controlled (and the runtime/environment fallbacks bypass schema
|
|
28
28
|
* validation entirely), so the path is guarded twice: the profile must be a
|
|
29
29
|
* single safe path segment, and the resolved file must stay under the
|
|
30
30
|
* profiles directory.
|
package/lib/types/lan-bind.js
CHANGED
|
@@ -28,7 +28,7 @@ export const LAN_BIND_BLOCK_BEGIN = '# --- remote-web-ui lan-bind block (managed
|
|
|
28
28
|
export const LAN_BIND_BLOCK_END = '# --- end remote-web-ui lan-bind block ---';
|
|
29
29
|
/**
|
|
30
30
|
* The absolute path of the profile patch file this toggle manages. The value
|
|
31
|
-
* is config-controlled (and the
|
|
31
|
+
* is config-controlled (and the runtime/environment fallbacks bypass schema
|
|
32
32
|
* validation entirely), so the path is guarded twice: the profile must be a
|
|
33
33
|
* single safe path segment, and the resolved file must stay under the
|
|
34
34
|
* profiles directory.
|
|
@@ -4,19 +4,50 @@
|
|
|
4
4
|
* the loader accepts a standalone install of the same package side by side;
|
|
5
5
|
* without this guard the second instance would still re-register the same
|
|
6
6
|
* webserver routes, tools, settings namespaces, and system-prompt sections
|
|
7
|
-
* and fail the boot. mountOnce makes
|
|
8
|
-
*
|
|
9
|
-
* package name in the client module host).
|
|
7
|
+
* and fail the boot. mountOnce makes a mount of an already-mounted package a
|
|
8
|
+
* no-op for as long as the first instance lives (the browser half is already
|
|
9
|
+
* deduped by package name in the client module host).
|
|
10
|
+
*
|
|
11
|
+
* A no-op is only safe while the holder is ALIVE. The holder can be disposed
|
|
12
|
+
* long after the refused mount ran its course: the Host reloads a profile by
|
|
13
|
+
* creating the new loader entries before the old ones are torn down (a
|
|
14
|
+
* plugin-manager enable/disable/install write, a settings-driven row reload,
|
|
15
|
+
* HMR), so the new aggregate shell entry mounts its family plugin while the
|
|
16
|
+
* previous entry still owns the name. Dropping that refused mount lost the
|
|
17
|
+
* plugin for good - the previous entry then disposed its own mount, releasing
|
|
18
|
+
* the name with nobody left to take it, and the family row stayed listed as
|
|
19
|
+
* active while its host routes 404ed (the task-board panel showed
|
|
20
|
+
* "board.hostError.notMounted", no degraded record appeared, and only a Host
|
|
21
|
+
* restart recovered it).
|
|
22
|
+
*
|
|
23
|
+
* The refused mount is therefore QUEUED, not dropped, and replayed the moment
|
|
24
|
+
* the holder releases the name - if the waiting fiber is still alive then. The
|
|
25
|
+
* single-instance guarantee is unchanged: exactly one mount is live per
|
|
26
|
+
* package name, and the replay re-enters the guard so a later mount still
|
|
27
|
+
* dedupes against it.
|
|
10
28
|
*
|
|
11
29
|
* The registry rides a global symbol so two module instances of the same
|
|
12
|
-
* package (npm copy vs repository link) still share one verdict.
|
|
13
|
-
*
|
|
30
|
+
* package (npm copy vs repository link) still share one verdict. That symbol
|
|
31
|
+
* is a CROSS-REPOSITORY contract, not this file's private state: the four
|
|
32
|
+
* satellite packages (dsh-skins / dsh-pet / dsh-presets /
|
|
33
|
+
* dsh-community-plugins) are separate repositories carrying their own copy of
|
|
34
|
+
* this guard, rebuilt on their own schedule, so the value under `MOUNTED`
|
|
35
|
+
* must keep the shape every published copy reads (a `Set` of package names
|
|
36
|
+
* with `has`/`add`/`delete`). The wait queues this guard added therefore live
|
|
37
|
+
* under their own additive key, and the registry reads back a `Set` even when
|
|
38
|
+
* some other build left a different value there. Changing `MOUNTED`'s shape
|
|
39
|
+
* in place broke that contract once: a satellite's legacy copy created a
|
|
40
|
+
* `Set`, the family's new copy read it as a `Map`, and every family row
|
|
41
|
+
* mounted after it failed with "claims.get is not a function".
|
|
42
|
+
*
|
|
43
|
+
* cordis `ctx.effect` runs its callback immediately and treats the callback's
|
|
14
44
|
* return value as the fiber disposer, so the unmarker is returned, not run.
|
|
15
45
|
*/
|
|
16
46
|
/**
|
|
17
47
|
* Wrap a cordis plugin apply so the package runs at most once per process.
|
|
18
|
-
* The first mount registers normally and
|
|
19
|
-
*
|
|
48
|
+
* The first mount registers normally and releases the name when its fiber
|
|
49
|
+
* disposes; a mount refused while that name is held waits for the release and
|
|
50
|
+
* then runs, unless its own fiber disposes first.
|
|
20
51
|
* @param packageName - npm package identity shared by every install source.
|
|
21
52
|
* @param fn - the original plugin apply.
|
|
22
53
|
* @returns an apply of the same shape.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mount-once.d.ts","sourceRoot":"","sources":["../../src/mount-once.ts"],"names":[],"mappings":"AACA
|
|
1
|
+
{"version":3,"file":"mount-once.d.ts","sourceRoot":"","sources":["../../src/mount-once.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AA6CH;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,CA0C9F"}
|