@linxin666/dsh-remote-web-ui 0.4.1 → 0.4.2
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 +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +132 -29
- package/lib/client.js.map +1 -1
- package/lib/index.js +117 -15
- package/lib/types/client/RemoteEntry.js +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +5 -3
- package/lib/types/client/locales.js +2 -2
- package/lib/types/client/pair-api.d.ts +4 -0
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +11 -7
- package/lib/types/client/remote-channel.d.ts +27 -7
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +25 -17
- package/lib/types/client/update-api.js +2 -2
- package/lib/types/lan-bind.d.ts +4 -0
- package/lib/types/lan-bind.d.ts.map +1 -1
- package/lib/types/lan-bind.js +64 -1
- package/lib/types/pairing.d.ts +15 -2
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +26 -14
- package/lib/types/pairing.persist.test.js +43 -3
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +13 -3
- package/lib/types/remote-channel-rules.d.ts +74 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +104 -0
- package/lib/types/update.d.ts +10 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +32 -3
- package/package.json +20 -19
- package/src/client/RemoteEntry.tsx +1 -1
- package/src/client/index.ts +5 -3
- package/src/client/locales.ts +2 -2
- package/src/client/pair-api.ts +11 -7
- package/src/client/remote-channel.ts +38 -15
- package/src/client/remote.module.css +18 -0
- package/src/client/update-api.ts +2 -2
- package/src/lan-bind.ts +62 -1
- package/src/pairing.persist.test.ts +47 -3
- package/src/pairing.ts +26 -12
- package/src/remote-channel-boot.ts +13 -3
- package/src/remote-channel-rules.ts +113 -0
- package/src/update.ts +36 -3
package/lib/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { z as z$1 } from "zod";
|
|
|
11
11
|
import http, { request } from "node:http";
|
|
12
12
|
import { connect } from "node:net";
|
|
13
13
|
import { spawn, spawnSync } from "node:child_process";
|
|
14
|
+
import { parseDocument } from "yaml";
|
|
14
15
|
import { EventEmitter } from "node:events";
|
|
15
16
|
import { Tunnel, bin, install } from "cloudflared";
|
|
16
17
|
//#region src/pairing.ts
|
|
@@ -108,6 +109,14 @@ var PairingService = class {
|
|
|
108
109
|
* Restore device sessions persisted by a previous process run. A corrupt
|
|
109
110
|
* or missing file is tolerated (an empty device table, never a throw) —
|
|
110
111
|
* persistence is an availability convenience, not a security boundary.
|
|
112
|
+
*
|
|
113
|
+
* Every still-valid session is restored, deliberately without applying
|
|
114
|
+
* `maxDevices`: the constructor runs with whatever cap the configuration
|
|
115
|
+
* layer has delivered so far (the schema default until the saved settings
|
|
116
|
+
* row is applied), and trimming here would permanently drop authorizations
|
|
117
|
+
* the user never revoked — the trimmed table reaches disk on the next
|
|
118
|
+
* heartbeat/sweep write. The cap is enforced where a device is admitted
|
|
119
|
+
* (see `accept`), against the configuration in force at that moment.
|
|
111
120
|
*/
|
|
112
121
|
loadPersisted() {
|
|
113
122
|
const file = this.config.devicesFile;
|
|
@@ -127,17 +136,9 @@ var PairingService = class {
|
|
|
127
136
|
...label !== void 0 ? { userAgent: label } : {}
|
|
128
137
|
});
|
|
129
138
|
}
|
|
130
|
-
this.clampToMaxDevices();
|
|
131
139
|
if (this.evictIdle()) this.persistRevocation();
|
|
132
140
|
} catch {}
|
|
133
141
|
}
|
|
134
|
-
/** FIFO-cap the device table (a persisted file may outlive a lowered cap). */
|
|
135
|
-
clampToMaxDevices() {
|
|
136
|
-
if (this.devices.size <= this.config.maxDevices) return;
|
|
137
|
-
const overflow = this.devices.size - this.config.maxDevices;
|
|
138
|
-
const ordered = [...this.devices.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
|
|
139
|
-
for (const [id] of ordered.slice(0, overflow)) this.devices.delete(id);
|
|
140
|
-
}
|
|
141
142
|
/** Drop sessions whose lastSeenAt is older than idleExpireMs. */
|
|
142
143
|
evictIdle() {
|
|
143
144
|
const now = this.clock.now();
|
|
@@ -294,13 +295,14 @@ var PairingService = class {
|
|
|
294
295
|
};
|
|
295
296
|
const deviceId = this.clock.randomToken();
|
|
296
297
|
const now = this.clock.now();
|
|
297
|
-
|
|
298
|
+
while (this.devices.size >= this.config.maxDevices) {
|
|
298
299
|
let oldest;
|
|
299
300
|
for (const [id, session] of this.devices) if (oldest === void 0 || session.createdAt < oldest.createdAt) oldest = {
|
|
300
301
|
id,
|
|
301
302
|
createdAt: session.createdAt
|
|
302
303
|
};
|
|
303
|
-
if (oldest
|
|
304
|
+
if (oldest === void 0) break;
|
|
305
|
+
this.devices.delete(oldest.id);
|
|
304
306
|
}
|
|
305
307
|
const label = sanitizeUserAgent(userAgent);
|
|
306
308
|
this.devices.set(deviceId, {
|
|
@@ -402,6 +404,15 @@ var PairingService = class {
|
|
|
402
404
|
hasDevice(deviceId) {
|
|
403
405
|
return this.liveSession(deviceId) !== void 0;
|
|
404
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* Number of device sessions in the table, including idle ones whose idle
|
|
409
|
+
* window has not been swept yet. Restore keeps every persisted session
|
|
410
|
+
* (see `loadPersisted`), so this can exceed the configured cap until the
|
|
411
|
+
* next `accept` trims the excess.
|
|
412
|
+
*/
|
|
413
|
+
deviceCount() {
|
|
414
|
+
return this.devices.size;
|
|
415
|
+
}
|
|
405
416
|
/** Subscribe to snapshot changes (each emit passes a fresh snapshot). */
|
|
406
417
|
onState(listener) {
|
|
407
418
|
this.listeners.add(listener);
|
|
@@ -883,6 +894,32 @@ function localOnlyDenial(innerPath) {
|
|
|
883
894
|
/** The gated mirror prefix (must match src/remote-methods.ts). */
|
|
884
895
|
const REMOTE_PREFIX = "/remote";
|
|
885
896
|
/**
|
|
897
|
+
* The schemes that can deliver a page a remote party controls: the network
|
|
898
|
+
* transports, plus the documents a network page can mint. Every other scheme
|
|
899
|
+
* is registered and served by an application on this machine, so its page is
|
|
900
|
+
* the machine's own page whatever the application calls the scheme.
|
|
901
|
+
*
|
|
902
|
+
* The list is deliberately the *web* side rather than an allowlist of known
|
|
903
|
+
* desktop shells. The official DSH Desktop shell serves its Web GUI from
|
|
904
|
+
* `dsh-app://app/` (`location.hostname === 'app'`), which no hostname
|
|
905
|
+
* predicate can recognise; a scheme allowlist fixed that instance (#1682) but
|
|
906
|
+
* left the next shell — or a `file:` page — fenced behind a pairing page it
|
|
907
|
+
* can never complete, because a pairing link is reachable only over the
|
|
908
|
+
* network. Naming the web side instead makes an unknown application scheme
|
|
909
|
+
* local by construction.
|
|
910
|
+
*
|
|
911
|
+
* `blob:`, `data:`, `about:` and `filesystem:` stay on the web side: a
|
|
912
|
+
* network page mints those documents, so they must keep the fence.
|
|
913
|
+
*/
|
|
914
|
+
const WEB_PAGE_PROTOCOLS = [
|
|
915
|
+
"http:",
|
|
916
|
+
"https:",
|
|
917
|
+
"blob:",
|
|
918
|
+
"data:",
|
|
919
|
+
"about:",
|
|
920
|
+
"filesystem:"
|
|
921
|
+
];
|
|
922
|
+
/**
|
|
886
923
|
* The window global the device-gated app landing publishes to grant host mode.
|
|
887
924
|
* Set by the /pair-app capture script, read by the parse-time boot patch: the
|
|
888
925
|
* transport hook (ownsHost) is server-granted, not asserted from the origin.
|
|
@@ -910,6 +947,7 @@ const REMOTE_CHANNEL_RULES = {
|
|
|
910
947
|
deviceQuery: REMOTE_DEVICE_QUERY,
|
|
911
948
|
uploadPath: "/api/session/uploadFileBinary",
|
|
912
949
|
uploadHookGlobal: "__DSH_FILE_UPLOAD__",
|
|
950
|
+
webProtocols: WEB_PAGE_PROTOCOLS,
|
|
913
951
|
hostGrantGlobal: REMOTE_HOST_GRANT_GLOBAL
|
|
914
952
|
};
|
|
915
953
|
/** The window global the boot patch publishes its seat under. */
|
|
@@ -2677,6 +2715,47 @@ function stripManagedBlock(content) {
|
|
|
2677
2715
|
* re-asserts the block at every boot so CLI flags (--port, --host) win by
|
|
2678
2716
|
* rewriting it before the next start.
|
|
2679
2717
|
*/
|
|
2718
|
+
/**
|
|
2719
|
+
* Whether the base opens a flow collection at the root. That is the shape the
|
|
2720
|
+
* plugin manager's YAML document round-trip leaves behind when it appends rows
|
|
2721
|
+
* to the profile's placeholder `[]`: `[ { id: … }, … ]`. Every other base
|
|
2722
|
+
* (empty, block sequence, comment-only prefix) keeps the historical string
|
|
2723
|
+
* concatenation, so the common paths stay byte-identical.
|
|
2724
|
+
*/
|
|
2725
|
+
function isFlowRoot(base) {
|
|
2726
|
+
return base.trimStart().startsWith("[");
|
|
2727
|
+
}
|
|
2728
|
+
/** Force one parsed collection (and its item collections) into block style. */
|
|
2729
|
+
function toBlockStyle(node) {
|
|
2730
|
+
if (typeof node !== "object" || node === null) return;
|
|
2731
|
+
const collection = node;
|
|
2732
|
+
if (!Array.isArray(collection.items) && typeof collection.flow !== "boolean") return;
|
|
2733
|
+
collection.flow = false;
|
|
2734
|
+
for (const item of collection.items ?? []) toBlockStyle(item);
|
|
2735
|
+
}
|
|
2736
|
+
/**
|
|
2737
|
+
* Produce the file text for one bind state. A flow-style base is re-emitted as
|
|
2738
|
+
* a block sequence before the managed block is appended, so the file stays
|
|
2739
|
+
* exactly one valid YAML document; concatenating the block onto a non-empty
|
|
2740
|
+
* flow array produced two root documents and the profile failed to parse at
|
|
2741
|
+
* `dsh web` startup (#1675). A base that cannot be parsed is refused with the
|
|
2742
|
+
* file path rather than written half-valid.
|
|
2743
|
+
*/
|
|
2744
|
+
function mergePatchContent(base, block, file) {
|
|
2745
|
+
if (base.length === 0) return block;
|
|
2746
|
+
if (!isFlowRoot(base)) return `${base}\n\n${block}`;
|
|
2747
|
+
const doc = parseDocument(base, { uniqueKeys: false });
|
|
2748
|
+
if (doc.errors.length > 0 || doc.contents === null) throw new Error(`remote-web-ui: cannot update the lan-bind block in ${file}: ${doc.errors[0]?.message ?? "the patch file is not a YAML document"}`);
|
|
2749
|
+
if (!("items" in doc.contents)) throw new Error(`remote-web-ui: cannot update the lan-bind block in ${file}: the patch file is not a top-level patch list`);
|
|
2750
|
+
toBlockStyle(doc.contents);
|
|
2751
|
+
return `${doc.toString({ lineWidth: 0 }).trimEnd()}\n\n${block}`;
|
|
2752
|
+
}
|
|
2753
|
+
/**
|
|
2754
|
+
* Render the managed block for one bind state. The values are static: the
|
|
2755
|
+
* user patch layer has no reliable lazy service evaluation, and the plugin
|
|
2756
|
+
* re-asserts the block at every boot so CLI flags (--port, --host) win by
|
|
2757
|
+
* rewriting it before the next start.
|
|
2758
|
+
*/
|
|
2680
2759
|
function managedBlock(host, port) {
|
|
2681
2760
|
return [
|
|
2682
2761
|
LAN_BIND_BLOCK_BEGIN,
|
|
@@ -2727,14 +2806,16 @@ function lanBindState(profile, home = dshHome()) {
|
|
|
2727
2806
|
* unterminated block (BEGIN marker without END, which stripManagedBlock
|
|
2728
2807
|
* cannot match) is truncated at its BEGIN marker first so the rewrite can
|
|
2729
2808
|
* never stack a second webserver row onto the orphan.
|
|
2809
|
+
*
|
|
2810
|
+
* The written file is always exactly one valid YAML document: a base the
|
|
2811
|
+
* plugin manager left in flow style is re-emitted as a block sequence before
|
|
2812
|
+
* the managed block is appended (see mergePatchContent).
|
|
2730
2813
|
*/
|
|
2731
2814
|
function writeLanBind(host, port, profile, home = dshHome()) {
|
|
2732
2815
|
const file = profilePatchFile(profile, home);
|
|
2733
2816
|
const stripped = stripManagedBlock(readPatchContent(file));
|
|
2734
2817
|
const orphanBegin = stripped.indexOf(LAN_BIND_BLOCK_BEGIN);
|
|
2735
|
-
const
|
|
2736
|
-
const block = managedBlock(host, port);
|
|
2737
|
-
const content = base.length > 0 ? `${base}\n\n${block}` : block;
|
|
2818
|
+
const content = mergePatchContent((orphanBegin === -1 ? stripped : stripped.slice(0, orphanBegin)).trimEnd().replace(/\[\s*\]\s*$/, "").trimEnd(), managedBlock(host, port), file);
|
|
2738
2819
|
const mode = existsSync(file) ? statSync(file).mode & 511 : 384;
|
|
2739
2820
|
mkdirSync(dirname(file), { recursive: true });
|
|
2740
2821
|
const temp = `${file}.remote-web-ui-tmp-${process.pid.toString(36)}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -3769,6 +3850,27 @@ function familyUpdatePackages(anchor, anchorManifest, profileManifest) {
|
|
|
3769
3850
|
return [...names];
|
|
3770
3851
|
}
|
|
3771
3852
|
/**
|
|
3853
|
+
* Probe every package's latest version with a bounded number of requests in
|
|
3854
|
+
* flight, preserving the caller's order in the result.
|
|
3855
|
+
* @param names - package names to probe.
|
|
3856
|
+
* @param fetchLatest - the probe for one package (returns undefined on failure).
|
|
3857
|
+
* @returns one result per name, in the input order.
|
|
3858
|
+
*/
|
|
3859
|
+
async function probeLatestVersions(names, fetchLatest) {
|
|
3860
|
+
const results = new Array(names.length);
|
|
3861
|
+
let next = 0;
|
|
3862
|
+
const worker = async () => {
|
|
3863
|
+
for (;;) {
|
|
3864
|
+
const index = next++;
|
|
3865
|
+
if (index >= names.length) return;
|
|
3866
|
+
results[index] = await fetchLatest(names[index]);
|
|
3867
|
+
}
|
|
3868
|
+
};
|
|
3869
|
+
const workers = Math.min(4, names.length);
|
|
3870
|
+
await Promise.all(Array.from({ length: workers }, () => worker()));
|
|
3871
|
+
return results;
|
|
3872
|
+
}
|
|
3873
|
+
/**
|
|
3772
3874
|
* Probe the npm registry for one package's latest release.
|
|
3773
3875
|
* @param name - the package name (scope slash URL-encoded).
|
|
3774
3876
|
* @param fetchImpl - the fetch implementation (global fetch in the host).
|
|
@@ -3924,7 +4026,7 @@ async function checkUpdates(deps) {
|
|
|
3924
4026
|
outdated: false
|
|
3925
4027
|
};
|
|
3926
4028
|
const names = familyUpdatePackages(anchor, manifest, profileManifest);
|
|
3927
|
-
const latestList = await
|
|
4029
|
+
const latestList = await probeLatestVersions(names, deps.fetchLatest);
|
|
3928
4030
|
const packages = [];
|
|
3929
4031
|
let probeFailures = 0;
|
|
3930
4032
|
names.forEach((name, index) => {
|
|
@@ -4355,7 +4457,7 @@ function mountOnce(packageName, fn) {
|
|
|
4355
4457
|
function buildRemoteChannelBootScript(rules = REMOTE_CHANNEL_RULES) {
|
|
4356
4458
|
const json = JSON.stringify(rules);
|
|
4357
4459
|
const seat = JSON.stringify(REMOTE_CHANNEL_BOOT_GLOBAL);
|
|
4358
|
-
return "(function(){try{var w=window,loc=w.location,h=loc.hostname;if(h==='localhost'||h==='::1'||h==='[::1]'||/^127(\\.\\d{1,3}){3}$/.test(h))return;var
|
|
4460
|
+
return "(function(){try{var w=window,loc=w.location,h=loc.hostname;var R=" + json + ";if(h==='localhost'||h==='::1'||h==='[::1]'||/^127(\\.\\d{1,3}){3}$/.test(h))return;var pr=loc.protocol;if(typeof pr===\"string\"&&pr!==\"\"&&R.webProtocols.indexOf(pr)===-1)return;try{if(w.__DSH_TRANSPORT__&&w.__DSH_TRANSPORT__.ownsHost===true&&h.indexOf(\".\")===-1&&h.indexOf(\":\")===-1)return}catch(e){}try{if(w[R.hostGrantGlobal]===true){if(w.__DSH_TRANSPORT__===undefined)w.__DSH_TRANSPORT__={};w.__DSH_TRANSPORT__.ownsHost=true}}catch(e){}function rdv(){try{return w.sessionStorage.getItem(R.deviceKey)}catch(e){return null}}function att(init){var dv=rdv();if(dv===null)return init;var h=init&&init.headers;if(typeof Headers!==\"undefined\"&&h instanceof Headers){try{h.set(R.deviceHeader,dv)}catch(e){}return init}if(typeof h===\"object\"&&h!==null){var o={};for(var k in h)o[k]=h[k];o[R.deviceHeader]=dv;return Object.assign({},init,{headers:o})}return init}function sf(p){if(p.indexOf(R.pairPrefix)===0)return false;if(p.indexOf(R.updatePrefix)===0)return false;if(p===R.settingsBridgePrefix||p.indexOf(R.settingsBridgePrefix+\"/\")===0)return false;if(p.indexOf(R.apiPrefix)===0)return true;if(p.indexOf(R.sidebarPrefix)===0||p===\"/sidebar\")return true;if(p.indexOf(R.gitPrefix)===0||p===\"/git\")return true;if(p.indexOf(R.petPrefix)===0||p===\"/pet\")return true;return false}function sw(p){return R.wsPaths.indexOf(p)!==-1}function rp(p){return R.remotePrefix+p}function so(u){return u.origin===loc.origin}function rr(raw){var u;try{u=new URL(raw,loc.href)}catch(e){return raw}if(u.origin!==loc.origin)return raw;if(!sf(u.pathname))return raw;u.pathname=rp(u.pathname);if(raw.charAt(0)===\"/\"&&raw.charAt(1)!==\"/\")return u.pathname+u.search+u.hash;return u.href}function uc(v){if(typeof v!==\"object\"||v===null)return undefined;var n=v.result;if(typeof n===\"object\"&&n!==null){var e=n.error;if(typeof e===\"object\"&&e!==null&&typeof e.code===\"string\")return e.code}var t=v.error;if(typeof t===\"object\"&&t!==null&&typeof t.code===\"string\")return t.code;return undefined}var seat={onUnpaired:null,onPaired:null,pendingUnpaired:false,restore:function(){}};function denied(res){if(res.status!==403)return false;return res.clone().json().then(function(b){return uc(b)===\"unpaired\"}).catch(function(){return false})}function signal(unpaired){if(unpaired){if(seat.onUnpaired)seat.onUnpaired();else seat.pendingUnpaired=true}else{seat.pendingUnpaired=false;if(seat.onPaired)seat.onPaired()}}var of=w.fetch;w.fetch=function(input,init){var raw=typeof input===\"string\"||input instanceof URL?input.toString():input.url;var url=new URL(raw,loc.href);if(so(url)&&sf(url.pathname)){var next=new URL(url);next.pathname=rp(url.pathname);var target=typeof input===\"string\"||input instanceof URL?next.toString():new Request(next,input);return Promise.resolve(of.call(w,target,att(init))).then(function(res){void Promise.resolve(denied(res)).then(signal);return res})}return of.call(w,input,init)};var OW=w.WebSocket;w.WebSocket=function(url,protocols){var p=new URL(url.toString(),loc.href);var o=p.protocol===\"wss:\"?\"https://\"+p.host:p.protocol===\"ws:\"?\"http://\"+p.host:\"\";if(o!==\"\"&&o===loc.origin&&sw(p.pathname)){var nx=new URL(p);nx.pathname=rp(p.pathname);var dvv=rdv();if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);return protocols!==undefined?new OW(nx,protocols):new OW(nx)}return protocols!==undefined?new OW(url,protocols):new OW(url)};w.WebSocket.prototype=OW.prototype;w.WebSocket.CONNECTING=OW.CONNECTING;w.WebSocket.OPEN=OW.OPEN;w.WebSocket.CLOSING=OW.CLOSING;w.WebSocket.CLOSED=OW.CLOSED;var OE=w.EventSource;if(OE!==undefined){w.EventSource=function(url,cfg){var p=new URL(url.toString(),loc.href);if(so(p)&&sf(p.pathname)){var nx=new URL(p);nx.pathname=rp(p.pathname);var dvv=rdv();if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);return new OE(nx,cfg)}return new OE(url,cfg)};w.EventSource.prototype=OE.prototype}var restores=[];function patchSrc(C){if(C===undefined)return;var d=Object.getOwnPropertyDescriptor(C.prototype,\"src\");if(d===undefined||d.configurable===false||d.set===undefined)return;var os=d.set;Object.defineProperty(C.prototype,\"src\",{configurable:true,enumerable:d.enumerable!==false,get:d.get,set:function(v){os.call(this,rr(String(v)))}});restores.push(function(){Object.defineProperty(C.prototype,\"src\",d)})}patchSrc(w.HTMLImageElement);patchSrc(w.HTMLScriptElement);patchSrc(w.HTMLIFrameElement);function uf(u,init){var raw=typeof u===\"string\"?u:u.href;if(typeof raw!==\"string\")return of.call(w,u,init||{});var q=new URL(raw,loc.href);if(so(q)&&q.pathname===R.uploadPath)return w.fetch(raw,init||{});return of.call(w,u,init||{})}try{if(w[R.uploadHookGlobal]===undefined)w[R.uploadHookGlobal]={fetch:uf}}catch(e){}seat.restore=function(){w.fetch=of;w.WebSocket=OW;if(OE!==undefined)w.EventSource=OE;for(var i=0;i<restores.length;i++)restores[i]();try{delete w[" + seat + "]}catch(e){w[" + seat + "]=undefined}};w[" + seat + "]=seat" + buildBootWatchdogScript() + "}catch(e){}})();";
|
|
4359
4461
|
}
|
|
4360
4462
|
/** Boot-watchdog latch: one self-reload per session while the boot is broken. */
|
|
4361
4463
|
const BOOT_WATCHDOG_KEY = "dsh-remote-boot-reload";
|
|
@@ -122,7 +122,7 @@ export function RemoteEntry({ wide, t }) {
|
|
|
122
122
|
// it there would just start a doomed reconnect loop.
|
|
123
123
|
if (next.kind !== 'ready' && next.kind !== 'lan-required')
|
|
124
124
|
return;
|
|
125
|
-
const source = new EventSource('
|
|
125
|
+
const source = new EventSource('api/pair/events');
|
|
126
126
|
eventSource.current = source;
|
|
127
127
|
source.onmessage = (event) => {
|
|
128
128
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnE,OAAO,KAAK,EAAE,UAAU,EAAe,MAAM,4CAA4C,CAAA;AAezF,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnE,OAAO,KAAK,EAAE,UAAU,EAAe,MAAM,4CAA4C,CAAA;AAezF,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAqBrD,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACrE,YAAY,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AACnE,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAC7C,YAAY,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAC/F,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACzD,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAErE,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,0CAA0C;QAC1C,MAAM,EAAE,SAAS,CAAA;KAClB;IAED,UAAU,OAAO;QACf;;;;WAIG;QACH;;;;;WAKG;QACH,oBAAoB,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,4BAA4B,CAAA;SAAE,CAAA;KAC3F;CACF;AAED,4GAA4G;AAC5G,MAAM,WAAW,uBAAuB;IACtC,oEAAoE;IACpE,IAAI,EAAE,OAAO,CAAA;CACd;AAED,mEAAmE;AACnE,MAAM,WAAW,4BAA4B;IAC3C,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,KAAK,CAAA;CACjB;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;;;;;;WASG;QACH,aAAa,CAAC,EAAE;YACd,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE;gBAAE,SAAS,EAAE,MAAM,CAAC;gBAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,CAAC,GAAG,SAAS,CAAA;aAAE,GAAG,UAAU,CAAC,CAAC,CAAC,CAAA;SAClG,CAAA;KACF;CACF;AAyBD,wCAAwC;AACxC,eAAO,MAAM,MAAM,UAA6D,CAAA;AAEhF;;;GAGG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CA2R9C"}
|
|
@@ -15,7 +15,8 @@ import { RemoteSettingsCard, RemoteSettingsCardController } from "./RemoteSettin
|
|
|
15
15
|
import { en, zh } from "./locales.js";
|
|
16
16
|
import { PAIR_FAILED_MARKER, runPairBootFlow } from "./deep-link.js";
|
|
17
17
|
import { readPairGatePolicy, sendHeartbeat, shouldStopHeartbeat } from "./pair-api.js";
|
|
18
|
-
import { channelTransition, installRemoteChannel,
|
|
18
|
+
import { channelTransition, installRemoteChannel, pageOriginFacts, remoteChannelRequired, REMOTE_CHANNEL_BOOT_GLOBAL, } from "./remote-channel.js";
|
|
19
|
+
import { isLocalPage } from "../remote-channel-rules.js";
|
|
19
20
|
import { FenceNotice } from "./FenceNotice.js";
|
|
20
21
|
import { reportDailyHeartbeat } from "./telemetry.js";
|
|
21
22
|
import { startMobileAdapt } from "./mobile-adapt.js";
|
|
@@ -246,7 +247,7 @@ export function apply(ctx) {
|
|
|
246
247
|
}
|
|
247
248
|
showFenceNotice();
|
|
248
249
|
};
|
|
249
|
-
const channelActive = () => remoteChannelRequired(window
|
|
250
|
+
const channelActive = () => remoteChannelRequired(pageOriginFacts(window), settingsForm.getSnapshot(), hostPairingPolicy);
|
|
250
251
|
// The parse-time boot patch (issue #987), when the served index carried
|
|
251
252
|
// it: already installed before any boot entry ran, so adopting its seat
|
|
252
253
|
// beats patching a second time (which would double-rewrite onto
|
|
@@ -299,7 +300,8 @@ export function apply(ctx) {
|
|
|
299
300
|
};
|
|
300
301
|
settingsForm.subscribe(syncChannel);
|
|
301
302
|
syncChannel();
|
|
302
|
-
|
|
303
|
+
const pageOrigin = pageOriginFacts(window);
|
|
304
|
+
if (!isLocalPage(pageOrigin.hostname, pageOrigin.protocol, pageOrigin.transportOwnsHost) && settingsForm.getSnapshot().status !== 'ready') {
|
|
303
305
|
void readPairGatePolicy().then((policy) => {
|
|
304
306
|
hostPairingPolicy = policy.requirePairingForLan;
|
|
305
307
|
syncChannel();
|
|
@@ -14,7 +14,7 @@ export const zh = {
|
|
|
14
14
|
'status.disconnected': '已配对设备离线',
|
|
15
15
|
'status.stopped': '已停止远程访问',
|
|
16
16
|
'status.lanRequired': '此功能需要局域网绑定或公网地址才能使用',
|
|
17
|
-
'status.lanRequiredHint': '当前服务仅绑定在 127.0.0.1
|
|
17
|
+
'status.lanRequiredHint': '当前服务仅绑定在 127.0.0.1 且未配置公网地址,手机无法访问。请在本插件的设置卡片中打开“局域网访问”(装在全家桶里时在「设置 → Web 插件 → 远程访问设置」,单独安装时在设置 → 插件 → 本插件行),或用 dsh web --host 0.0.0.0 重新启动,或填写内网穿透的公网地址。',
|
|
18
18
|
'status.loopbackRequired': '配对面板仅限本机使用',
|
|
19
19
|
'status.loopbackRequiredHint': '请通过 http://127.0.0.1 打开此页面后重试;手机请使用配对链接访问。',
|
|
20
20
|
'status.unreachable': '无法连接配对服务',
|
|
@@ -180,7 +180,7 @@ export const en = {
|
|
|
180
180
|
'status.disconnected': 'Paired devices offline',
|
|
181
181
|
'status.stopped': 'Remote access stopped',
|
|
182
182
|
'status.lanRequired': 'This feature needs a LAN bind or a public address',
|
|
183
|
-
'status.lanRequiredHint': 'The server is bound to 127.0.0.1 and no public address is configured, so a phone cannot reach it. Turn on LAN access in
|
|
183
|
+
'status.lanRequiredHint': 'The server is bound to 127.0.0.1 and no public address is configured, so a phone cannot reach it. Turn on LAN access in this plugin settings card (under Settings → Web Plugins → Remote access when the full family is installed, or on this plugin row under Settings → Plugins for a standalone install), or restart with dsh web --host 0.0.0.0, or set the tunneled public address.',
|
|
184
184
|
'status.loopbackRequired': 'The pairing panel works on this machine only',
|
|
185
185
|
'status.loopbackRequiredHint': 'Open this page at http://127.0.0.1 to mint a QR code; phones use the paired link.',
|
|
186
186
|
'status.unreachable': 'Cannot reach the pairing service',
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* Browser-side wire helpers for the /api/pair surface. Plain fetch over
|
|
3
3
|
* same-origin /api (like the connection client); JSON bodies, string
|
|
4
4
|
* responses for the error codes.
|
|
5
|
+
*
|
|
6
|
+
* The paths are DOCUMENT-RELATIVE (issue #1707): the GUI is served with
|
|
7
|
+
* `<base href="./">`, so a sub-path deployment resolves them against its entry
|
|
8
|
+
* directory instead of escaping to the origin root.
|
|
5
9
|
*/
|
|
6
10
|
/** issue() response. */
|
|
7
11
|
export interface IssueResult {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pair-api.d.ts","sourceRoot":"","sources":["../../../src/client/pair-api.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"pair-api.d.ts","sourceRoot":"","sources":["../../../src/client/pair-api.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
* Browser-side wire helpers for the /api/pair surface. Plain fetch over
|
|
3
3
|
* same-origin /api (like the connection client); JSON bodies, string
|
|
4
4
|
* responses for the error codes.
|
|
5
|
+
*
|
|
6
|
+
* The paths are DOCUMENT-RELATIVE (issue #1707): the GUI is served with
|
|
7
|
+
* `<base href="./">`, so a sub-path deployment resolves them against its entry
|
|
8
|
+
* directory instead of escaping to the origin root.
|
|
5
9
|
*/
|
|
6
10
|
/** Read the host-authoritative desktop pairing policy. */
|
|
7
11
|
export async function readPairGatePolicy() {
|
|
8
|
-
const response = await fetch('
|
|
12
|
+
const response = await fetch('api/pair/status');
|
|
9
13
|
if (!response.ok)
|
|
10
14
|
throw new Error(`remote-web-ui: status failed with ${String(response.status)}`);
|
|
11
15
|
const value = await response.json();
|
|
@@ -25,7 +29,7 @@ export async function readPairGatePolicy() {
|
|
|
25
29
|
* origin — the panel is a desktop control endpoint).
|
|
26
30
|
*/
|
|
27
31
|
export async function issuePair(address) {
|
|
28
|
-
const response = await fetch('
|
|
32
|
+
const response = await fetch('api/pair/issue', {
|
|
29
33
|
method: 'POST',
|
|
30
34
|
headers: { 'content-type': 'application/json' },
|
|
31
35
|
body: JSON.stringify({
|
|
@@ -50,7 +54,7 @@ export async function issuePair(address) {
|
|
|
50
54
|
* @returns the wire result.
|
|
51
55
|
*/
|
|
52
56
|
export async function acceptPair(token) {
|
|
53
|
-
const response = await fetch('
|
|
57
|
+
const response = await fetch('api/pair/accept', {
|
|
54
58
|
method: 'POST',
|
|
55
59
|
headers: { 'content-type': 'application/json' },
|
|
56
60
|
body: JSON.stringify({ token }),
|
|
@@ -65,7 +69,7 @@ export async function acceptPair(token) {
|
|
|
65
69
|
}
|
|
66
70
|
/** Revoke mobile access (paired devices + the current token). */
|
|
67
71
|
export async function stopPair() {
|
|
68
|
-
const response = await fetch('
|
|
72
|
+
const response = await fetch('api/pair/stop', { method: 'POST' });
|
|
69
73
|
if (!response.ok)
|
|
70
74
|
throw new Error(`remote-web-ui: stop failed with ${String(response.status)}`);
|
|
71
75
|
}
|
|
@@ -74,7 +78,7 @@ export async function stopPair() {
|
|
|
74
78
|
* @param deviceId - the session id of the row to drop.
|
|
75
79
|
*/
|
|
76
80
|
export async function revokePair(deviceId) {
|
|
77
|
-
const response = await fetch('
|
|
81
|
+
const response = await fetch('api/pair/revoke', {
|
|
78
82
|
method: 'POST',
|
|
79
83
|
headers: { 'content-type': 'application/json' },
|
|
80
84
|
body: JSON.stringify({ deviceId }),
|
|
@@ -90,7 +94,7 @@ export async function revokePair(deviceId) {
|
|
|
90
94
|
* proves this page is not paired (see {@link shouldStopHeartbeat}).
|
|
91
95
|
*/
|
|
92
96
|
export async function sendHeartbeat() {
|
|
93
|
-
const response = await fetch('
|
|
97
|
+
const response = await fetch('api/pair/heartbeat', { method: 'POST' });
|
|
94
98
|
return response.status;
|
|
95
99
|
}
|
|
96
100
|
/**
|
|
@@ -135,7 +139,7 @@ export class LanBindStatusError extends Error {
|
|
|
135
139
|
}
|
|
136
140
|
/** Read the LAN-bind facts (loopback-only endpoint). */
|
|
137
141
|
export async function readLanBindStatus() {
|
|
138
|
-
const response = await fetch('
|
|
142
|
+
const response = await fetch('api/pair/lan-bind');
|
|
139
143
|
if (!response.ok)
|
|
140
144
|
throw new LanBindStatusError(response.status, `remote-web-ui: lan-bind status failed with ${String(response.status)}`);
|
|
141
145
|
return await response.json();
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* Pure helpers are exported for unit tests; `installRemoteChannel` patches
|
|
22
22
|
* the given window and returns their restore.
|
|
23
23
|
*/
|
|
24
|
-
import { REMOTE_API_PREFIX, REMOTE_PREFIX } from '../remote-channel-rules.ts';
|
|
24
|
+
import { isLoopbackHostname as isLoopbackName, REMOTE_API_PREFIX, REMOTE_PREFIX } from '../remote-channel-rules.ts';
|
|
25
25
|
export { REMOTE_API_PREFIX, REMOTE_PREFIX };
|
|
26
26
|
export type { RemoteChannelBootSeat } from '../remote-channel-rules.ts';
|
|
27
27
|
export { REMOTE_CHANNEL_BOOT_GLOBAL } from '../remote-channel-rules.ts';
|
|
@@ -33,15 +33,35 @@ export interface RemoteChannelSettingsSnapshot {
|
|
|
33
33
|
requirePairingForLan?: boolean;
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Facts about the live page that decide whether it is local. Defaults come
|
|
38
|
+
* from `window` so ordinary call sites stay argument-free; tests pass them
|
|
39
|
+
* explicitly.
|
|
40
|
+
*/
|
|
41
|
+
export interface PageOriginFacts {
|
|
42
|
+
hostname: string;
|
|
43
|
+
protocol?: string;
|
|
44
|
+
transportOwnsHost?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Read the page-origin facts from a window-like object: the hostname and
|
|
48
|
+
* protocol of the page itself plus the official transport's host-owner flag
|
|
49
|
+
* (`__DSH_TRANSPORT__.ownsHost`, set by the desktop shell before any boot
|
|
50
|
+
* entry, and by this plugin's own paired landing).
|
|
51
|
+
* @param win - the window to read (defaults to the real one).
|
|
52
|
+
* @returns the facts `isLocalPage`/`remoteChannelRequired` decide from.
|
|
53
|
+
*/
|
|
54
|
+
export declare function pageOriginFacts(win?: Pick<Window, 'location'> & Partial<Pick<Window, 'navigator'>>): PageOriginFacts;
|
|
36
55
|
/** Decide whether a remote desktop channel is required from local or host policy. */
|
|
37
|
-
export declare function remoteChannelRequired(
|
|
56
|
+
export declare function remoteChannelRequired(origin: PageOriginFacts, snapshot: RemoteChannelSettingsSnapshot, hostPairingPolicy: boolean | undefined): boolean;
|
|
38
57
|
/**
|
|
39
|
-
* Browser-safe loopback classification for the page origin
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
58
|
+
* Browser-safe loopback classification for the page origin. The definition
|
|
59
|
+
* lives once in the rules module, which the inlined boot script mirrors
|
|
60
|
+
* verbatim, so the browser half and the parse-time patch can never disagree
|
|
61
|
+
* (an IPv6-loopback origin judged remote would rewrite every call onto a
|
|
62
|
+
* channel it can never pair on).
|
|
43
63
|
*/
|
|
44
|
-
export declare
|
|
64
|
+
export declare const isLoopbackHostname: typeof isLoopbackName;
|
|
45
65
|
/**
|
|
46
66
|
* Whether one same-origin path must ride the gated channel (fetch, EventSource,
|
|
47
67
|
* img/script/iframe src).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-channel.d.ts","sourceRoot":"","sources":["../../../src/client/remote-channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EACL,iBAAiB,EAEjB,aAAa,EACd,MAAM,4BAA4B,CAAA;AAEnC,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,CAAA;AAC3C,YAAY,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AACvE,OAAO,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAA;AAIvE,qEAAqE;AACrE,MAAM,WAAW,6BAA6B;IAC5C,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,aAAa,GAAG,MAAM,CAAA;IACpD,KAAK,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,oBAAoB,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAC9D;AAED,qFAAqF;AACrF,wBAAgB,qBAAqB,CACnC,
|
|
1
|
+
{"version":3,"file":"remote-channel.d.ts","sourceRoot":"","sources":["../../../src/client/remote-channel.ts"],"names":[],"mappings":"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"}
|
|
@@ -21,13 +21,29 @@
|
|
|
21
21
|
* Pure helpers are exported for unit tests; `installRemoteChannel` patches
|
|
22
22
|
* the given window and returns their restore.
|
|
23
23
|
*/
|
|
24
|
-
import { REMOTE_API_PREFIX, REMOTE_CHANNEL_RULES, REMOTE_PREFIX, } from "../remote-channel-rules.js";
|
|
24
|
+
import { isLoopbackHostname as isLoopbackName, isLocalPage, REMOTE_API_PREFIX, REMOTE_CHANNEL_RULES, REMOTE_PREFIX, } from "../remote-channel-rules.js";
|
|
25
25
|
export { REMOTE_API_PREFIX, REMOTE_PREFIX };
|
|
26
26
|
export { REMOTE_CHANNEL_BOOT_GLOBAL } from "../remote-channel-rules.js";
|
|
27
27
|
const RULES = REMOTE_CHANNEL_RULES;
|
|
28
|
+
/**
|
|
29
|
+
* Read the page-origin facts from a window-like object: the hostname and
|
|
30
|
+
* protocol of the page itself plus the official transport's host-owner flag
|
|
31
|
+
* (`__DSH_TRANSPORT__.ownsHost`, set by the desktop shell before any boot
|
|
32
|
+
* entry, and by this plugin's own paired landing).
|
|
33
|
+
* @param win - the window to read (defaults to the real one).
|
|
34
|
+
* @returns the facts `isLocalPage`/`remoteChannelRequired` decide from.
|
|
35
|
+
*/
|
|
36
|
+
export function pageOriginFacts(win = window) {
|
|
37
|
+
const transport = win.__DSH_TRANSPORT__;
|
|
38
|
+
return {
|
|
39
|
+
hostname: win.location.hostname,
|
|
40
|
+
protocol: win.location.protocol,
|
|
41
|
+
...(transport?.ownsHost === true ? { transportOwnsHost: true } : {}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
28
44
|
/** Decide whether a remote desktop channel is required from local or host policy. */
|
|
29
|
-
export function remoteChannelRequired(
|
|
30
|
-
if (
|
|
45
|
+
export function remoteChannelRequired(origin, snapshot, hostPairingPolicy) {
|
|
46
|
+
if (isLocalPage(origin.hostname, origin.protocol, origin.transportOwnsHost))
|
|
31
47
|
return false;
|
|
32
48
|
if (snapshot.status === 'ready') {
|
|
33
49
|
return (snapshot.value?.enabled ?? true) && (snapshot.value?.requirePairingForLan ?? true);
|
|
@@ -37,21 +53,13 @@ export function remoteChannelRequired(hostname, snapshot, hostPairingPolicy) {
|
|
|
37
53
|
return hostPairingPolicy !== false;
|
|
38
54
|
}
|
|
39
55
|
/**
|
|
40
|
-
* Browser-safe loopback classification for the page origin
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
56
|
+
* Browser-safe loopback classification for the page origin. The definition
|
|
57
|
+
* lives once in the rules module, which the inlined boot script mirrors
|
|
58
|
+
* verbatim, so the browser half and the parse-time patch can never disagree
|
|
59
|
+
* (an IPv6-loopback origin judged remote would rewrite every call onto a
|
|
60
|
+
* channel it can never pair on).
|
|
44
61
|
*/
|
|
45
|
-
export
|
|
46
|
-
// WHATWG location.hostname keeps IPv6 literals bracketed ("[::1]"); the
|
|
47
|
-
// host-side shared predicate accepts that spelling, so this browser copy must
|
|
48
|
-
// too, or an IPv6-loopback origin is judged remote and rewrites every call
|
|
49
|
-
// onto the gated channel where it can never pair.
|
|
50
|
-
if (hostname === 'localhost' || hostname === '::1' || hostname === '[::1]')
|
|
51
|
-
return true;
|
|
52
|
-
const parts = hostname.split('.');
|
|
53
|
-
return parts.length === 4 && parts[0] === '127' && parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255);
|
|
54
|
-
}
|
|
62
|
+
export const isLoopbackHostname = isLoopbackName;
|
|
55
63
|
/**
|
|
56
64
|
* Whether one same-origin path must ride the gated channel (fetch, EventSource,
|
|
57
65
|
* img/script/iframe src).
|
|
@@ -24,7 +24,7 @@ export class UpdateStatusError extends Error {
|
|
|
24
24
|
export async function fetchUpdateStatus() {
|
|
25
25
|
let response;
|
|
26
26
|
try {
|
|
27
|
-
response = await fetch('
|
|
27
|
+
response = await fetch('api/update/status');
|
|
28
28
|
}
|
|
29
29
|
catch {
|
|
30
30
|
// Network-level failure: nothing came back at all.
|
|
@@ -40,7 +40,7 @@ export async function fetchUpdateStatus() {
|
|
|
40
40
|
* @returns the run outcome.
|
|
41
41
|
*/
|
|
42
42
|
export async function runUpdate() {
|
|
43
|
-
const response = await fetch('
|
|
43
|
+
const response = await fetch('api/update/run', { method: 'POST' });
|
|
44
44
|
if (!response.ok)
|
|
45
45
|
throw new Error('update run unavailable');
|
|
46
46
|
return await response.json();
|
package/lib/types/lan-bind.d.ts
CHANGED
|
@@ -64,6 +64,10 @@ export declare function lanBindState(profile: string, home?: string): {
|
|
|
64
64
|
* unterminated block (BEGIN marker without END, which stripManagedBlock
|
|
65
65
|
* cannot match) is truncated at its BEGIN marker first so the rewrite can
|
|
66
66
|
* never stack a second webserver row onto the orphan.
|
|
67
|
+
*
|
|
68
|
+
* The written file is always exactly one valid YAML document: a base the
|
|
69
|
+
* plugin manager left in flow style is re-emitted as a block sequence before
|
|
70
|
+
* the managed block is appended (see mergePatchContent).
|
|
67
71
|
*/
|
|
68
72
|
export declare function writeLanBind(host: LanBindHost, port: number, profile: string, home?: string): void;
|
|
69
73
|
//# sourceMappingURL=lan-bind.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lan-bind.d.ts","sourceRoot":"","sources":["../../src/lan-bind.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;
|
|
1
|
+
{"version":3,"file":"lan-bind.d.ts","sourceRoot":"","sources":["../../src/lan-bind.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAUH,eAAO,MAAM,oBAAoB,mEAAmE,CAAA;AACpG,eAAO,MAAM,kBAAkB,+CAA+C,CAAA;AAE9E,iDAAiD;AACjD,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,WAAW,CAAA;AAEjD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAkB,GAAG,MAAM,CAUlF;AAYD,qEAAqE;AACrE,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKzD;AA0DD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAcpE;AAED,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,WAAW,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;IACjC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;CACzB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAW3E;AAED,0EAA0E;AAC1E,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAkB,GAAG;IAAE,YAAY,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAI/H;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAkB,GAAG,IAAI,CAa7G"}
|