@linxin666/dsh-remote-web-ui 0.2.9 → 0.3.0

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 (83) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +41 -8
  3. package/README.zh.md +7 -2
  4. package/lib/client.js +65 -34
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +645 -61
  7. package/lib/mobile.js +874 -64
  8. package/lib/mobile.js.map +1 -1
  9. package/lib/types/client/index.d.ts.map +1 -1
  10. package/lib/types/client/index.js +35 -5
  11. package/lib/types/client/remote-channel.d.ts +4 -5
  12. package/lib/types/client/remote-channel.d.ts.map +1 -1
  13. package/lib/types/client/remote-channel.js +13 -28
  14. package/lib/types/http.d.ts +37 -8
  15. package/lib/types/http.d.ts.map +1 -1
  16. package/lib/types/http.js +76 -11
  17. package/lib/types/index.d.ts.map +1 -1
  18. package/lib/types/index.js +25 -1
  19. package/lib/types/mobile/api.d.ts +57 -0
  20. package/lib/types/mobile/api.d.ts.map +1 -1
  21. package/lib/types/mobile/api.js +29 -0
  22. package/lib/types/mobile/markdown.d.ts +7 -1
  23. package/lib/types/mobile/markdown.d.ts.map +1 -1
  24. package/lib/types/mobile/markdown.js +16 -7
  25. package/lib/types/mobile/markdown.test.js +8 -0
  26. package/lib/types/mobile/mobile-styles.d.ts +1 -1
  27. package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
  28. package/lib/types/mobile/mobile-styles.js +325 -6
  29. package/lib/types/mobile/views/ChatView.d.ts +10 -0
  30. package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
  31. package/lib/types/mobile/views/ChatView.js +205 -8
  32. package/lib/types/mobile/views/ChatView.stream.test.d.ts +2 -0
  33. package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +1 -0
  34. package/lib/types/mobile/views/ChatView.stream.test.js +205 -0
  35. package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
  36. package/lib/types/mobile/views/WorkspaceView.js +46 -5
  37. package/lib/types/mobile/views/WorkspaceView.test.js +129 -2
  38. package/lib/types/mobile-api.d.ts +9 -3
  39. package/lib/types/mobile-api.d.ts.map +1 -1
  40. package/lib/types/mobile-api.js +61 -16
  41. package/lib/types/mobile-pending.d.ts +38 -0
  42. package/lib/types/mobile-pending.d.ts.map +1 -0
  43. package/lib/types/mobile-pending.js +59 -0
  44. package/lib/types/paired-model-catalog.d.ts +17 -0
  45. package/lib/types/paired-model-catalog.d.ts.map +1 -0
  46. package/lib/types/paired-model-catalog.js +252 -0
  47. package/lib/types/pairing.d.ts +13 -4
  48. package/lib/types/pairing.d.ts.map +1 -1
  49. package/lib/types/pairing.js +13 -5
  50. package/lib/types/remote-api.d.ts.map +1 -1
  51. package/lib/types/remote-channel-boot.d.ts +26 -0
  52. package/lib/types/remote-channel-boot.d.ts.map +1 -0
  53. package/lib/types/remote-channel-boot.js +128 -0
  54. package/lib/types/remote-channel-rules.d.ts +40 -0
  55. package/lib/types/remote-channel-rules.d.ts.map +1 -0
  56. package/lib/types/remote-channel-rules.js +32 -0
  57. package/lib/types/routes.d.ts.map +1 -1
  58. package/lib/types/routes.js +12 -23
  59. package/lib/types/uuid-polyfill.d.ts +10 -0
  60. package/lib/types/uuid-polyfill.d.ts.map +1 -0
  61. package/lib/types/uuid-polyfill.js +27 -0
  62. package/package.json +1 -1
  63. package/src/client/index.ts +41 -5
  64. package/src/client/remote-channel.ts +18 -28
  65. package/src/http.ts +87 -12
  66. package/src/index.ts +27 -2
  67. package/src/mobile/api.ts +91 -0
  68. package/src/mobile/markdown.test.ts +9 -0
  69. package/src/mobile/markdown.ts +17 -7
  70. package/src/mobile/mobile-styles.ts +326 -6
  71. package/src/mobile/views/ChatView.stream.test.tsx +231 -0
  72. package/src/mobile/views/ChatView.tsx +328 -10
  73. package/src/mobile/views/WorkspaceView.test.tsx +163 -2
  74. package/src/mobile/views/WorkspaceView.tsx +152 -26
  75. package/src/mobile-api.ts +59 -17
  76. package/src/mobile-pending.ts +88 -0
  77. package/src/paired-model-catalog.ts +224 -0
  78. package/src/pairing.ts +13 -5
  79. package/src/remote-api.ts +0 -1
  80. package/src/remote-channel-boot.ts +131 -0
  81. package/src/remote-channel-rules.ts +62 -0
  82. package/src/routes.ts +13 -24
  83. package/src/uuid-polyfill.ts +28 -0
package/lib/index.js CHANGED
@@ -24,7 +24,7 @@ import { spawn } from "node:child_process";
24
24
  * @module @deepseek-ai/dsh-settings/redact
25
25
  */
26
26
  /** Whether a value is a plain data object the walker may recurse into. */
27
- function isRecord(value) {
27
+ function isRecord$1(value) {
28
28
  return typeof value === "object" && value !== null && !Array.isArray(value);
29
29
  }
30
30
  function walk(node, value, path, secrets) {
@@ -39,7 +39,7 @@ function walk(node, value, path, secrets) {
39
39
  switch (node.type) {
40
40
  case "object": {
41
41
  const properties = node.dict ?? {};
42
- const source = isRecord(value) ? value : void 0;
42
+ const source = isRecord$1(value) ? value : void 0;
43
43
  const rebuilt = {};
44
44
  if (source !== void 0) for (const [key, entry] of Object.entries(source)) {
45
45
  if (key in properties) continue;
@@ -52,7 +52,7 @@ function walk(node, value, path, secrets) {
52
52
  return source === void 0 && Object.keys(rebuilt).length === 0 ? value : rebuilt;
53
53
  }
54
54
  case "dict": {
55
- if (!isRecord(value)) return value;
55
+ if (!isRecord$1(value)) return value;
56
56
  const rebuilt = {};
57
57
  for (const [key, entry] of Object.entries(value)) {
58
58
  const stripped = walk(node.inner, entry, [...path, key], secrets);
@@ -250,10 +250,13 @@ const defaultClock = {
250
250
  randomToken: () => randomBytes(16).toString("hex")
251
251
  };
252
252
  /**
253
- * The pairing state machine. All mutations notify state listeners after the
254
- * commit point that makes them true, and notification dedupes against the
255
- * last emitted snapshot — time-driven transitions (a device aging offline)
256
- * surface on the next sweep without any mutation.
253
+ * The pairing state machine. Structural mutations issue/accept/stop/revoke
254
+ * and config updates (LAN bases, tunnel, posture) notify state listeners
255
+ * after the commit point that makes them true, and notification dedupes
256
+ * against the last emitted snapshot. Presence-only updates (touchDevice /
257
+ * heartbeat) just mark the store dirty and broadcast on the next sweep,
258
+ * which also surfaces time-driven transitions (a device aging offline)
259
+ * without any mutation.
257
260
  */
258
261
  var PairingService = class {
259
262
  config;
@@ -491,6 +494,12 @@ var PairingService = class {
491
494
  * The api/gate path: record activity for a device id and report whether
492
495
  * the request may proceed. Unknown or revoked ids (including any device
493
496
  * after stop() or idle expiry) are refused.
497
+ *
498
+ * Presence refreshes are throttled on purpose: every gated request (and
499
+ * the mobile SSE keepalive) lands here, so a broadcast per call would fan
500
+ * a full snapshot out to every status stream on the hot path. The refresh
501
+ * only updates lastSeenAt and marks the store dirty; the snapshot reaches
502
+ * listeners at the next sweep() (structural changes notify immediately).
494
503
  * @param deviceId - the cookie value of the requesting device.
495
504
  * @returns true when the device session is live and was refreshed.
496
505
  */
@@ -499,7 +508,6 @@ var PairingService = class {
499
508
  if (session === void 0) return false;
500
509
  session.lastSeenAt = this.clock.now();
501
510
  this.dirty = true;
502
- this.notify();
503
511
  return true;
504
512
  }
505
513
  /** Explicit presence heartbeat (the phone's client sends these). */
@@ -793,18 +801,18 @@ var RemoteWebUiPairing = class extends Service {
793
801
  };
794
802
  //#endregion
795
803
  //#region src/http.ts
796
- /** One JSON response. */
797
- function writeJson(res, status, body) {
798
- const payload = JSON.stringify(body);
799
- res.writeHead(status, {
800
- "content-type": "application/json; charset=utf-8",
801
- "referrer-policy": "no-referrer"
802
- });
803
- res.end(payload);
804
- }
804
+ /** Default body cap for readJsonBody: 64 KiB. */
805
+ const DEFAULT_JSON_BODY_MAX_BYTES = 64 * 1024;
806
+ /** Family-default JSON response headers; callers may append or override. */
807
+ const JSON_HEADERS = {
808
+ "content-type": "application/json; charset=utf-8",
809
+ "referrer-policy": "no-referrer"
810
+ };
805
811
  /**
806
- * Read a request body up to maxBytes and parse it as JSON.
807
- * @throws 'body too large' beyond the cap, or the JSON.parse error.
812
+ * Strict bounded body reader: parse a request body of at most maxBytes as
813
+ * JSON.
814
+ * @throws 'body too large' past the cap, or the JSON.parse error for an
815
+ * invalid or empty payload.
808
816
  */
809
817
  async function readBoundedJson(req, maxBytes) {
810
818
  const chunks = [];
@@ -817,6 +825,54 @@ async function readBoundedJson(req, maxBytes) {
817
825
  }
818
826
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
819
827
  }
828
+ /**
829
+ * Lenient bounded body reader: parse a request body as JSON, or null on an
830
+ * empty body, invalid JSON, or a body past maxBytes (default 64 KiB).
831
+ * Overflow destroys the request instead of draining the remainder (no drain
832
+ * call, matching the current repo-wide behavior); callers must not keep
833
+ * reading the request afterwards. With objectOnly, non-JSON-object payloads
834
+ * also yield null.
835
+ */
836
+ async function readJsonBody(req, opts = {}) {
837
+ const maxBytes = opts.maxBytes ?? DEFAULT_JSON_BODY_MAX_BYTES;
838
+ const chunks = [];
839
+ let size = 0;
840
+ for await (const chunk of req) {
841
+ const buffer = chunk;
842
+ size += buffer.length;
843
+ if (size > maxBytes) {
844
+ req.destroy();
845
+ return null;
846
+ }
847
+ chunks.push(buffer);
848
+ }
849
+ const text = Buffer.concat(chunks).toString("utf8");
850
+ if (text === "") return null;
851
+ try {
852
+ const parsed = JSON.parse(text);
853
+ if (opts.objectOnly && !isJsonObject(parsed)) return null;
854
+ return parsed;
855
+ } catch {
856
+ return null;
857
+ }
858
+ }
859
+ /** Whether a value is a JSON object: typeof object, not null, not an array. */
860
+ function isJsonObject(value) {
861
+ return typeof value === "object" && value !== null && !Array.isArray(value);
862
+ }
863
+ /**
864
+ * Write one JSON response. Default headers are the family defaults
865
+ * (content-type and referrer-policy); caller headers are appended or
866
+ * override them.
867
+ */
868
+ function writeJson(res, status, body, headers = {}) {
869
+ const payload = JSON.stringify(body);
870
+ res.writeHead(status, {
871
+ ...JSON_HEADERS,
872
+ ...headers
873
+ });
874
+ res.end(payload);
875
+ }
820
876
  //#endregion
821
877
  //#region src/routes.ts
822
878
  /**
@@ -857,7 +913,7 @@ function isTrustedApiRequest(request, trustedHosts) {
857
913
  }
858
914
  }
859
915
  /** Cap on pairing request bodies (tokens and workspace ids are tiny). */
860
- const MAX_BODY_BYTES = 4096;
916
+ const MAX_BODY_BYTES$1 = 4096;
861
917
  /**
862
918
  * The host authority of a configured public base URL, e.g. `foo.trycloudflare.com`
863
919
  * from `https://foo.trycloudflare.com`. Undefined when the URL does not parse —
@@ -902,24 +958,16 @@ const acceptPayloadSchema = z$1.object({ token: z$1.string().default("") });
902
958
  const revokePayloadSchema = z$1.object({ deviceId: z$1.string().min(1) });
903
959
  const pairActionPayloadSchema = z$1.object({}).passthrough();
904
960
  /**
905
- * Parse a pair request body through schema. A missing/empty or non-object
906
- * body is treated as an empty object (the same way the previous manual reads
907
- * yielded absent fields), and a value that fails the schema returns
908
- * `undefined` so the caller can answer with the existing error shape.
961
+ * Parse a pair request body through schema. A missing/empty, unparseable
962
+ * or non-object body (shared readJsonBody with objectOnly yields null for
963
+ * all of them) is treated as an empty object — the desktop stop/heartbeat
964
+ * send no body — and a value that fails the schema returns `undefined` so
965
+ * the caller can answer with the existing error shape.
909
966
  */
910
967
  function parsePairPayload(schema, body) {
911
968
  const result = schema.safeParse(body ?? {});
912
969
  return result.success ? result.data : void 0;
913
970
  }
914
- /** Read a request body up to MAX_BODY_BYTES and parse it as JSON (undefined on failure). */
915
- async function readJsonBody$1(req) {
916
- try {
917
- const parsed = await readBoundedJson(req, MAX_BODY_BYTES);
918
- return typeof parsed === "object" && parsed !== null ? parsed : void 0;
919
- } catch {
920
- return;
921
- }
922
- }
923
971
  /** The SSE fan-out for desktop panel status. */
924
972
  var PairingEventsStream = class {
925
973
  streams = /* @__PURE__ */ new Set();
@@ -1023,7 +1071,10 @@ function makeRoutes(deps) {
1023
1071
  });
1024
1072
  return;
1025
1073
  }
1026
- const body = await readJsonBody$1(req);
1074
+ const body = await readJsonBody(req, {
1075
+ maxBytes: MAX_BODY_BYTES$1,
1076
+ objectOnly: true
1077
+ });
1027
1078
  const payload = parsePairPayload(issuePayloadSchema, body);
1028
1079
  if (payload === void 0) {
1029
1080
  writeJson(res, 400, {
@@ -1069,7 +1120,10 @@ function makeRoutes(deps) {
1069
1120
  });
1070
1121
  return;
1071
1122
  }
1072
- const body = await readJsonBody$1(req);
1123
+ const body = await readJsonBody(req, {
1124
+ maxBytes: MAX_BODY_BYTES$1,
1125
+ objectOnly: true
1126
+ });
1073
1127
  const payload = parsePairPayload(acceptPayloadSchema, body);
1074
1128
  if (payload === void 0) {
1075
1129
  writeJson(res, 400, {
@@ -1087,14 +1141,10 @@ function makeRoutes(deps) {
1087
1141
  });
1088
1142
  return;
1089
1143
  }
1090
- res.writeHead(200, {
1091
- "content-type": "application/json; charset=utf-8",
1092
- "set-cookie": [`${service.config.cookieName}=${result.deviceId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${String(COOKIE_MAX_AGE_SEC)}`]
1093
- });
1094
- res.end(JSON.stringify({
1144
+ writeJson(res, 200, {
1095
1145
  ok: true,
1096
1146
  deviceId: result.deviceId
1097
- }));
1147
+ }, { "set-cookie": [`${service.config.cookieName}=${result.deviceId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${String(COOKIE_MAX_AGE_SEC)}`] });
1098
1148
  };
1099
1149
  const handleStop = async (req, res) => {
1100
1150
  if (!requireMethod(req, res, "POST")) return;
@@ -1105,7 +1155,10 @@ function makeRoutes(deps) {
1105
1155
  });
1106
1156
  return;
1107
1157
  }
1108
- const body = await readJsonBody$1(req);
1158
+ const body = await readJsonBody(req, {
1159
+ maxBytes: MAX_BODY_BYTES$1,
1160
+ objectOnly: true
1161
+ });
1109
1162
  if (parsePairPayload(pairActionPayloadSchema, body) === void 0) {
1110
1163
  writeJson(res, 400, {
1111
1164
  ok: false,
@@ -1125,7 +1178,10 @@ function makeRoutes(deps) {
1125
1178
  });
1126
1179
  return;
1127
1180
  }
1128
- const body = await readJsonBody$1(req);
1181
+ const body = await readJsonBody(req, {
1182
+ maxBytes: MAX_BODY_BYTES$1,
1183
+ objectOnly: true
1184
+ });
1129
1185
  const payload = parsePairPayload(revokePayloadSchema, body);
1130
1186
  if (payload === void 0) {
1131
1187
  writeJson(res, 400, {
@@ -1152,7 +1208,10 @@ function makeRoutes(deps) {
1152
1208
  });
1153
1209
  return;
1154
1210
  }
1155
- const body = await readJsonBody$1(req);
1211
+ const body = await readJsonBody(req, {
1212
+ maxBytes: MAX_BODY_BYTES$1,
1213
+ objectOnly: true
1214
+ });
1156
1215
  if (parsePairPayload(pairActionPayloadSchema, body) === void 0) {
1157
1216
  writeJson(res, 400, {
1158
1217
  ok: false,
@@ -1487,6 +1546,8 @@ function makeMobileRoutes() {
1487
1546
  * device at a time.
1488
1547
  */
1489
1548
  const MOBILE_ALLOWLIST = /* @__PURE__ */ new Set([
1549
+ "host.listDirectory",
1550
+ "workspace.create",
1490
1551
  "workspace.list",
1491
1552
  "agentPreset.list",
1492
1553
  "session.create",
@@ -1504,6 +1565,8 @@ const MOBILE_ALLOWLIST = /* @__PURE__ */ new Set([
1504
1565
  * settings-domain write).
1505
1566
  */
1506
1567
  const MOBILE_PREFERENCES_METHOD = "mobile.preferences";
1568
+ const MOBILE_PENDING_METHOD = "mobile.pending";
1569
+ const MOBILE_RESPOND_METHOD = "mobile.respond";
1507
1570
  /** One session.list page (thin phones load incrementally). */
1508
1571
  const SESSION_PAGE_SIZE = 20;
1509
1572
  /** SSE keep-alive ping cadence for the live mux stream (single connection). */
@@ -1586,7 +1649,7 @@ function makeMobileApiRoutes(deps) {
1586
1649
  return;
1587
1650
  }
1588
1651
  const method = pathname.slice(MOBILE_API_METHOD_PREFIX.length);
1589
- const local = method === MOBILE_PREFERENCES_METHOD;
1652
+ const local = method === MOBILE_PREFERENCES_METHOD || method === MOBILE_PENDING_METHOD || method === MOBILE_RESPOND_METHOD;
1590
1653
  if (!MOBILE_ALLOWLIST.has(method) && !local) {
1591
1654
  writeJson(res, 403, {
1592
1655
  ok: false,
@@ -1599,7 +1662,7 @@ function makeMobileApiRoutes(deps) {
1599
1662
  }
1600
1663
  let envelope;
1601
1664
  try {
1602
- envelope = await readJsonBody(req);
1665
+ envelope = await readBoundedJson(req, 64 * 1024);
1603
1666
  } catch {
1604
1667
  writeJson(res, 400, {
1605
1668
  ok: false,
@@ -1623,7 +1686,7 @@ function makeMobileApiRoutes(deps) {
1623
1686
  return;
1624
1687
  }
1625
1688
  if (local) {
1626
- writeJson(res, 200, {
1689
+ if (method === MOBILE_PREFERENCES_METHOD) writeJson(res, 200, {
1627
1690
  type: "server-response",
1628
1691
  rpcId,
1629
1692
  result: {
@@ -1631,6 +1694,48 @@ function makeMobileApiRoutes(deps) {
1631
1694
  value: { mobileEnterToSend: mobileEnterToSend() }
1632
1695
  }
1633
1696
  });
1697
+ else if (method === MOBILE_PENDING_METHOD) {
1698
+ const payload = parsed.payload;
1699
+ writeJson(res, 200, {
1700
+ type: "server-response",
1701
+ rpcId,
1702
+ result: {
1703
+ ok: true,
1704
+ value: deps.pendingTracker.pending(payload?.sessionId)
1705
+ }
1706
+ });
1707
+ } else if (method === MOBILE_RESPOND_METHOD) {
1708
+ const payload = parsed.payload;
1709
+ try {
1710
+ writeJson(res, 200, {
1711
+ type: "server-response",
1712
+ rpcId,
1713
+ result: {
1714
+ ok: true,
1715
+ value: await apiProxy.respond({
1716
+ type: "client-response",
1717
+ rpcId: RpcId(payload.rpcId),
1718
+ result: {
1719
+ ok: true,
1720
+ value: payload.response
1721
+ }
1722
+ })
1723
+ }
1724
+ });
1725
+ } catch (error) {
1726
+ writeJson(res, 200, {
1727
+ type: "server-response",
1728
+ rpcId,
1729
+ result: {
1730
+ ok: false,
1731
+ error: {
1732
+ code: "internal",
1733
+ message: error instanceof Error ? error.message : String(error)
1734
+ }
1735
+ }
1736
+ });
1737
+ }
1738
+ }
1634
1739
  return;
1635
1740
  }
1636
1741
  try {
@@ -1661,8 +1766,13 @@ function makeMobileApiRoutes(deps) {
1661
1766
  return;
1662
1767
  }
1663
1768
  if (!gateOk(req)) {
1664
- res.writeHead(403);
1665
- res.end("forbidden");
1769
+ writeJson(res, 403, {
1770
+ ok: false,
1771
+ error: {
1772
+ code: "unpaired",
1773
+ message: "mobile session is not paired"
1774
+ }
1775
+ });
1666
1776
  return;
1667
1777
  }
1668
1778
  res.writeHead(200, {
@@ -1694,6 +1804,7 @@ function makeMobileApiRoutes(deps) {
1694
1804
  }, controller.signal);
1695
1805
  for await (const frame of frames) {
1696
1806
  if (closed) break;
1807
+ deps.pendingTracker.onFrame(frame);
1697
1808
  res.write(`data: ${JSON.stringify(frame)}\n\n`);
1698
1809
  }
1699
1810
  } catch {} finally {
@@ -1712,10 +1823,6 @@ function makeMobileApiRoutes(deps) {
1712
1823
  handler: handleEvents
1713
1824
  }];
1714
1825
  }
1715
- /** Read a request body as JSON (bounded). */
1716
- async function readJsonBody(req) {
1717
- return readBoundedJson(req, 64 * 1024);
1718
- }
1719
1826
  /** Dispatch one allowlisted method through the host ApiProxy. */
1720
1827
  async function dispatch(apiProxy, method, payload, rpcId, signal) {
1721
1828
  const request = {
@@ -1757,6 +1864,8 @@ async function dispatch(apiProxy, method, payload, rpcId, signal) {
1757
1864
  result: response.result
1758
1865
  });
1759
1866
  if (method === "workspace.list") return wrap(await apiProxy.workspace.list(request));
1867
+ if (method === "workspace.create") return wrap(await apiProxy.workspace.create(request));
1868
+ if (method === "host.listDirectory") return wrap(await apiProxy.host.listDirectory(request, signal ?? new AbortController().signal));
1760
1869
  if (method === "agentPreset.list") return wrap(await apiProxy.agentPresets.list(request));
1761
1870
  if (method === "session.create") return wrap(await apiProxy.sessions.create(request));
1762
1871
  if (method === "session.history") return wrap(await apiProxy.sessions.history(request));
@@ -1768,6 +1877,379 @@ async function dispatch(apiProxy, method, payload, rpcId, signal) {
1768
1877
  throw new Error(`unhandled allowlisted method ${method}`);
1769
1878
  }
1770
1879
  //#endregion
1880
+ //#region src/mobile-pending.ts
1881
+ var PendingTracker = class {
1882
+ sessions = /* @__PURE__ */ new Map();
1883
+ /** Process one mux frame and update the pending state. */
1884
+ onFrame(frame) {
1885
+ const payload = frame.payload;
1886
+ if (payload.type === "approval/requested") this._getOrInit(payload.sessionId).approvals.set(payload.approvalId, {
1887
+ rpcId: frame.rpcId,
1888
+ approvalId: payload.approvalId,
1889
+ toolName: payload.toolName,
1890
+ callId: payload.callId,
1891
+ reason: payload.reason
1892
+ });
1893
+ else if (payload.type === "approval/resolved") {
1894
+ const state = this.sessions.get(payload.sessionId);
1895
+ if (state) state.approvals.delete(payload.approvalId);
1896
+ } else if (payload.type === "question/requested") this._getOrInit(payload.sessionId).questions.push({
1897
+ rpcId: frame.rpcId,
1898
+ questions: payload.questions
1899
+ });
1900
+ else if (payload.type === "question/resolved") {
1901
+ const state = this.sessions.get(payload.sessionId);
1902
+ if (state) state.questions = state.questions.filter((q) => q.rpcId !== payload.questionRpcId);
1903
+ }
1904
+ }
1905
+ _getOrInit(sessionId) {
1906
+ let state = this.sessions.get(sessionId);
1907
+ if (!state) {
1908
+ state = {
1909
+ approvals: /* @__PURE__ */ new Map(),
1910
+ questions: []
1911
+ };
1912
+ this.sessions.set(sessionId, state);
1913
+ }
1914
+ return state;
1915
+ }
1916
+ /** Query pending items for a session. */
1917
+ pending(sessionId) {
1918
+ const state = this.sessions.get(sessionId);
1919
+ if (!state) return {
1920
+ approvals: [],
1921
+ questions: []
1922
+ };
1923
+ return {
1924
+ approvals: Array.from(state.approvals.values()),
1925
+ questions: state.questions
1926
+ };
1927
+ }
1928
+ /** Clear all pending state for a session. */
1929
+ clear(sessionId) {
1930
+ this.sessions.delete(sessionId);
1931
+ }
1932
+ };
1933
+ //#endregion
1934
+ //#region src/paired-model-catalog.ts
1935
+ const MAX_BODY_BYTES = 16 * 1024;
1936
+ const MAX_IDENTIFIER_LENGTH = 160;
1937
+ const MAX_DISPLAY_NAME_LENGTH = 240;
1938
+ const MAX_TOKEN_LIMIT = 1e7;
1939
+ const REASONING_EFFORTS = [
1940
+ "off",
1941
+ "minimal",
1942
+ "low",
1943
+ "medium",
1944
+ "high",
1945
+ "xhigh",
1946
+ "max"
1947
+ ];
1948
+ const stringField = (max) => z$1.string().trim().min(1).max(max).refine((value) => !/[\r\n\0]/.test(value), "must not contain a newline or NUL");
1949
+ const providerSchema = z$1.object({ provider: stringField(MAX_IDENTIFIER_LENGTH) }).strict();
1950
+ const modelSchema = z$1.object({
1951
+ id: stringField(MAX_IDENTIFIER_LENGTH),
1952
+ name: stringField(MAX_DISPLAY_NAME_LENGTH).optional(),
1953
+ contextWindow: z$1.number().int().safe().positive().max(MAX_TOKEN_LIMIT).optional(),
1954
+ maxTokens: z$1.number().int().safe().positive().max(MAX_TOKEN_LIMIT).optional(),
1955
+ reasoningEfforts: z$1.array(z$1.enum(REASONING_EFFORTS)).min(1).refine((efforts) => efforts.every((effort, index) => index === 0 || REASONING_EFFORTS.indexOf(efforts[index - 1]) < REASONING_EFFORTS.indexOf(effort)), "reasoning efforts must be unique and in canonical order").optional()
1956
+ }).strict();
1957
+ const upsertSchema = z$1.object({
1958
+ provider: stringField(MAX_IDENTIFIER_LENGTH),
1959
+ model: modelSchema
1960
+ }).strict();
1961
+ const PAIRED_MODEL_CATALOG_PATHS = {
1962
+ catalog: "/api/pair/model-catalog",
1963
+ discover: "/api/pair/model-catalog/discover",
1964
+ upsert: "/api/pair/model-catalog/upsert"
1965
+ };
1966
+ let rpcSequence = 0;
1967
+ function request$1(payload) {
1968
+ rpcSequence += 1;
1969
+ return {
1970
+ rpcId: `paired-model-catalog-${Date.now().toString(36)}-${rpcSequence.toString(36)}`,
1971
+ payload
1972
+ };
1973
+ }
1974
+ function isRecord(value) {
1975
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1976
+ }
1977
+ function valueOf(response) {
1978
+ return response.result.ok ? response.result.value : void 0;
1979
+ }
1980
+ function errorCode(response) {
1981
+ return response.result.ok ? void 0 : response.result.error.code;
1982
+ }
1983
+ function reject(res, status, error) {
1984
+ writeJson(res, status, { error });
1985
+ }
1986
+ function publicProviderId(value) {
1987
+ const normalized = value.replace(/[\r\n\0]/g, " ").trim().slice(0, MAX_IDENTIFIER_LENGTH);
1988
+ return normalized === "" ? "unknown" : normalized;
1989
+ }
1990
+ function publicCatalog(value) {
1991
+ return {
1992
+ groups: value.groups.map((group) => ({
1993
+ id: group.id,
1994
+ name: group.name,
1995
+ models: group.models.map((model) => ({
1996
+ id: model.id,
1997
+ name: model.name,
1998
+ ...model.description === void 0 ? {} : { description: model.description },
1999
+ ...model.reasoning === void 0 ? {} : { reasoning: model.reasoning }
2000
+ }))
2001
+ })),
2002
+ failures: value.failures.map((failure) => ({
2003
+ id: failure.id,
2004
+ name: failure.name,
2005
+ message: `model catalog unavailable for provider ${publicProviderId(failure.id)}`
2006
+ }))
2007
+ };
2008
+ }
2009
+ function publicCandidates(models) {
2010
+ return models.map((model) => ({
2011
+ id: model.id,
2012
+ ...model.name === void 0 ? {} : { name: model.name },
2013
+ ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
2014
+ ...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens }
2015
+ }));
2016
+ }
2017
+ function fieldsOf(model, includeId) {
2018
+ const fields = {
2019
+ ...includeId ? { id: model.id } : {},
2020
+ ...model.name === void 0 ? {} : { name: model.name },
2021
+ ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
2022
+ ...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens }
2023
+ };
2024
+ if (model.reasoningEfforts !== void 0) fields.reasoningEfforts = Object.fromEntries(model.reasoningEfforts.map((effort) => [effort, effort === "off" ? null : effort]));
2025
+ return fields;
2026
+ }
2027
+ function groupFor(catalog, provider) {
2028
+ return catalog.groups.find((group) => group.id === provider);
2029
+ }
2030
+ async function readCatalog(apiProxy) {
2031
+ try {
2032
+ return valueOf(await apiProxy.llm.models(request$1({})));
2033
+ } catch {
2034
+ return;
2035
+ }
2036
+ }
2037
+ /** Resolve one exact active llm-pi-ai provider address, never an input-supplied path. */
2038
+ async function eligibleProvider(apiProxy, requested) {
2039
+ let providersResponse;
2040
+ let settingsResponse;
2041
+ try {
2042
+ providersResponse = await apiProxy.llm.providers(request$1({}));
2043
+ settingsResponse = await apiProxy.settings.describe(request$1({}));
2044
+ } catch {
2045
+ return {
2046
+ status: 502,
2047
+ error: "model catalog capability is unavailable"
2048
+ };
2049
+ }
2050
+ const providers = valueOf(providersResponse);
2051
+ const settings = valueOf(settingsResponse);
2052
+ if (providers === void 0 || settings === void 0) return {
2053
+ status: 502,
2054
+ error: "model catalog capability is unavailable"
2055
+ };
2056
+ const route = providers.providers.find((candidate) => candidate.provider === requested);
2057
+ if (route === void 0) return {
2058
+ status: 404,
2059
+ error: `unknown provider ${requested}`
2060
+ };
2061
+ if (!settings.writable || !route.active || route.settingsNs !== "llm-pi-ai" || route.settingsPath.length !== 2 || route.settingsPath[0] !== "providers" || route.settingsPath[1] !== requested) return {
2062
+ status: 403,
2063
+ error: `provider ${requested} is not eligible for the paired model catalog`
2064
+ };
2065
+ const namespace = settings.namespaces.find((candidate) => candidate.ns === "llm-pi-ai");
2066
+ const root = namespace !== void 0 && isRecord(namespace.value) ? namespace.value : void 0;
2067
+ const providersValue = root !== void 0 && isRecord(root.providers) ? root.providers : void 0;
2068
+ const profile = providersValue !== void 0 && isRecord(providersValue[requested]) ? providersValue[requested] : void 0;
2069
+ if (namespace === void 0 || profile === void 0) return {
2070
+ status: 403,
2071
+ error: `provider ${requested} is not eligible for the paired model catalog`
2072
+ };
2073
+ return { provider: {
2074
+ provider: requested,
2075
+ displayName: route.displayName,
2076
+ namespace,
2077
+ profile
2078
+ } };
2079
+ }
2080
+ function updatedModels(existing, model) {
2081
+ const next = existing.map((entry) => isRecord(entry) ? { ...entry } : entry);
2082
+ const index = next.findIndex((entry) => isRecord(entry) && entry.id === model.id);
2083
+ const patch = fieldsOf(model, true);
2084
+ if (index >= 0 && isRecord(next[index])) next[index] = {
2085
+ ...next[index],
2086
+ ...patch
2087
+ };
2088
+ else next.push(patch);
2089
+ return next;
2090
+ }
2091
+ function mutationPlan(profile, provider, model, catalog) {
2092
+ if (Array.isArray(profile.models)) return {
2093
+ path: [
2094
+ "providers",
2095
+ provider,
2096
+ "models"
2097
+ ],
2098
+ value: updatedModels(profile.models, model)
2099
+ };
2100
+ const group = groupFor(catalog, provider);
2101
+ if (group?.models.some((entry) => entry.id === model.id) === true) {
2102
+ const current = (isRecord(profile.modelOverrides) ? profile.modelOverrides : void 0)?.[model.id];
2103
+ const existing = isRecord(current) ? current : {};
2104
+ return {
2105
+ path: [
2106
+ "providers",
2107
+ provider,
2108
+ "modelOverrides",
2109
+ model.id
2110
+ ],
2111
+ value: {
2112
+ ...existing,
2113
+ ...fieldsOf(model, false)
2114
+ }
2115
+ };
2116
+ }
2117
+ if (group === void 0 || catalog.failures.some((failure) => failure.id === provider)) return void 0;
2118
+ return {
2119
+ path: [
2120
+ "providers",
2121
+ provider,
2122
+ "models"
2123
+ ],
2124
+ value: [...group.models.map((entry) => ({ id: entry.id })), fieldsOf(model, true)]
2125
+ };
2126
+ }
2127
+ function paired(req, service) {
2128
+ const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
2129
+ return deviceId !== void 0 && service.touchDevice(deviceId);
2130
+ }
2131
+ async function bodyOf(req, schema) {
2132
+ try {
2133
+ return schema.parse(await readBoundedJson(req, MAX_BODY_BYTES));
2134
+ } catch {
2135
+ return;
2136
+ }
2137
+ }
2138
+ /** Build the narrow paired-device model catalog routes. */
2139
+ function makePairedModelCatalogRoutes(deps) {
2140
+ const { service, apiProxy, lanAddresses } = deps;
2141
+ /** Same fence as the /api/pair family: loopback, the advertised LAN literals, or the configured public host. */
2142
+ const fence = (req) => {
2143
+ const publicHost = publicHostOf(service.publicBaseUrl);
2144
+ return isTrustedApiRequest(req, publicHost === void 0 ? lanAddresses : [...lanAddresses, publicHost]);
2145
+ };
2146
+ const gate = (req, res) => {
2147
+ if (!fence(req)) {
2148
+ req.resume();
2149
+ reject(res, 403, "paired model catalog is not reachable for this origin");
2150
+ return false;
2151
+ }
2152
+ if (!paired(req, service)) {
2153
+ req.resume();
2154
+ reject(res, 403, "paired model catalog requires a live paired device");
2155
+ return false;
2156
+ }
2157
+ return true;
2158
+ };
2159
+ const catalog = async (req, res) => {
2160
+ if (!gate(req, res)) return;
2161
+ if (req.method !== "GET") return reject(res, 405, "method not allowed");
2162
+ let providersResponse;
2163
+ let settingsResponse;
2164
+ try {
2165
+ providersResponse = await apiProxy.llm.providers(request$1({}));
2166
+ settingsResponse = await apiProxy.settings.describe(request$1({}));
2167
+ } catch {
2168
+ return reject(res, 502, "model catalog capability is unavailable");
2169
+ }
2170
+ const providers = valueOf(providersResponse);
2171
+ const settings = valueOf(settingsResponse);
2172
+ if (providers === void 0 || settings === void 0) return reject(res, 502, "model catalog capability is unavailable");
2173
+ const namespace = settings.namespaces.find((candidate) => candidate.ns === "llm-pi-ai");
2174
+ const providerMap = namespace !== void 0 && isRecord(namespace.value) && isRecord(namespace.value.providers) ? namespace.value.providers : void 0;
2175
+ writeJson(res, 200, {
2176
+ capability: "paired-model-catalog",
2177
+ providers: settings.writable && providerMap !== void 0 ? providers.providers.filter((candidate) => candidate.active && candidate.settingsNs === "llm-pi-ai" && candidate.settingsPath.length === 2 && candidate.settingsPath[0] === "providers" && candidate.settingsPath[1] === candidate.provider && isRecord(providerMap[candidate.provider])).map((candidate) => ({
2178
+ provider: candidate.provider,
2179
+ displayName: candidate.displayName
2180
+ })) : []
2181
+ });
2182
+ };
2183
+ const discover = async (req, res) => {
2184
+ if (!gate(req, res)) return;
2185
+ if (req.method !== "POST") return reject(res, 405, "method not allowed");
2186
+ const body = await bodyOf(req, providerSchema);
2187
+ if (body === void 0) return reject(res, 400, "invalid model catalog request");
2188
+ const eligible = await eligibleProvider(apiProxy, body.provider);
2189
+ if (eligible.provider === void 0) return reject(res, eligible.status ?? 502, eligible.error ?? "model catalog capability is unavailable");
2190
+ try {
2191
+ const result = valueOf(await apiProxy.llm.discoverModels(request$1({
2192
+ settingsNs: "llm-pi-ai",
2193
+ provider: eligible.provider.provider
2194
+ })));
2195
+ if (result === void 0) return reject(res, 502, `model discovery failed for provider ${eligible.provider.provider}`);
2196
+ writeJson(res, 200, { models: publicCandidates(result.models) });
2197
+ } catch {
2198
+ reject(res, 502, `model discovery failed for provider ${eligible.provider.provider}`);
2199
+ }
2200
+ };
2201
+ const upsert = async (req, res) => {
2202
+ if (!gate(req, res)) return;
2203
+ if (req.method !== "POST") return reject(res, 405, "method not allowed");
2204
+ const body = await bodyOf(req, upsertSchema);
2205
+ if (body === void 0) return reject(res, 400, "invalid model catalog request");
2206
+ const eligible = await eligibleProvider(apiProxy, body.provider);
2207
+ if (eligible.provider === void 0) return reject(res, eligible.status ?? 502, eligible.error ?? "model catalog capability is unavailable");
2208
+ const before = await readCatalog(apiProxy);
2209
+ if (before === void 0) return reject(res, 502, `model catalog is unavailable for provider ${body.provider}`);
2210
+ const plan = mutationPlan(eligible.provider.profile, body.provider, body.model, before);
2211
+ if (plan === void 0) return reject(res, 502, `model catalog is unavailable for provider ${body.provider}`);
2212
+ let mutation;
2213
+ try {
2214
+ mutation = await apiProxy.settings.mutate(request$1({
2215
+ ns: "llm-pi-ai",
2216
+ expectedRevision: eligible.provider.namespace.revision,
2217
+ ops: [{
2218
+ op: "set",
2219
+ path: plan.path,
2220
+ value: plan.value
2221
+ }]
2222
+ }));
2223
+ } catch {
2224
+ return reject(res, 502, `model update failed for provider ${body.provider}`);
2225
+ }
2226
+ const code = errorCode(mutation);
2227
+ if (code === "settings-conflict") return reject(res, 409, `model update conflicted for provider ${body.provider}`);
2228
+ if (code === "settings-rejected") return reject(res, 422, `model update was rejected for provider ${body.provider}`);
2229
+ if (code !== void 0) return reject(res, 502, `model update failed for provider ${body.provider}`);
2230
+ const after = await readCatalog(apiProxy);
2231
+ if (after === void 0) return reject(res, 502, `model catalog is unavailable for provider ${body.provider}`);
2232
+ writeJson(res, 200, publicCatalog(after));
2233
+ };
2234
+ return [
2235
+ {
2236
+ kind: "exact",
2237
+ path: PAIRED_MODEL_CATALOG_PATHS.catalog,
2238
+ handler: catalog
2239
+ },
2240
+ {
2241
+ kind: "exact",
2242
+ path: PAIRED_MODEL_CATALOG_PATHS.discover,
2243
+ handler: discover
2244
+ },
2245
+ {
2246
+ kind: "exact",
2247
+ path: PAIRED_MODEL_CATALOG_PATHS.upsert,
2248
+ handler: upsert
2249
+ }
2250
+ ];
2251
+ }
2252
+ //#endregion
1771
2253
  //#region src/loopback-proxy.ts
1772
2254
  /** WebSocket handshake headers forwarded to the loopback upstream. */
1773
2255
  const WS_FORWARD_HEADERS = [
@@ -1881,13 +2363,13 @@ function proxyLoopbackUpgrade(req, socket, head, port, upstreamPath) {
1881
2363
  * client half can pin them without importing the host SDK graph.
1882
2364
  */
1883
2365
  /** Gated mirror of same-origin fenced paths (`/remote` + original pathname). */
1884
- const REMOTE_PREFIX = "/remote";
2366
+ const REMOTE_PREFIX$1 = "/remote";
1885
2367
  /** Connection-plugin method prefix under the gated channel. */
1886
- const REMOTE_API_PREFIX = `${REMOTE_PREFIX}/api`;
2368
+ const REMOTE_API_PREFIX$1 = `${REMOTE_PREFIX$1}/api`;
1887
2369
  /** WebSocket event-stream paths served by the channel (client rewrites to these). */
1888
2370
  const REMOTE_API_PATHS = {
1889
- mux: `${REMOTE_API_PREFIX}/events.mux`,
1890
- host: `${REMOTE_API_PREFIX}/events.host`
2371
+ mux: `${REMOTE_API_PREFIX$1}/events.mux`,
2372
+ host: `${REMOTE_API_PREFIX$1}/events.host`
1891
2373
  };
1892
2374
  /**
1893
2375
  * Exact upgrade paths registered on webServer (the SDK matches upgrades by
@@ -1896,9 +2378,9 @@ const REMOTE_API_PATHS = {
1896
2378
  const REMOTE_UPGRADE_PATHS = [
1897
2379
  REMOTE_API_PATHS.mux,
1898
2380
  REMOTE_API_PATHS.host,
1899
- `${REMOTE_PREFIX}/sidebar/ws/terminal`,
1900
- `${REMOTE_PREFIX}/sidebar/ws/agent-terminals`,
1901
- `${REMOTE_API_PREFIX}/dsh-ssh/terminal`
2381
+ `${REMOTE_PREFIX$1}/sidebar/ws/terminal`,
2382
+ `${REMOTE_PREFIX$1}/sidebar/ws/agent-terminals`,
2383
+ `${REMOTE_API_PREFIX$1}/dsh-ssh/terminal`
1902
2384
  ];
1903
2385
  /**
1904
2386
  * Loopback-only methods of the host API surface, mirrored from
@@ -2026,7 +2508,7 @@ function makeRemoteApiRoutes(deps) {
2026
2508
  };
2027
2509
  return [{
2028
2510
  kind: "prefix",
2029
- path: REMOTE_PREFIX,
2511
+ path: REMOTE_PREFIX$1,
2030
2512
  handler
2031
2513
  }];
2032
2514
  }
@@ -2993,6 +3475,86 @@ function mountOnce(packageName, fn) {
2993
3475
  return fn(...args);
2994
3476
  });
2995
3477
  }
3478
+ /** The live rule set. */
3479
+ const REMOTE_CHANNEL_RULES = {
3480
+ remotePrefix: "/remote",
3481
+ apiPrefix: "/api/",
3482
+ pairPrefix: "/api/pair/",
3483
+ updatePrefix: "/api/update/",
3484
+ desktopLauncherPrefix: "/api/dsh-desktop-launcher",
3485
+ settingsBridgePrefix: "/api/dsh-web-ui-settings",
3486
+ sidebarPrefix: "/sidebar/",
3487
+ gitPrefix: "/git/",
3488
+ petPrefix: "/pet/",
3489
+ wsPaths: [
3490
+ "/api/events.mux",
3491
+ "/api/events.host",
3492
+ "/sidebar/ws/terminal",
3493
+ "/sidebar/ws/agent-terminals",
3494
+ "/api/dsh-ssh/terminal"
3495
+ ]
3496
+ };
3497
+ /** The window global the boot patch publishes its seat under. */
3498
+ const REMOTE_CHANNEL_BOOT_GLOBAL = "__DSH_REMOTE_CHANNEL_BOOT__";
3499
+ //#endregion
3500
+ //#region src/remote-channel-boot.ts
3501
+ /**
3502
+ * Parse-time remote-channel boot patch (issue #987): the browser-half patch
3503
+ * (client/remote-channel.ts) installs when this plugin's boot entry runs,
3504
+ * but `dsh-client-connection` boots earlier and opens its event streams
3505
+ * unrewritten — on a non-loopback origin the SDK fence rejects them and the
3506
+ * workspace list never loads. The host therefore inlines this classic script
3507
+ * right after the opening <head> tag (webserver/index-inject), so the
3508
+ * fetch/WebSocket/EventSource/src rewrite is active before ANY boot entry
3509
+ * executes. The plugin's client apply later adopts the installed seat
3510
+ * (hooks + pending unpaired signal) instead of patching twice.
3511
+ *
3512
+ * The rewrite decisions are generated from REMOTE_CHANNEL_RULES, the same
3513
+ * data the browser patch consumes — the two cannot drift apart. The script
3514
+ * self-skips on loopback origins and never throws.
3515
+ * @module @linxin666/dsh-remote-web-ui/remote-channel-boot
3516
+ */
3517
+ /**
3518
+ * Build the inline boot script. The result contains no `<\/script` sequence
3519
+ * (the injection contract) and installs a {@link RemoteChannelBootSeat} on
3520
+ * the window global.
3521
+ */
3522
+ function buildRemoteChannelBootScript(rules = REMOTE_CHANNEL_RULES) {
3523
+ const json = JSON.stringify(rules);
3524
+ const seat = JSON.stringify(REMOTE_CHANNEL_BOOT_GLOBAL);
3525
+ return "(function(){try{var w=window,loc=w.location,h=loc.hostname;if(h==='localhost'||h==='::1'||/^127(\\.\\d{1,3}){3}$/.test(h))return;var R=" + json + ";function sf(p){if(p.indexOf(R.pairPrefix)===0)return false;if(p.indexOf(R.updatePrefix)===0)return false;if(p===R.desktopLauncherPrefix||p.indexOf(R.desktopLauncherPrefix+\"/\")===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,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);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);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);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}catch(e){}})();";
3526
+ }
3527
+ /** The script this plugin contributes; built once from the live rules. */
3528
+ const REMOTE_CHANNEL_BOOT_SCRIPT = buildRemoteChannelBootScript();
3529
+ //#endregion
3530
+ //#region src/uuid-polyfill.ts
3531
+ /**
3532
+ * Polyfill for crypto.randomUUID for non-secure contexts (LAN HTTP)
3533
+ * where the browser does not expose crypto.randomUUID.
3534
+ *
3535
+ * The script string is injected as an inline <script> in the desktop HTML
3536
+ * head so the SDK's mintRpcId() sees a working randomUUID even on plain
3537
+ * http:// origins (#1024).
3538
+ */
3539
+ const UUID_POLYFILL_SCRIPT = [
3540
+ "(function(){",
3541
+ "var g=typeof globalThis!==\"undefined\"?globalThis:typeof window!==\"undefined\"?window:self;",
3542
+ "if(typeof g.crypto===\"undefined\"){g.crypto={};}",
3543
+ "if(typeof g.crypto.randomUUID!==\"function\"){",
3544
+ "g.crypto.randomUUID=function(){",
3545
+ "var a=new Uint8Array(16);",
3546
+ "if(g.crypto.getRandomValues){g.crypto.getRandomValues(a);}",
3547
+ "else{for(var j=0;j<16;j++){a[j]=Math.random()*256|0;}}",
3548
+ "a[6]=(a[6]&0x0f)|0x40;",
3549
+ "a[8]=(a[8]&0x3f)|0x80;",
3550
+ "var h=\"\",x=\"0123456789abcdef\";",
3551
+ "for(var i=0;i<16;i++){h+=x[a[i]>>4]+x[a[i]&0x0f];",
3552
+ "if(i===3||i===5||i===7||i===9)h+=\"-\";}",
3553
+ "return h;",
3554
+ "};",
3555
+ "}",
3556
+ "})();"
3557
+ ].join("");
2996
3558
  //#endregion
2997
3559
  //#region src/index.ts
2998
3560
  /**
@@ -3198,8 +3760,14 @@ function applyImpl(ctx, config) {
3198
3760
  ...apiProxy !== void 0 ? makeMobileApiRoutes({
3199
3761
  service,
3200
3762
  apiProxy,
3763
+ pendingTracker: new PendingTracker(),
3201
3764
  mobileEnterToSend: () => resolve().mobileEnterToSend
3202
3765
  }) : [],
3766
+ ...apiProxy !== void 0 ? makePairedModelCatalogRoutes({
3767
+ service,
3768
+ apiProxy,
3769
+ lanAddresses
3770
+ }) : [],
3203
3771
  ...makeRemoteApiRoutes({
3204
3772
  service,
3205
3773
  port: ctx.webServer.port,
@@ -3294,6 +3862,22 @@ function applyImpl(ctx, config) {
3294
3862
  }
3295
3863
  runPostureProbe();
3296
3864
  };
3865
+ ctx.effect(() => ctx.on("webserver/index-inject", (table) => {
3866
+ table.push({
3867
+ kind: "script",
3868
+ placement: "head",
3869
+ text: UUID_POLYFILL_SCRIPT
3870
+ });
3871
+ }), "remote-web-ui: uuid polyfill");
3872
+ ctx.effect(() => ctx.on("webserver/index-inject", (table) => {
3873
+ const value = resolve();
3874
+ if (!value.enabled || !value.requirePairingForLan) return;
3875
+ table.push({
3876
+ kind: "script",
3877
+ placement: "head",
3878
+ text: REMOTE_CHANNEL_BOOT_SCRIPT
3879
+ });
3880
+ }), "remote-web-ui: remote channel boot patch");
3297
3881
  installSettingsSection(ctx, REMOTE_WEB_UI_SETTINGS_NAMESPACE, Config, config ?? {}, {
3298
3882
  setSource: (source) => {
3299
3883
  current = source;