@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.
Files changed (46) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +1 -1
  3. package/README.zh.md +1 -1
  4. package/lib/client.js +132 -29
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +117 -15
  7. package/lib/types/client/RemoteEntry.js +1 -1
  8. package/lib/types/client/index.d.ts.map +1 -1
  9. package/lib/types/client/index.js +5 -3
  10. package/lib/types/client/locales.js +2 -2
  11. package/lib/types/client/pair-api.d.ts +4 -0
  12. package/lib/types/client/pair-api.d.ts.map +1 -1
  13. package/lib/types/client/pair-api.js +11 -7
  14. package/lib/types/client/remote-channel.d.ts +27 -7
  15. package/lib/types/client/remote-channel.d.ts.map +1 -1
  16. package/lib/types/client/remote-channel.js +25 -17
  17. package/lib/types/client/update-api.js +2 -2
  18. package/lib/types/lan-bind.d.ts +4 -0
  19. package/lib/types/lan-bind.d.ts.map +1 -1
  20. package/lib/types/lan-bind.js +64 -1
  21. package/lib/types/pairing.d.ts +15 -2
  22. package/lib/types/pairing.d.ts.map +1 -1
  23. package/lib/types/pairing.js +26 -14
  24. package/lib/types/pairing.persist.test.js +43 -3
  25. package/lib/types/remote-channel-boot.d.ts.map +1 -1
  26. package/lib/types/remote-channel-boot.js +13 -3
  27. package/lib/types/remote-channel-rules.d.ts +74 -0
  28. package/lib/types/remote-channel-rules.d.ts.map +1 -1
  29. package/lib/types/remote-channel-rules.js +104 -0
  30. package/lib/types/update.d.ts +10 -0
  31. package/lib/types/update.d.ts.map +1 -1
  32. package/lib/types/update.js +32 -3
  33. package/package.json +20 -19
  34. package/src/client/RemoteEntry.tsx +1 -1
  35. package/src/client/index.ts +5 -3
  36. package/src/client/locales.ts +2 -2
  37. package/src/client/pair-api.ts +11 -7
  38. package/src/client/remote-channel.ts +38 -15
  39. package/src/client/remote.module.css +18 -0
  40. package/src/client/update-api.ts +2 -2
  41. package/src/lan-bind.ts +62 -1
  42. package/src/pairing.persist.test.ts +47 -3
  43. package/src/pairing.ts +26 -12
  44. package/src/remote-channel-boot.ts +13 -3
  45. package/src/remote-channel-rules.ts +113 -0
  46. 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
- if (this.devices.size >= this.config.maxDevices) {
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 !== void 0) this.devices.delete(oldest.id);
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 base = (orphanBegin === -1 ? stripped : stripped.slice(0, orphanBegin)).trimEnd().replace(/\[\s*\]\s*$/, "").trimEnd();
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 Promise.all(names.map((name) => deps.fetchLatest(name)));
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 R=" + json + ";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){}})();";
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('/api/pair/events');
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;AAoBrD,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,CA0R9C"}
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, isLoopbackHostname, remoteChannelRequired, REMOTE_CHANNEL_BOOT_GLOBAL, } from "./remote-channel.js";
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.location.hostname, settingsForm.getSnapshot(), hostPairingPolicy);
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
- if (!isLoopbackHostname(window.location.hostname) && settingsForm.getSnapshot().status !== 'ready') {
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 且未配置公网地址,手机无法访问。请在「设置 → Web 插件 → 远程访问设置」卡片中打开“局域网访问”,或用 dsh web --host 0.0.0.0 重新启动,或填写内网穿透的公网地址。',
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 the settings card under Settings → Web Plugins → Remote access, or restart with dsh web --host 0.0.0.0, or set the tunneled 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 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":"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"}
1
+ {"version":3,"file":"pair-api.d.ts","sourceRoot":"","sources":["../../../src/client/pair-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,wBAAwB;AACxB,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,IAAI,CAAA;IACR,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,8DAA8D;IAC9D,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,KAAK,CAAA;IACT,IAAI,EAAE,cAAc,CAAA;CACrB;AAED,uEAAuE;AACvE,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,KAAK,CAAA;IACT,IAAI,EAAE,iBAAiB,CAAA;CACxB;AAED,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,KAAK,CAAA;IACT,IAAI,EAAE,WAAW,CAAA;CAClB;AAED,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,gBAAgB,GAAG,mBAAmB,GAAG,qBAAqB,CAAA;AAExG,oEAAoE;AACpE,MAAM,WAAW,cAAc;IAC7B,oBAAoB,EAAE,OAAO,CAAA;CAC9B;AAED,0DAA0D;AAC1D,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,cAAc,CAAC,CAQlE;AAED,8BAA8B;AAC9B,MAAM,MAAM,aAAa,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,SAAS,GAAG,WAAW,CAAA;CAAE,CAAA;AAExE,sEAAsE;AACtE,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAA;IACxC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,+EAA+E;AAC/E,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,KAAK,GAAG,aAAa,GAAG,SAAS,GAAG,QAAQ,CAAA;IACnD,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,oEAAoE;AACpE,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,EAAE,CAAA;CAC5C;AAED,kCAAkC;AAClC,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,CAAA;IACb,KAAK,EAAE,cAAc,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,cAAc,CAAA;IAC5E,YAAY,EAAE,OAAO,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,gDAAgD;IAChD,OAAO,CAAC,EAAE,WAAW,EAAE,CAAA;IACvB,mEAAmE;IACnE,MAAM,CAAC,EAAE,iBAAiB,CAAA;IAC1B,uEAAuE;IACvE,KAAK,CAAC,EAAE,gBAAgB,CAAA;IACxB,mEAAmE;IACnE,OAAO,CAAC,EAAE,YAAY,CAAA;CACvB;AAED,iEAAiE;AACjE,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,OAAO,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAexE;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG,aAAa,CAAC,CAWrF;AAED,iEAAiE;AACjE,wBAAsB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAG9C;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQhE;AAED;;;;GAIG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,CAGrD;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED,4DAA4D;AAC5D,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAIhE;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI/C;AAED,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,wEAAwE;IACxE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,+DAA+D;IAC/D,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,QAAQ,EAAE;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1D,QAAQ,EAAE,MAAM,CAAA;IAChB,wEAAwE;IACxE,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED,iFAAiF;AACjF,qBAAa,kBAAmB,SAAQ,KAAK;aAMf,MAAM,EAAE,MAAM,GAAG,SAAS;IALtD;;;;OAIG;gBACyB,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM;CAIxE;AAED,wDAAwD;AACxD,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,YAAY,CAAC,CAI/D;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEzE;AAED,iDAAiD;AACjD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKnD;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMtD;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAsB7D"}
@@ -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('/api/pair/status');
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('/api/pair/issue', {
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('/api/pair/accept', {
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('/api/pair/stop', { method: 'POST' });
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('/api/pair/revoke', {
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('/api/pair/heartbeat', { method: 'POST' });
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('/api/pair/lan-bind');
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(hostname: string, snapshot: RemoteChannelSettingsSnapshot, hostPairingPolicy: boolean | undefined): boolean;
56
+ export declare function remoteChannelRequired(origin: PageOriginFacts, snapshot: RemoteChannelSettingsSnapshot, hostPairingPolicy: boolean | undefined): boolean;
38
57
  /**
39
- * Browser-safe loopback classification for the page origin (the SDK client
40
- * exports its own; this copy keeps the module dependency-free).
41
- * @param hostname - a location hostname (IPv6 without brackets).
42
- * @returns true for localhost, IPv6 loopback, or any 127/8 literal.
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 function isLoopbackHostname(hostname: string): boolean;
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":"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"}
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(hostname, snapshot, hostPairingPolicy) {
30
- if (isLoopbackHostname(hostname))
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 (the SDK client
41
- * exports its own; this copy keeps the module dependency-free).
42
- * @param hostname - a location hostname (IPv6 without brackets).
43
- * @returns true for localhost, IPv6 loopback, or any 127/8 literal.
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 function isLoopbackHostname(hostname) {
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('/api/update/status');
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('/api/update/run', { method: 'POST' });
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();
@@ -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;AASH,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;AAED;;;;;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;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAkB,GAAG,IAAI,CAa7G"}
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"}