dsh-zen-remote 2.3.11 → 2.4.1

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 (249) hide show
  1. package/CHANGELOG.md +45 -78
  2. package/README.md +94 -84
  3. package/README.zh-CN.md +90 -81
  4. package/bin/zen-remote.mjs +5 -0
  5. package/docs/interface.md +6 -6
  6. package/docs/remote-access.en.md +6 -4
  7. package/docs/remote-access.md +6 -4
  8. package/dsh-push.mjs +96 -10
  9. package/lib/adapters/dsh-better-sidebar/index.js +7 -3
  10. package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
  11. package/lib/admin-routes.js +10 -2
  12. package/lib/admin-routes.js.map +1 -1
  13. package/lib/cli/args.js +251 -0
  14. package/lib/cli/args.js.map +1 -0
  15. package/lib/cli/bootstrap.js +449 -0
  16. package/lib/cli/bootstrap.js.map +1 -0
  17. package/lib/cli/command-setup.js +484 -0
  18. package/lib/cli/command-setup.js.map +1 -0
  19. package/lib/cli/commands-client.js +359 -0
  20. package/lib/cli/commands-client.js.map +1 -0
  21. package/lib/cli/commands-host.js +1108 -0
  22. package/lib/cli/commands-host.js.map +1 -0
  23. package/lib/cli/commands-status.js +279 -0
  24. package/lib/cli/commands-status.js.map +1 -0
  25. package/lib/cli/config-writer.js +334 -0
  26. package/lib/cli/config-writer.js.map +1 -0
  27. package/lib/cli/context.js +10 -0
  28. package/lib/cli/context.js.map +1 -0
  29. package/lib/cli/i18n.js +620 -0
  30. package/lib/cli/i18n.js.map +1 -0
  31. package/lib/cli/ipc-client.js +160 -0
  32. package/lib/cli/ipc-client.js.map +1 -0
  33. package/lib/cli/ipc-server.js +464 -0
  34. package/lib/cli/ipc-server.js.map +1 -0
  35. package/lib/cli/main.js +245 -0
  36. package/lib/cli/main.js.map +1 -0
  37. package/lib/cli/qr-terminal.js +48 -0
  38. package/lib/cli/qr-terminal.js.map +1 -0
  39. package/lib/client-data/qr.js +286 -0
  40. package/lib/client-data/qr.js.map +1 -0
  41. package/lib/client-routes.js +308 -25
  42. package/lib/client-routes.js.map +1 -1
  43. package/lib/client.js +1013 -229
  44. package/lib/config.js +50 -12
  45. package/lib/config.js.map +1 -1
  46. package/lib/debug-report.js +28 -1
  47. package/lib/debug-report.js.map +1 -1
  48. package/lib/fetch-route-intercept.js +334 -26
  49. package/lib/fetch-route-intercept.js.map +1 -1
  50. package/lib/index.js +608 -375
  51. package/lib/index.js.map +1 -1
  52. package/lib/intercept.js +1833 -280
  53. package/lib/intercept.js.map +1 -1
  54. package/lib/lan-gate-server.cjs +36 -10
  55. package/lib/merge-streams.js +103 -11
  56. package/lib/merge-streams.js.map +1 -1
  57. package/lib/plugin-route-intercept.js +93 -63
  58. package/lib/plugin-route-intercept.js.map +1 -1
  59. package/lib/relay-access.js +8 -1
  60. package/lib/relay-access.js.map +1 -1
  61. package/lib/relay-client.js +133 -4
  62. package/lib/relay-client.js.map +1 -1
  63. package/lib/relay-pool.js +275 -0
  64. package/lib/relay-pool.js.map +1 -0
  65. package/lib/relay-server.js +269 -17
  66. package/lib/relay-server.js.map +1 -1
  67. package/lib/restart-fields.js +6 -4
  68. package/lib/restart-fields.js.map +1 -1
  69. package/lib/share-ops.js +68 -0
  70. package/lib/share-ops.js.map +1 -1
  71. package/lib/share-store.js +65 -27
  72. package/lib/share-store.js.map +1 -1
  73. package/lib/state-store.js +400 -0
  74. package/lib/state-store.js.map +1 -0
  75. package/lib/types/adapters/dsh-better-sidebar/index.d.ts +3 -2
  76. package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
  77. package/lib/types/admin-routes.d.ts.map +1 -1
  78. package/lib/types/cli/args.d.ts +78 -0
  79. package/lib/types/cli/args.d.ts.map +1 -0
  80. package/lib/types/cli/bootstrap.d.ts +170 -0
  81. package/lib/types/cli/bootstrap.d.ts.map +1 -0
  82. package/lib/types/cli/command-setup.d.ts +72 -0
  83. package/lib/types/cli/command-setup.d.ts.map +1 -0
  84. package/lib/types/cli/commands-client.d.ts +36 -0
  85. package/lib/types/cli/commands-client.d.ts.map +1 -0
  86. package/lib/types/cli/commands-host.d.ts +82 -0
  87. package/lib/types/cli/commands-host.d.ts.map +1 -0
  88. package/lib/types/cli/commands-status.d.ts +46 -0
  89. package/lib/types/cli/commands-status.d.ts.map +1 -0
  90. package/lib/types/cli/config-writer.d.ts +92 -0
  91. package/lib/types/cli/config-writer.d.ts.map +1 -0
  92. package/lib/types/cli/context.d.ts +39 -0
  93. package/lib/types/cli/context.d.ts.map +1 -0
  94. package/lib/types/cli/i18n.d.ts +32 -0
  95. package/lib/types/cli/i18n.d.ts.map +1 -0
  96. package/lib/types/cli/ipc-client.d.ts +83 -0
  97. package/lib/types/cli/ipc-client.d.ts.map +1 -0
  98. package/lib/types/cli/ipc-server.d.ts +113 -0
  99. package/lib/types/cli/ipc-server.d.ts.map +1 -0
  100. package/lib/types/cli/main.d.ts +57 -0
  101. package/lib/types/cli/main.d.ts.map +1 -0
  102. package/lib/types/cli/qr-terminal.d.ts +17 -0
  103. package/lib/types/cli/qr-terminal.d.ts.map +1 -0
  104. package/lib/types/client/RemoteStatusIcon.d.ts.map +1 -1
  105. package/lib/types/client/current-session-report.d.ts +4 -0
  106. package/lib/types/client/current-session-report.d.ts.map +1 -1
  107. package/lib/types/client/effects/model-group-side.d.ts.map +1 -1
  108. package/lib/types/client/effects/remote-session.d.ts.map +1 -1
  109. package/lib/types/client/locales.d.ts +47 -3
  110. package/lib/types/client/locales.d.ts.map +1 -1
  111. package/lib/types/client/settings/SettingsSection.d.ts.map +1 -1
  112. package/lib/types/client/settings/settings-css.d.ts +1 -1
  113. package/lib/types/client/settings/settings-css.d.ts.map +1 -1
  114. package/lib/types/client/styles/remote-session.css.d.ts +1 -1
  115. package/lib/types/client/styles/remote-session.css.d.ts.map +1 -1
  116. package/lib/types/client/wizard/GatewayWizard.d.ts.map +1 -1
  117. package/lib/types/client/wizard/cards/ProxyCard.d.ts.map +1 -1
  118. package/lib/types/client/wizard/wizard-css.d.ts +1 -1
  119. package/lib/types/client/wizard/wizard-css.d.ts.map +1 -1
  120. package/lib/types/client-data/model-group-side.d.ts +23 -2
  121. package/lib/types/client-data/model-group-side.d.ts.map +1 -1
  122. package/lib/types/client-data/remote-status.d.ts +47 -1
  123. package/lib/types/client-data/remote-status.d.ts.map +1 -1
  124. package/lib/types/client-data/settings-form.d.ts +85 -1
  125. package/lib/types/client-data/settings-form.d.ts.map +1 -1
  126. package/lib/types/client-data/wizard.d.ts +56 -1
  127. package/lib/types/client-data/wizard.d.ts.map +1 -1
  128. package/lib/types/client-routes.d.ts +25 -1
  129. package/lib/types/client-routes.d.ts.map +1 -1
  130. package/lib/types/config.d.ts +40 -14
  131. package/lib/types/config.d.ts.map +1 -1
  132. package/lib/types/debug-report.d.ts.map +1 -1
  133. package/lib/types/fetch-route-intercept.d.ts +66 -8
  134. package/lib/types/fetch-route-intercept.d.ts.map +1 -1
  135. package/lib/types/index.d.ts +9 -2
  136. package/lib/types/index.d.ts.map +1 -1
  137. package/lib/types/intercept.d.ts +72 -10
  138. package/lib/types/intercept.d.ts.map +1 -1
  139. package/lib/types/merge-streams.d.ts +79 -1
  140. package/lib/types/merge-streams.d.ts.map +1 -1
  141. package/lib/types/plugin-route-intercept.d.ts +25 -9
  142. package/lib/types/plugin-route-intercept.d.ts.map +1 -1
  143. package/lib/types/relay-access.d.ts +3 -1
  144. package/lib/types/relay-access.d.ts.map +1 -1
  145. package/lib/types/relay-client.d.ts +23 -2
  146. package/lib/types/relay-client.d.ts.map +1 -1
  147. package/lib/types/relay-pool.d.ts +148 -0
  148. package/lib/types/relay-pool.d.ts.map +1 -0
  149. package/lib/types/relay-server.d.ts +30 -3
  150. package/lib/types/relay-server.d.ts.map +1 -1
  151. package/lib/types/restart-fields.d.ts +5 -5
  152. package/lib/types/restart-fields.d.ts.map +1 -1
  153. package/lib/types/share-ops.d.ts +47 -1
  154. package/lib/types/share-ops.d.ts.map +1 -1
  155. package/lib/types/share-store.d.ts.map +1 -1
  156. package/lib/types/state-store.d.ts +179 -0
  157. package/lib/types/state-store.d.ts.map +1 -0
  158. package/lib/types/upload-limits.d.ts +15 -0
  159. package/lib/types/upload-limits.d.ts.map +1 -1
  160. package/lib/types/virtual-id.d.ts +21 -0
  161. package/lib/types/virtual-id.d.ts.map +1 -1
  162. package/lib/types/wizard/probe.d.ts.map +1 -1
  163. package/lib/types/wizard/proxy-snippets.d.ts +3 -0
  164. package/lib/types/wizard/proxy-snippets.d.ts.map +1 -1
  165. package/lib/types/wizard/routes.d.ts +1 -0
  166. package/lib/types/wizard/routes.d.ts.map +1 -1
  167. package/lib/types/wizard/status.d.ts +9 -0
  168. package/lib/types/wizard/status.d.ts.map +1 -1
  169. package/lib/types/wizard/verify.d.ts +3 -0
  170. package/lib/types/wizard/verify.d.ts.map +1 -1
  171. package/lib/upload-limits.js +15 -0
  172. package/lib/upload-limits.js.map +1 -1
  173. package/lib/virtual-id.js +60 -0
  174. package/lib/virtual-id.js.map +1 -1
  175. package/lib/wizard/probe.js +46 -1
  176. package/lib/wizard/probe.js.map +1 -1
  177. package/lib/wizard/proxy-snippets.js +14 -1
  178. package/lib/wizard/proxy-snippets.js.map +1 -1
  179. package/lib/wizard/routes.js +23 -4
  180. package/lib/wizard/routes.js.map +1 -1
  181. package/lib/wizard/status.js +21 -0
  182. package/lib/wizard/status.js.map +1 -1
  183. package/lib/wizard/verify.js +31 -8
  184. package/lib/wizard/verify.js.map +1 -1
  185. package/package.json +8 -4
  186. package/pwa/icons/badge-96.png +0 -0
  187. package/pwa/icons/badge.svg +1 -0
  188. package/pwa/manifest.json +2 -1
  189. package/pwa/sw.js +3 -1
  190. package/scripts/check-debug-report.mjs +41 -1
  191. package/scripts/check-settings-form.mjs +14 -3
  192. package/scripts/check-settings-view.mjs +247 -0
  193. package/scripts/check-wizard-ui.mjs +37 -0
  194. package/src/adapters/dsh-better-sidebar/index.ts +7 -3
  195. package/src/admin-routes.ts +10 -2
  196. package/src/cli/args.ts +289 -0
  197. package/src/cli/bootstrap.ts +569 -0
  198. package/src/cli/command-setup.ts +540 -0
  199. package/src/cli/commands-client.ts +369 -0
  200. package/src/cli/commands-host.ts +1127 -0
  201. package/src/cli/commands-status.ts +296 -0
  202. package/src/cli/config-writer.ts +354 -0
  203. package/src/cli/context.ts +38 -0
  204. package/src/cli/i18n.ts +645 -0
  205. package/src/cli/ipc-client.ts +216 -0
  206. package/src/cli/ipc-server.ts +531 -0
  207. package/src/cli/main.ts +285 -0
  208. package/src/cli/qr-terminal.ts +49 -0
  209. package/src/client/RemoteStatusIcon.tsx +4 -1
  210. package/src/client/current-session-report.ts +28 -4
  211. package/src/client/effects/model-group-side.ts +19 -5
  212. package/src/client/effects/remote-session.ts +26 -6
  213. package/src/client/locales.ts +96 -6
  214. package/src/client/settings/SettingsSection.tsx +287 -92
  215. package/src/client/settings/settings-css.ts +18 -0
  216. package/src/client/styles/remote-session.css.ts +15 -1
  217. package/src/client/wizard/GatewayWizard.tsx +470 -109
  218. package/src/client/wizard/cards/ProxyCard.tsx +2 -1
  219. package/src/client/wizard/wizard-css.ts +23 -0
  220. package/src/client-data/model-group-side.ts +37 -10
  221. package/src/client-data/remote-status.ts +112 -17
  222. package/src/client-data/settings-form.ts +164 -8
  223. package/src/client-data/wizard.ts +112 -2
  224. package/src/client-routes.ts +334 -26
  225. package/src/config.ts +62 -16
  226. package/src/debug-report.ts +27 -1
  227. package/src/fetch-route-intercept.ts +381 -32
  228. package/src/index.ts +608 -380
  229. package/src/intercept.ts +1866 -286
  230. package/src/merge-streams.ts +155 -10
  231. package/src/plugin-route-intercept.ts +120 -71
  232. package/src/relay-access.ts +14 -3
  233. package/src/relay-client.ts +163 -5
  234. package/src/relay-pool.ts +387 -0
  235. package/src/relay-server.ts +312 -16
  236. package/src/restart-fields.ts +6 -4
  237. package/src/share-ops.ts +82 -1
  238. package/src/share-store.ts +59 -26
  239. package/src/state-store.ts +428 -0
  240. package/src/upload-limits.ts +16 -0
  241. package/src/virtual-id.ts +57 -0
  242. package/src/wizard/probe.ts +54 -2
  243. package/src/wizard/proxy-snippets.ts +17 -2
  244. package/src/wizard/routes.ts +24 -5
  245. package/src/wizard/status.ts +28 -0
  246. package/src/wizard/verify.ts +33 -11
  247. package/AGENTS.md +0 -185
  248. package/docs/adr/0001-plugin-bridge-forwards-plugin-routes.md +0 -15
  249. package/docs/adr/0002-relay-emulates-websocket.md +0 -8
@@ -327,11 +327,51 @@ export function installPluginRouteIntercept(options) {
327
327
  },
328
328
  };
329
329
  }
330
- const { webServer, registry, relay, getServerId, getStatus, log } = options;
330
+ const { webServer, registry, log } = options;
331
331
  const adapters = options.adapters ?? [];
332
332
  const getCurrentSession = options.getCurrentSession;
333
333
  const getKnownRealIds = options.getKnownRealIds;
334
334
  const onMintedVirtualIds = options.onMintedVirtualIds;
335
+ const { relay, getServerId, getStatus } = options;
336
+ const { getRelayForServer, getStatusForServer, getKnownRealIdsForServer } = options;
337
+ // Exactly one mode, like installIntercept: the pool faces or the single
338
+ // row client — a wiring that passes neither would refuse every forwarded
339
+ // request as server-changed / not-bridged, so it is refused here instead.
340
+ if ((getRelayForServer === undefined && (relay === undefined || getServerId === undefined))
341
+ || (getStatusForServer === undefined && getStatus === undefined)) {
342
+ throw new Error('installPluginRouteIntercept requires either `relay` + `getServerId` + `getStatus` (single server) or `getRelayForServer` + `getStatusForServer` (pool)');
343
+ }
344
+ // The single-server facts, proven present by the guard above.
345
+ const singleRelay = relay;
346
+ const singleGetServerId = getServerId;
347
+ const singleGetStatus = getStatus;
348
+ /** MS-4: the relay client serving one server id, or `undefined` (the
349
+ * `server-changed` refusal). */
350
+ const relayOfServer = (serverId) => getRelayForServer !== undefined ? getRelayForServer(serverId) : serverId === singleGetServerId() ? singleRelay : undefined;
351
+ const statusesOfServer = (serverId) => getStatusForServer !== undefined ? getStatusForServer(serverId) : singleGetStatus();
352
+ const knownRealIdsOfServer = (serverId) => getKnownRealIdsForServer !== undefined ? getKnownRealIdsForServer(serverId) : getKnownRealIds?.();
353
+ /** MS-4: the one server every located virtual id of a request belongs to,
354
+ * with its relay client — or the refusal reason, judged id by id in hit
355
+ * order (the single-server walls' order). A request naming ids of two
356
+ * servers is refused like a foreign one: it has no single home. */
357
+ const serverOfHits = (hits) => {
358
+ let located;
359
+ for (const idHit of hits) {
360
+ const parts = fromVirtual(idHit.value);
361
+ if (parts === undefined || parts.id === '')
362
+ return 'bad-query';
363
+ if (located === undefined) {
364
+ const target = relayOfServer(parts.serverId);
365
+ if (target === undefined)
366
+ return 'server-changed';
367
+ located = { serverId: parts.serverId, relay: target };
368
+ }
369
+ else if (parts.serverId !== located.serverId) {
370
+ return 'server-changed';
371
+ }
372
+ }
373
+ return located ?? 'bad-query';
374
+ };
335
375
  const target = webServer;
336
376
  const savedMatch = target.match;
337
377
  let installed = true;
@@ -496,7 +536,8 @@ export function installPluginRouteIntercept(options) {
496
536
  answerUnbridged(res, declared.plugin, 'bad-query');
497
537
  return;
498
538
  }
499
- if (parts.serverId !== getServerId()) {
539
+ const targetRelay = relayOfServer(parts.serverId);
540
+ if (targetRelay === undefined) {
500
541
  answerUnbridged(res, declared.plugin, 'server-changed');
501
542
  return;
502
543
  }
@@ -522,7 +563,7 @@ export function installPluginRouteIntercept(options) {
522
563
  log?.('plugin request for %s refused locally: body-too-large (%s declared)', declared.path, req.headers['content-length']);
523
564
  return;
524
565
  }
525
- const status = getStatus().find((entry) => entry.plugin === declared.plugin);
566
+ const status = statusesOfServer(parts.serverId).find((entry) => entry.plugin === declared.plugin);
526
567
  if (status === undefined || !status.bridged) {
527
568
  answerUnbridged(res, declared.plugin, status?.reason ?? 'not-on-server');
528
569
  return;
@@ -594,7 +635,7 @@ export function installPluginRouteIntercept(options) {
594
635
  bodyStream = Readable.toWeb(gate);
595
636
  req.pipe(gate);
596
637
  }
597
- const response = await relay.pluginRequest({
638
+ const response = await targetRelay.pluginRequest({
598
639
  plugin: declared.plugin,
599
640
  version: declared.version,
600
641
  digest: declared.digest,
@@ -628,7 +669,7 @@ export function installPluginRouteIntercept(options) {
628
669
  // known inventory, read live right here (undefined when the wiring
629
670
  // passed no getter — the scan stays request-local).
630
671
  if (declared.rewriteIds) {
631
- await answerRewriteIds(res, response, idMapping, declared.rewriteFields, parts.serverId, getKnownRealIds?.(), onMintedVirtualIds);
672
+ await answerRewriteIds(res, response, idMapping, declared.rewriteFields, parts.serverId, knownRealIdsOfServer(parts.serverId), onMintedVirtualIds);
632
673
  return;
633
674
  }
634
675
  const contentType = response.headers.get('content-type');
@@ -827,7 +868,6 @@ export function installPluginRouteIntercept(options) {
827
868
  answerUnbridged(res, plugin, 'method-not-bridged');
828
869
  return;
829
870
  }
830
- const serverId = getServerId();
831
871
  // PB-22: the request's own real→virtual pairs, collected while the
832
872
  // wall validates each hit — the READ-ONLY mapping a rewriteIds
833
873
  // route's answer scan consults (`zr~` carries the real id inside, so
@@ -839,24 +879,20 @@ export function installPluginRouteIntercept(options) {
839
879
  // ONE more source beside this mapping — the known-real-session
840
880
  // inventory, read live at the answer (never copied here) and
841
881
  // consumed read-only; nothing here writes any session table.
842
- const idMapping = new Map();
843
- let locatedServerId;
844
- for (const idHit of verdict.hits) {
845
- const parts = fromVirtual(idHit.value);
846
- if (parts === undefined || parts.id === '') {
847
- answerUnbridged(res, plugin, 'bad-query');
848
- return;
849
- }
850
- if (parts.serverId !== serverId) {
851
- answerUnbridged(res, plugin, 'server-changed');
852
- return;
853
- }
854
- idMapping.set(parts.id, idHit.value);
855
- locatedServerId = parts.serverId;
882
+ // MS-4: every located id must name ONE server the wiring serves;
883
+ // that server's relay, verdicts and inventory carry the rest.
884
+ const home = serverOfHits(verdict.hits);
885
+ if (home === 'bad-query' || home === 'server-changed') {
886
+ answerUnbridged(res, plugin, home);
887
+ return;
856
888
  }
889
+ const locatedServerId = home.serverId;
890
+ const idMapping = new Map();
891
+ for (const idHit of verdict.hits)
892
+ idMapping.set(fromVirtual(idHit.value).id, idHit.value);
857
893
  // The bridge verdicts judged from the SAME live table the query
858
894
  // half reads.
859
- const status = getStatus().find((entry) => entry.plugin === plugin);
895
+ const status = statusesOfServer(locatedServerId).find((entry) => entry.plugin === plugin);
860
896
  if (status === undefined || !status.bridged) {
861
897
  answerUnbridged(res, plugin, status?.reason ?? 'not-on-server');
862
898
  return;
@@ -919,7 +955,7 @@ export function installPluginRouteIntercept(options) {
919
955
  if (Number.isFinite(declaredLength) && declaredLength > 0)
920
956
  contentLength = declaredLength;
921
957
  }
922
- const response = await relay.pluginRequest({
958
+ const response = await home.relay.pluginRequest({
923
959
  plugin,
924
960
  version: declared.version,
925
961
  digest: declared.digest,
@@ -942,7 +978,7 @@ export function installPluginRouteIntercept(options) {
942
978
  return;
943
979
  }
944
980
  if (declared.rewriteIds) {
945
- await answerRewriteIds(res, response, idMapping, declared.rewriteFields, locatedServerId, getKnownRealIds?.(), onMintedVirtualIds);
981
+ await answerRewriteIds(res, response, idMapping, declared.rewriteFields, locatedServerId, knownRealIdsOfServer(locatedServerId), onMintedVirtualIds);
946
982
  return;
947
983
  }
948
984
  const contentType = response.headers.get('content-type');
@@ -1143,23 +1179,18 @@ export function installPluginRouteIntercept(options) {
1143
1179
  answerUnbridged(res, plugin, 'method-not-bridged');
1144
1180
  return;
1145
1181
  }
1146
- const serverId = getServerId();
1147
- for (const idHit of verdict.hits) {
1148
- const parts = fromVirtual(idHit.value);
1149
- if (parts === undefined || parts.id === '') {
1150
- answerUnbridged(res, plugin, 'bad-query');
1151
- return;
1152
- }
1153
- if (parts.serverId !== serverId) {
1154
- answerUnbridged(res, plugin, 'server-changed');
1155
- return;
1156
- }
1182
+ // MS-4: one home server for every located id.
1183
+ const home = serverOfHits(verdict.hits);
1184
+ if (home === 'bad-query' || home === 'server-changed') {
1185
+ answerUnbridged(res, plugin, home);
1186
+ return;
1157
1187
  }
1188
+ const serverId = home.serverId;
1158
1189
  // The bridge verdicts (version skew, consent, offline…) judged from
1159
1190
  // the SAME live table the cooperative surrogate reads — the
1160
1191
  // adapter's registry entry makes the plugin indistinguishable from
1161
1192
  // a cooperative one here.
1162
- const status = getStatus().find((entry) => entry.plugin === plugin);
1193
+ const status = statusesOfServer(serverId).find((entry) => entry.plugin === plugin);
1163
1194
  if (status === undefined || !status.bridged) {
1164
1195
  answerUnbridged(res, plugin, status?.reason ?? 'not-on-server');
1165
1196
  return;
@@ -1218,7 +1249,7 @@ export function installPluginRouteIntercept(options) {
1218
1249
  bodyStream = Readable.toWeb(gate);
1219
1250
  req.pipe(gate);
1220
1251
  }
1221
- const response = await relay.pluginRequest({
1252
+ const response = await home.relay.pluginRequest({
1222
1253
  plugin,
1223
1254
  version: adapter.declaration.version,
1224
1255
  digest: adapter.digest,
@@ -1322,7 +1353,7 @@ export function installPluginRouteIntercept(options) {
1322
1353
  pathname,
1323
1354
  body: JSON.parse(text),
1324
1355
  virtualize: (id) => {
1325
- const virtualId = toVirtual(serverId ?? '', id);
1356
+ const virtualId = toVirtual(serverId, id);
1326
1357
  if (isVirtual(virtualId))
1327
1358
  minted.add(virtualId);
1328
1359
  return virtualId;
@@ -1571,6 +1602,9 @@ export function installPluginRouteIntercept(options) {
1571
1602
  };
1572
1603
  // ---- the remote pipe: the PB-10 tunnel, frame by frame -------------
1573
1604
  let tunnelHandle;
1605
+ // MS-4: the server the tunnel was opened on — set together with
1606
+ // tunnelHandle, the relay every later send and the close travel.
1607
+ let tunnelServer;
1574
1608
  // The connection-wide real→virtual mapping the rewriteIds deep scan
1575
1609
  // consults: EVERY validated remote frame's ids merge in (a watch list
1576
1610
  // that grows mid-connection makes the later answers rewritable too —
@@ -1591,7 +1625,7 @@ export function installPluginRouteIntercept(options) {
1591
1625
  * never a silent broken flag: that would leak `tunnelsCurrent` and
1592
1626
  * leave the browser hanging on a dead tunnel. */
1593
1627
  const sendTextUpstream = async (frame, parsedJson, hits) => {
1594
- if (upstreamBroken || tunnelHandle === undefined)
1628
+ if (upstreamBroken || tunnelHandle === undefined || tunnelServer === undefined)
1595
1629
  return;
1596
1630
  const rewrite = hits.length > 0
1597
1631
  ? rewriteAdapterRequest(declared.view, declared.path, new URLSearchParams(), parsedJson, hits, (virtualId) => fromVirtual(virtualId).id)
@@ -1600,7 +1634,7 @@ export function installPluginRouteIntercept(options) {
1600
1634
  const seq = upstreamSeq;
1601
1635
  upstreamSeq += 1;
1602
1636
  try {
1603
- await relay.pluginTunnelSend(tunnelHandle.tunnel, seq, encodeRfc6455Frame(0x1, payload, true, frame.fin));
1637
+ await tunnelServer.relay.pluginTunnelSend(tunnelHandle.tunnel, seq, encodeRfc6455Frame(0x1, payload, true, frame.fin));
1604
1638
  }
1605
1639
  catch {
1606
1640
  killExchange();
@@ -1652,7 +1686,7 @@ export function installPluginRouteIntercept(options) {
1652
1686
  hooked = true;
1653
1687
  }
1654
1688
  else if (declared.rewriteIds) {
1655
- const scanned = deepRewriteIds(parsedJson, idMapping, getKnownRealIds?.(), serverId);
1689
+ const scanned = deepRewriteIds(parsedJson, idMapping, knownRealIdsOfServer(serverId), serverId);
1656
1690
  if (declared.rewriteFields !== undefined)
1657
1691
  rewriteIdsAtFields(scanned, declared.rewriteFields, serverId);
1658
1692
  outPayload = Buffer.from(JSON.stringify(scanned), 'utf8');
@@ -1761,25 +1795,19 @@ export function installPluginRouteIntercept(options) {
1761
1795
  }
1762
1796
  return;
1763
1797
  }
1764
- const serverId = getServerId();
1765
- if (serverId === undefined) {
1798
+ // MS-4: the frame's ids name one served server, and once the tunnel
1799
+ // is open every later frame must name the tunnel's own server.
1800
+ const home = serverOfHits(verdict.hits);
1801
+ if (home === 'bad-query' || home === 'server-changed' || (tunnelServer !== undefined && home.serverId !== tunnelServer.serverId)) {
1766
1802
  refuseAndDie('server-changed');
1767
1803
  return;
1768
1804
  }
1769
- const mapping = new Map();
1770
- for (const idHit of verdict.hits) {
1771
- const parts = fromVirtual(idHit.value);
1772
- if (parts === undefined || parts.id === '' || parts.serverId !== serverId) {
1773
- refuseAndDie('server-changed');
1774
- return;
1775
- }
1776
- mapping.set(parts.id, idHit.value);
1777
- }
1805
+ const serverId = home.serverId;
1778
1806
  // This frame's ids join the connection-wide mapping (see idMapping).
1779
- for (const [key, value] of mapping)
1780
- idMapping.set(key, value);
1807
+ for (const idHit of verdict.hits)
1808
+ idMapping.set(fromVirtual(idHit.value).id, idHit.value);
1781
1809
  if (tunnelHandle === undefined) {
1782
- const status = getStatus().find((entry) => entry.plugin === plugin);
1810
+ const status = statusesOfServer(serverId).find((entry) => entry.plugin === plugin);
1783
1811
  if (status === undefined || !status.bridged) {
1784
1812
  refuseAndDie(status?.reason ?? 'not-on-server');
1785
1813
  return;
@@ -1789,7 +1817,7 @@ export function installPluginRouteIntercept(options) {
1789
1817
  let opened;
1790
1818
  try {
1791
1819
  log?.('opening tunnel for %s', declared.path);
1792
- opened = await relay.pluginTunnelOpen({
1820
+ opened = await home.relay.pluginTunnelOpen({
1793
1821
  plugin,
1794
1822
  version: declared.version,
1795
1823
  digest: declared.digest,
@@ -1805,6 +1833,7 @@ export function installPluginRouteIntercept(options) {
1805
1833
  }
1806
1834
  tunnelsCurrent += 1;
1807
1835
  tunnelsOpened += 1;
1836
+ tunnelServer = home;
1808
1837
  tunnelHandle = opened;
1809
1838
  log?.('tunnel open for %s: %s', declared.path, opened.tunnel);
1810
1839
  teardownTunnel = () => {
@@ -1813,7 +1842,7 @@ export function installPluginRouteIntercept(options) {
1813
1842
  upstreamBroken = true;
1814
1843
  tunnelsCurrent -= 1;
1815
1844
  tunnelsClosed += 1;
1816
- void relay.pluginTunnelClose(opened.tunnel).catch(() => { });
1845
+ void home.relay.pluginTunnelClose(opened.tunnel).catch(() => { });
1817
1846
  };
1818
1847
  socket.on('close', teardownTunnel);
1819
1848
  socket.on('error', teardownTunnel);
@@ -1832,11 +1861,11 @@ export function installPluginRouteIntercept(options) {
1832
1861
  localSocket.push(frame.raw);
1833
1862
  return;
1834
1863
  }
1835
- if (tunnelHandle !== undefined && !upstreamBroken) {
1864
+ if (tunnelHandle !== undefined && tunnelServer !== undefined && !upstreamBroken) {
1836
1865
  const seq = upstreamSeq;
1837
1866
  upstreamSeq += 1;
1838
1867
  try {
1839
- await relay.pluginTunnelSend(tunnelHandle.tunnel, seq, frame.raw);
1868
+ await tunnelServer.relay.pluginTunnelSend(tunnelHandle.tunnel, seq, frame.raw);
1840
1869
  }
1841
1870
  catch {
1842
1871
  // The send died: tear the exchange down COMPLETELY — the
@@ -1948,7 +1977,8 @@ export function installPluginRouteIntercept(options) {
1948
1977
  answerUnbridgedSocket(socket, declared.plugin, 'bad-query');
1949
1978
  return;
1950
1979
  }
1951
- if (parts.serverId !== getServerId()) {
1980
+ const tunnelRelay = relayOfServer(parts.serverId);
1981
+ if (tunnelRelay === undefined) {
1952
1982
  answerUnbridgedSocket(socket, declared.plugin, 'server-changed');
1953
1983
  return;
1954
1984
  }
@@ -1956,7 +1986,7 @@ export function installPluginRouteIntercept(options) {
1956
1986
  // the tunnel opens only for a plugin bridged RIGHT NOW — an absent
1957
1987
  // or unbridged status refuses with its own reason, and the local
1958
1988
  // plugin handler is never reached.
1959
- const bridgeStatus = getStatus().find((entry) => entry.plugin === declared.plugin);
1989
+ const bridgeStatus = statusesOfServer(parts.serverId).find((entry) => entry.plugin === declared.plugin);
1960
1990
  if (bridgeStatus === undefined || !bridgeStatus.bridged) {
1961
1991
  answerUnbridgedSocket(socket, declared.plugin, bridgeStatus?.reason ?? 'not-on-server');
1962
1992
  return;
@@ -1977,7 +2007,7 @@ export function installPluginRouteIntercept(options) {
1977
2007
  let tunnel;
1978
2008
  try {
1979
2009
  log?.('opening tunnel for %s', declared.path);
1980
- tunnel = await relay.pluginTunnelOpen({
2010
+ tunnel = await tunnelRelay.pluginTunnelOpen({
1981
2011
  plugin: declared.plugin,
1982
2012
  version: declared.version,
1983
2013
  digest: declared.digest,
@@ -2058,7 +2088,7 @@ export function installPluginRouteIntercept(options) {
2058
2088
  if (upstreamBroken)
2059
2089
  return;
2060
2090
  try {
2061
- await relay.pluginTunnelSend(tunnel.tunnel, seq, bytes, controller.signal);
2091
+ await tunnelRelay.pluginTunnelSend(tunnel.tunnel, seq, bytes, controller.signal);
2062
2092
  }
2063
2093
  catch {
2064
2094
  upstreamBroken = true;
@@ -2070,7 +2100,7 @@ export function installPluginRouteIntercept(options) {
2070
2100
  upstreamBroken = true;
2071
2101
  tunnelsCurrent -= 1;
2072
2102
  tunnelsClosed += 1;
2073
- void relay.pluginTunnelClose(tunnel.tunnel).catch(() => { });
2103
+ void tunnelRelay.pluginTunnelClose(tunnel.tunnel).catch(() => { });
2074
2104
  };
2075
2105
  socket.on('close', teardownTunnel);
2076
2106
  socket.on('error', teardownTunnel);