@linxin666/dsh-remote-web-ui 0.3.6 → 0.3.9

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 (238) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +77 -468
  3. package/README.zh.md +83 -113
  4. package/lib/client.js +955 -203
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +1136 -1466
  7. package/lib/types/client/FooterRemoteEntry.d.ts +6 -7
  8. package/lib/types/client/FooterRemoteEntry.d.ts.map +1 -1
  9. package/lib/types/client/FooterRemoteEntry.js +1 -1
  10. package/lib/types/client/RemoteEntry.d.ts +7 -4
  11. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  12. package/lib/types/client/RemoteEntry.js +7 -10
  13. package/lib/types/client/RemotePanel.d.ts +2 -2
  14. package/lib/types/client/RemotePanel.d.ts.map +1 -1
  15. package/lib/types/client/RemotePanel.js +2 -2
  16. package/lib/types/client/RemoteSettingsCard.d.ts +9 -5
  17. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  18. package/lib/types/client/RemoteSettingsCard.js +56 -3
  19. package/lib/types/client/deep-link.d.ts +10 -13
  20. package/lib/types/client/deep-link.d.ts.map +1 -1
  21. package/lib/types/client/deep-link.js +14 -74
  22. package/lib/types/client/index.d.ts +2 -6
  23. package/lib/types/client/index.d.ts.map +1 -1
  24. package/lib/types/client/index.js +73 -39
  25. package/lib/types/client/locales.d.ts +26 -12
  26. package/lib/types/client/locales.d.ts.map +1 -1
  27. package/lib/types/client/locales.js +36 -22
  28. package/lib/types/client/mobile-adapt.d.ts +40 -0
  29. package/lib/types/client/mobile-adapt.d.ts.map +1 -0
  30. package/lib/types/client/mobile-adapt.js +967 -0
  31. package/lib/types/client/pair-api.d.ts +24 -6
  32. package/lib/types/client/pair-api.d.ts.map +1 -1
  33. package/lib/types/client/pair-api.js +9 -14
  34. package/lib/types/client/remote-channel.d.ts +3 -0
  35. package/lib/types/client/remote-channel.d.ts.map +1 -1
  36. package/lib/types/client/remote-channel.js +35 -3
  37. package/lib/types/client/settings-form.d.ts +35 -43
  38. package/lib/types/client/settings-form.d.ts.map +1 -1
  39. package/lib/types/client/settings-form.js +60 -53
  40. package/lib/types/firewall.d.ts +75 -0
  41. package/lib/types/firewall.d.ts.map +1 -0
  42. package/lib/types/firewall.js +184 -0
  43. package/lib/types/gate.d.ts +11 -5
  44. package/lib/types/gate.d.ts.map +1 -1
  45. package/lib/types/gate.js +11 -5
  46. package/lib/types/index.d.ts +39 -10
  47. package/lib/types/index.d.ts.map +1 -1
  48. package/lib/types/index.js +206 -58
  49. package/lib/types/inner-auth.d.ts +32 -0
  50. package/lib/types/inner-auth.d.ts.map +1 -0
  51. package/lib/types/inner-auth.js +72 -0
  52. package/lib/types/lan-bind-plan.d.ts +55 -0
  53. package/lib/types/lan-bind-plan.d.ts.map +1 -0
  54. package/lib/types/lan-bind-plan.js +60 -0
  55. package/lib/types/lan-bind.d.ts +69 -0
  56. package/lib/types/lan-bind.d.ts.map +1 -0
  57. package/lib/types/lan-bind.js +128 -0
  58. package/lib/types/loopback-proxy.d.ts +9 -2
  59. package/lib/types/loopback-proxy.d.ts.map +1 -1
  60. package/lib/types/loopback-proxy.js +24 -2
  61. package/lib/types/pairing.d.ts +15 -3
  62. package/lib/types/pairing.d.ts.map +1 -1
  63. package/lib/types/pairing.js +20 -6
  64. package/lib/types/remote-api.d.ts +30 -13
  65. package/lib/types/remote-api.d.ts.map +1 -1
  66. package/lib/types/remote-api.js +55 -42
  67. package/lib/types/remote-channel-boot.d.ts +21 -2
  68. package/lib/types/remote-channel-boot.d.ts.map +1 -1
  69. package/lib/types/remote-channel-boot.js +65 -3
  70. package/lib/types/remote-channel-rules.d.ts +8 -0
  71. package/lib/types/remote-channel-rules.d.ts.map +1 -1
  72. package/lib/types/remote-channel-rules.js +9 -2
  73. package/lib/types/remote-methods.d.ts +45 -11
  74. package/lib/types/remote-methods.d.ts.map +1 -1
  75. package/lib/types/remote-methods.js +56 -27
  76. package/lib/types/remote-presence-pet.d.ts +51 -0
  77. package/lib/types/remote-presence-pet.d.ts.map +1 -0
  78. package/lib/types/remote-presence-pet.js +101 -0
  79. package/lib/types/routes.d.ts +76 -4
  80. package/lib/types/routes.d.ts.map +1 -1
  81. package/lib/types/routes.js +336 -19
  82. package/lib/types/tunnel.d.ts.map +1 -1
  83. package/lib/types/tunnel.js +7 -1
  84. package/package.json +20 -18
  85. package/src/client/FooterRemoteEntry.tsx +9 -10
  86. package/src/client/RemoteEntry.tsx +13 -14
  87. package/src/client/RemotePanel.tsx +5 -16
  88. package/src/client/RemoteSettingsCard.tsx +72 -13
  89. package/src/client/deep-link.ts +14 -75
  90. package/src/client/index.ts +77 -39
  91. package/src/client/locales.ts +36 -22
  92. package/src/client/mobile-adapt.ts +913 -0
  93. package/src/client/pair-api.ts +26 -15
  94. package/src/client/remote-channel.ts +31 -3
  95. package/src/client/settings-form.ts +78 -92
  96. package/src/dsh-home.ts +1 -1
  97. package/src/firewall.ts +230 -0
  98. package/src/gate.ts +11 -5
  99. package/src/index.ts +235 -65
  100. package/src/inner-auth.ts +85 -0
  101. package/src/lan-bind-plan.ts +81 -0
  102. package/src/lan-bind.ts +145 -0
  103. package/src/loopback-proxy.ts +34 -0
  104. package/src/pairing.persist.test.ts +1 -1
  105. package/src/pairing.ts +20 -6
  106. package/src/remote-api.ts +67 -45
  107. package/src/remote-channel-boot.ts +70 -3
  108. package/src/remote-channel-rules.ts +17 -2
  109. package/src/remote-methods.ts +58 -27
  110. package/src/remote-presence-pet.ts +129 -0
  111. package/src/routes.ts +358 -19
  112. package/src/tunnel.ts +7 -1
  113. package/assets/apple-touch-icon.png +0 -0
  114. package/assets/mobile-icon-192.png +0 -0
  115. package/assets/mobile-icon-512.png +0 -0
  116. package/assets/mobile-service-worker.js +0 -63
  117. package/lib/mobile.js +0 -17658
  118. package/lib/mobile.js.map +0 -1
  119. package/lib/types/mobile/PairRequiredView.d.ts +0 -7
  120. package/lib/types/mobile/PairRequiredView.d.ts.map +0 -1
  121. package/lib/types/mobile/PairRequiredView.js +0 -28
  122. package/lib/types/mobile/PairRequiredView.test.d.ts +0 -2
  123. package/lib/types/mobile/PairRequiredView.test.d.ts.map +0 -1
  124. package/lib/types/mobile/PairRequiredView.test.js +0 -24
  125. package/lib/types/mobile/api.d.ts +0 -145
  126. package/lib/types/mobile/api.d.ts.map +0 -1
  127. package/lib/types/mobile/api.js +0 -103
  128. package/lib/types/mobile/index.d.ts +0 -10
  129. package/lib/types/mobile/index.d.ts.map +0 -1
  130. package/lib/types/mobile/index.js +0 -38
  131. package/lib/types/mobile/markdown.d.ts +0 -34
  132. package/lib/types/mobile/markdown.d.ts.map +0 -1
  133. package/lib/types/mobile/markdown.js +0 -285
  134. package/lib/types/mobile/markdown.test.d.ts +0 -2
  135. package/lib/types/mobile/markdown.test.d.ts.map +0 -1
  136. package/lib/types/mobile/markdown.test.js +0 -79
  137. package/lib/types/mobile/messages.d.ts +0 -149
  138. package/lib/types/mobile/messages.d.ts.map +0 -1
  139. package/lib/types/mobile/messages.js +0 -561
  140. package/lib/types/mobile/messages.test.d.ts +0 -2
  141. package/lib/types/mobile/messages.test.d.ts.map +0 -1
  142. package/lib/types/mobile/messages.test.js +0 -356
  143. package/lib/types/mobile/mobile-styles.d.ts +0 -6
  144. package/lib/types/mobile/mobile-styles.d.ts.map +0 -1
  145. package/lib/types/mobile/mobile-styles.js +0 -1438
  146. package/lib/types/mobile/mobile-theme.d.ts +0 -22
  147. package/lib/types/mobile/mobile-theme.d.ts.map +0 -1
  148. package/lib/types/mobile/mobile-theme.js +0 -70
  149. package/lib/types/mobile/mobile-theme.test.d.ts +0 -2
  150. package/lib/types/mobile/mobile-theme.test.d.ts.map +0 -1
  151. package/lib/types/mobile/mobile-theme.test.js +0 -71
  152. package/lib/types/mobile/mux.d.ts +0 -128
  153. package/lib/types/mobile/mux.d.ts.map +0 -1
  154. package/lib/types/mobile/mux.js +0 -305
  155. package/lib/types/mobile/mux.test.d.ts +0 -2
  156. package/lib/types/mobile/mux.test.d.ts.map +0 -1
  157. package/lib/types/mobile/mux.test.js +0 -267
  158. package/lib/types/mobile/pairing.d.ts +0 -31
  159. package/lib/types/mobile/pairing.d.ts.map +0 -1
  160. package/lib/types/mobile/pairing.js +0 -62
  161. package/lib/types/mobile/rpc.d.ts +0 -38
  162. package/lib/types/mobile/rpc.d.ts.map +0 -1
  163. package/lib/types/mobile/rpc.js +0 -83
  164. package/lib/types/mobile/theme-toggle.d.ts +0 -12
  165. package/lib/types/mobile/theme-toggle.d.ts.map +0 -1
  166. package/lib/types/mobile/theme-toggle.js +0 -26
  167. package/lib/types/mobile/views/App.d.ts +0 -57
  168. package/lib/types/mobile/views/App.d.ts.map +0 -1
  169. package/lib/types/mobile/views/App.js +0 -150
  170. package/lib/types/mobile/views/App.test.d.ts +0 -2
  171. package/lib/types/mobile/views/App.test.d.ts.map +0 -1
  172. package/lib/types/mobile/views/App.test.js +0 -52
  173. package/lib/types/mobile/views/ChatView.d.ts +0 -63
  174. package/lib/types/mobile/views/ChatView.d.ts.map +0 -1
  175. package/lib/types/mobile/views/ChatView.js +0 -821
  176. package/lib/types/mobile/views/ChatView.stream.test.d.ts +0 -2
  177. package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +0 -1
  178. package/lib/types/mobile/views/ChatView.stream.test.js +0 -236
  179. package/lib/types/mobile/views/ChatView.test.d.ts +0 -2
  180. package/lib/types/mobile/views/ChatView.test.d.ts.map +0 -1
  181. package/lib/types/mobile/views/ChatView.test.js +0 -718
  182. package/lib/types/mobile/views/SessionListView.d.ts +0 -27
  183. package/lib/types/mobile/views/SessionListView.d.ts.map +0 -1
  184. package/lib/types/mobile/views/SessionListView.js +0 -139
  185. package/lib/types/mobile/views/SessionListView.test.d.ts +0 -2
  186. package/lib/types/mobile/views/SessionListView.test.d.ts.map +0 -1
  187. package/lib/types/mobile/views/SessionListView.test.js +0 -167
  188. package/lib/types/mobile/views/WorkspaceView.d.ts +0 -21
  189. package/lib/types/mobile/views/WorkspaceView.d.ts.map +0 -1
  190. package/lib/types/mobile/views/WorkspaceView.js +0 -93
  191. package/lib/types/mobile/views/WorkspaceView.test.d.ts +0 -2
  192. package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +0 -1
  193. package/lib/types/mobile/views/WorkspaceView.test.js +0 -183
  194. package/lib/types/mobile-api.d.ts +0 -65
  195. package/lib/types/mobile-api.d.ts.map +0 -1
  196. package/lib/types/mobile-api.js +0 -404
  197. package/lib/types/mobile-pending.d.ts +0 -38
  198. package/lib/types/mobile-pending.d.ts.map +0 -1
  199. package/lib/types/mobile-pending.js +0 -59
  200. package/lib/types/mobile-pwa.d.ts +0 -6
  201. package/lib/types/mobile-pwa.d.ts.map +0 -1
  202. package/lib/types/mobile-pwa.js +0 -17
  203. package/lib/types/mobile-routes.d.ts +0 -13
  204. package/lib/types/mobile-routes.d.ts.map +0 -1
  205. package/lib/types/mobile-routes.js +0 -217
  206. package/lib/types/paired-model-catalog.d.ts +0 -17
  207. package/lib/types/paired-model-catalog.d.ts.map +0 -1
  208. package/lib/types/paired-model-catalog.js +0 -311
  209. package/src/mobile/PairRequiredView.test.tsx +0 -28
  210. package/src/mobile/PairRequiredView.tsx +0 -58
  211. package/src/mobile/api.ts +0 -229
  212. package/src/mobile/index.tsx +0 -41
  213. package/src/mobile/markdown.test.ts +0 -91
  214. package/src/mobile/markdown.ts +0 -291
  215. package/src/mobile/messages.test.ts +0 -402
  216. package/src/mobile/messages.ts +0 -685
  217. package/src/mobile/mobile-styles.ts +0 -1439
  218. package/src/mobile/mobile-theme.test.ts +0 -84
  219. package/src/mobile/mobile-theme.ts +0 -76
  220. package/src/mobile/mux.test.ts +0 -314
  221. package/src/mobile/mux.ts +0 -336
  222. package/src/mobile/pairing.ts +0 -77
  223. package/src/mobile/rpc.ts +0 -90
  224. package/src/mobile/theme-toggle.tsx +0 -47
  225. package/src/mobile/views/App.test.tsx +0 -62
  226. package/src/mobile/views/App.tsx +0 -213
  227. package/src/mobile/views/ChatView.stream.test.tsx +0 -268
  228. package/src/mobile/views/ChatView.test.tsx +0 -816
  229. package/src/mobile/views/ChatView.tsx +0 -1342
  230. package/src/mobile/views/SessionListView.test.tsx +0 -207
  231. package/src/mobile/views/SessionListView.tsx +0 -244
  232. package/src/mobile/views/WorkspaceView.test.tsx +0 -227
  233. package/src/mobile/views/WorkspaceView.tsx +0 -245
  234. package/src/mobile-api.ts +0 -418
  235. package/src/mobile-pending.ts +0 -88
  236. package/src/mobile-pwa.ts +0 -19
  237. package/src/mobile-routes.ts +0 -231
  238. package/src/paired-model-catalog.ts +0 -272
@@ -1,21 +1,21 @@
1
1
  /**
2
2
  * The remote desktop data channel: `/remote` is this plugin's own prefix, so
3
- * the paired-device cookie is the access control (exactly like `/m/api`).
4
- * After that gate, every fenced same-origin path the browser rewrote here is
5
- * re-issued to 127.0.0.1 as a loopback-shaped request so sibling plugin
6
- * fences (and the connection plugin's `/api`) accept it — no `--trusted-host`
7
- * and no per-plugin pairing consult.
3
+ * the paired-device cookie is the access control. After that gate, every
4
+ * fenced same-origin path the browser rewrote here is re-issued to 127.0.0.1
5
+ * as a loopback-shaped request so sibling plugin fences (and the connection
6
+ * plugin's `/api`) accept it — no `--trusted-host` and no per-plugin pairing
7
+ * consult.
8
8
  *
9
9
  * Security model:
10
10
  * - While `requirePairingForLan` is on (default), every request must carry a
11
11
  * live paired-device cookie, enforced before any bytes are forwarded and
12
12
  * before any host call. With the policy off, the cookie gate is skipped
13
- * (the loopback-only denials below still apply).
14
- * - The SDK's loopback-only privileged methods (native dialogs, the settings
15
- * plane, credentials — the `PRIVILEGED_METHODS` set of client-connection)
16
- * are denied here. The set is pinned by tests/remote-contract.spec.ts.
17
- * - `/api/pair/*`, `/api/update/*`, `/api/plugin-manager/*`,
18
- * `/api/dsh-desktop-launcher/*` and `/api/dsh-web-ui-settings/*` stay physically local.
13
+ * (the local-only denials below still apply).
14
+ * - A paired remote desktop is a full-control credential: the browser half
15
+ * flips the official UI into host mode (the transport ownsHost hook), so
16
+ * the configuration plane (settings, credentials, presets, deliverables)
17
+ * rides this channel like every other call. The four control planes in
18
+ * LOCAL_ONLY_PREFIXES stay physically local.
19
19
  * - Everything else is HTTP- or WebSocket-proxied to the local port with
20
20
  * Host rewritten, Origin and cookies dropped, and a synthetic same-origin
21
21
  * browser marker added after authentication. Plugin loopback fences then
@@ -24,8 +24,8 @@
24
24
  import { readCookie } from "./gate.js";
25
25
  import { writeJson } from "./http.js";
26
26
  import { proxyLoopbackHttp, proxyLoopbackUpgrade } from "./loopback-proxy.js";
27
- import { DESKTOP_LAUNCHER_PATH, LOOPBACK_ONLY_METHODS, PLUGIN_MANAGER_PATH, REMOTE_PREFIX, REMOTE_UPGRADE_PATHS, WEB_UI_SETTINGS_BRIDGE_PATH, } from "./remote-methods.js";
28
- export { DESKTOP_LAUNCHER_PATH, LOOPBACK_ONLY_METHODS, PLUGIN_MANAGER_PATH, REMOTE_API_PATHS, REMOTE_PREFIX, REMOTE_UPGRADE_PATHS, WEB_UI_SETTINGS_BRIDGE_PATH, } from "./remote-methods.js";
27
+ import { REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY, REMOTE_PREFIX, REMOTE_UPGRADE_PATHS, localOnlyDenial, } from "./remote-methods.js";
28
+ export { DESKTOP_LAUNCHER_PATH, LOCAL_ONLY_PREFIXES, PLUGIN_MANAGER_PATH, REMOTE_API_PATHS, REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY, REMOTE_PREFIX, REMOTE_UPGRADE_PATHS, WEB_UI_SETTINGS_BRIDGE_PATH, localOnlyDenial, } from "./remote-methods.js";
29
29
  export { REMOTE_API_PREFIX } from "./remote-methods.js";
30
30
  const ALLOWED_METHODS = new Set(['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']);
31
31
  /** Reject traversal and empty segments; allow plugin file-path characters. */
@@ -68,32 +68,28 @@ export function innerPathOf(pathname) {
68
68
  return rest;
69
69
  }
70
70
  /**
71
- * Whether a paired inner path must stay physically local.
71
+ * Whether a paired inner path must stay physically local (delegates to the
72
+ * shared LOCAL_ONLY_PREFIXES table).
72
73
  * @returns a denial message, or undefined when the path may be proxied.
73
74
  */
74
75
  export function loopbackOnlyDenial(innerPath) {
75
- if (innerPath === '/api/pair' || innerPath.startsWith('/api/pair/')) {
76
- return 'pairing endpoints stay loopback-only and stay unreachable from a paired remote desktop';
77
- }
78
- if (innerPath === '/api/update' || innerPath.startsWith('/api/update/')) {
79
- return 'update endpoints stay loopback-only and stay unreachable from a paired remote desktop';
80
- }
81
- if (innerPath === PLUGIN_MANAGER_PATH || innerPath.startsWith(`${PLUGIN_MANAGER_PATH}/`)) {
82
- return 'plugin-manager stays loopback-only and stays unreachable from a paired remote desktop';
83
- }
84
- if (innerPath === DESKTOP_LAUNCHER_PATH || innerPath.startsWith(`${DESKTOP_LAUNCHER_PATH}/`)) {
85
- return 'desktop-launcher endpoints stay loopback-only and stay unreachable from a paired remote desktop';
86
- }
87
- if (innerPath === WEB_UI_SETTINGS_BRIDGE_PATH || innerPath.startsWith(`${WEB_UI_SETTINGS_BRIDGE_PATH}/`)) {
88
- return 'settings-bridge endpoints stay loopback-only and stay unreachable from a paired remote desktop';
89
- }
90
- if (!innerPath.startsWith('/api/'))
76
+ return localOnlyDenial(innerPath);
77
+ }
78
+ /**
79
+ * Resolve a live device credential for a gated HTTP request: the pairing
80
+ * cookie first, then the cookieless header the boot patch attaches. Unknown
81
+ * or revoked ids are a no-op - a stale id never re-arms a device.
82
+ * @returns the touched device id, or undefined when neither credential is live.
83
+ */
84
+ export function pairedDeviceIdOf(req, service) {
85
+ const cookieDevice = readCookie(req.headers.cookie, service.config.cookieName);
86
+ const headerDevice = typeof req.headers[REMOTE_DEVICE_HEADER] === 'string'
87
+ ? req.headers[REMOTE_DEVICE_HEADER]
88
+ : undefined;
89
+ const id = cookieDevice ?? headerDevice;
90
+ if (id === undefined)
91
91
  return undefined;
92
- const method = innerPath.slice('/api/'.length);
93
- if (method !== '' && !method.includes('/') && LOOPBACK_ONLY_METHODS.has(method)) {
94
- return `${method} is loopback-only and stays unreachable from a paired remote desktop`;
95
- }
96
- return undefined;
92
+ return service.touchDevice(id) ? id : undefined;
97
93
  }
98
94
  /**
99
95
  * Build the remote desktop channel HTTP routes.
@@ -108,9 +104,8 @@ export function makeRemoteApiRoutes(deps) {
108
104
  // (a stale client rewrite must not 403); loopback-only denials stay below.
109
105
  const require = typeof requirePairingForLan === 'function' ? requirePairingForLan() : requirePairingForLan;
110
106
  if (require) {
111
- const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
112
- const paired = deviceId !== undefined && service.touchDevice(deviceId);
113
- if (!paired) {
107
+ const paired = pairedDeviceIdOf(req, service);
108
+ if (paired === undefined) {
114
109
  req.resume();
115
110
  envelopeError(res, 403, 'invalid-request', 'unpaired', 'this device is not paired with the desktop');
116
111
  return;
@@ -135,7 +130,7 @@ export function makeRemoteApiRoutes(deps) {
135
130
  envelopeError(res, 403, 'invalid-request', 'forbidden', denied);
136
131
  return;
137
132
  }
138
- proxyLoopbackHttp(req, res, port, `${inner}${url.search}`);
133
+ proxyLoopbackHttp(req, res, port, `${inner}${url.search}`, deps.auth);
139
134
  };
140
135
  return [{ kind: 'prefix', path: REMOTE_PREFIX, handler }];
141
136
  }
@@ -168,8 +163,16 @@ export function makeRemoteApiUpgradeRoutes(deps) {
168
163
  const handlerFor = (fallbackPath) => (req, socket, head) => {
169
164
  const require = typeof requirePairingForLan === 'function' ? requirePairingForLan() : requirePairingForLan;
170
165
  if (require) {
171
- const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
172
- if (deviceId === undefined || !service.touchDevice(deviceId)) {
166
+ // WebSocket handshakes cannot carry headers from the Web API, so the
167
+ // cookieless credential rides the query; the cookie stays the primary.
168
+ let queryDevice;
169
+ try {
170
+ queryDevice = new URL(req.url ?? '/', 'http://127.0.0.1').searchParams.get(REMOTE_DEVICE_QUERY) ?? undefined;
171
+ }
172
+ catch { /* fall through to the cookie */ }
173
+ const deviceId = pairedDeviceIdOf(req, service);
174
+ const paired = deviceId ?? (queryDevice !== undefined && service.touchDevice(queryDevice) ? queryDevice : undefined);
175
+ if (paired === undefined) {
173
176
  socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
174
177
  socket.destroy();
175
178
  return;
@@ -182,7 +185,17 @@ export function makeRemoteApiUpgradeRoutes(deps) {
182
185
  socket.destroy();
183
186
  return;
184
187
  }
185
- proxyLoopbackUpgrade(req, socket, head, port, inner);
188
+ // The inner handshake needs the browser-auth credential (the gateway
189
+ // event-stream route enforces it); resolving it is async, so the
190
+ // handshake bytes are written once it settles. A missing credential
191
+ // proceeds as before — the gateway route then refuses.
192
+ void Promise.resolve(deps.auth?.ready())
193
+ .catch(() => undefined)
194
+ .then((cookie) => {
195
+ if (socket.destroyed)
196
+ return;
197
+ proxyLoopbackUpgrade(req, socket, head, port, inner, typeof cookie === 'string' ? cookie : undefined);
198
+ });
186
199
  };
187
200
  return REMOTE_UPGRADE_PATHS.map((path) => ({
188
201
  path,
@@ -10,8 +10,15 @@
10
10
  * (hooks + pending unpaired signal) instead of patching twice.
11
11
  *
12
12
  * The rewrite decisions are generated from REMOTE_CHANNEL_RULES, the same
13
- * data the browser patch consumes — the two cannot drift apart. The script
14
- * self-skips on loopback origins and never throws.
13
+ * data the browser patch consumes — the two cannot drift apart. On this
14
+ * 0.1.2-alpha.2 line the "configuration plane is local" behavior lives in
15
+ * the browser (client plugins branch on connection.isLoopback), so the
16
+ * script also flips the official UI into host mode by installing the
17
+ * transport hook `__DSH_TRANSPORT__ = { ownsHost: true }` before the
18
+ * connection plugin reads it: the paired remote desktop gets the full
19
+ * settings/credentials/presets surface, and every call still rides the
20
+ * gated /remote channel. The script self-skips on loopback origins and
21
+ * never throws.
15
22
  * @module @linxin666/dsh-remote-web-ui/remote-channel-boot
16
23
  */
17
24
  import { type RemoteChannelRules } from './remote-channel-rules.ts';
@@ -21,6 +28,18 @@ import { type RemoteChannelRules } from './remote-channel-rules.ts';
21
28
  * the window global.
22
29
  */
23
30
  export declare function buildRemoteChannelBootScript(rules?: RemoteChannelRules): string;
31
+ /** Boot-watchdog latch: one self-reload per session while the boot is broken. */
32
+ export declare const BOOT_WATCHDOG_KEY = "dsh-remote-boot-reload";
33
+ /**
34
+ * Build the boot-watchdog script fragment. A remote boot has no recovery
35
+ * path of its own: the SPA mounts nothing when a boot-critical request dies
36
+ * (a tunnel-edge 429 under burst, a dropped stream, a boot-order race), and
37
+ * the phone then shows a permanently blank shell. The watchdog polls for the
38
+ * app's conversation surface and reloads once when it never appears; the
39
+ * sessionStorage latch keeps a genuinely broken deployment from looping,
40
+ * and a successful boot clears the latch so a later failure can recover.
41
+ */
42
+ export declare function buildBootWatchdogScript(waitMs?: number, pollMs?: number): string;
24
43
  /** The script this plugin contributes; built once from the live rules. */
25
44
  export declare const REMOTE_CHANNEL_BOOT_SCRIPT: string;
26
45
  //# sourceMappingURL=remote-channel-boot.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"remote-channel-boot.d.ts","sourceRoot":"","sources":["../../src/remote-channel-boot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAoD,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAErH;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,GAAE,kBAAyC,GAAG,MAAM,CAuGrG;AAED,0EAA0E;AAC1E,eAAO,MAAM,0BAA0B,EAAE,MAAuC,CAAA"}
1
+ {"version":3,"file":"remote-channel-boot.d.ts","sourceRoot":"","sources":["../../src/remote-channel-boot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAoD,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAErH;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,GAAE,kBAAyC,GAAG,MAAM,CA+HrG;AAED,iFAAiF;AACjF,eAAO,MAAM,iBAAiB,2BAA2B,CAAA;AAQzD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,SAAwB,EAAE,MAAM,SAAwB,GAAG,MAAM,CAgB9G;AAED,0EAA0E;AAC1E,eAAO,MAAM,0BAA0B,EAAE,MAAuC,CAAA"}
@@ -10,8 +10,15 @@
10
10
  * (hooks + pending unpaired signal) instead of patching twice.
11
11
  *
12
12
  * The rewrite decisions are generated from REMOTE_CHANNEL_RULES, the same
13
- * data the browser patch consumes — the two cannot drift apart. The script
14
- * self-skips on loopback origins and never throws.
13
+ * data the browser patch consumes — the two cannot drift apart. On this
14
+ * 0.1.2-alpha.2 line the "configuration plane is local" behavior lives in
15
+ * the browser (client plugins branch on connection.isLoopback), so the
16
+ * script also flips the official UI into host mode by installing the
17
+ * transport hook `__DSH_TRANSPORT__ = { ownsHost: true }` before the
18
+ * connection plugin reads it: the paired remote desktop gets the full
19
+ * settings/credentials/presets surface, and every call still rides the
20
+ * gated /remote channel. The script self-skips on loopback origins and
21
+ * never throws.
15
22
  * @module @linxin666/dsh-remote-web-ui/remote-channel-boot
16
23
  */
17
24
  import { REMOTE_CHANNEL_BOOT_GLOBAL, REMOTE_CHANNEL_RULES } from "./remote-channel-rules.js";
@@ -30,7 +37,23 @@ export function buildRemoteChannelBootScript(rules = REMOTE_CHANNEL_RULES) {
30
37
  'var w=window,loc=w.location,h=loc.hostname;' +
31
38
  // Loopback origins keep the original paths (mirrors isLoopbackHostname).
32
39
  "if(h==='localhost'||h==='::1'||/^127(\\.\\d{1,3}){3}$/.test(h))return;" +
40
+ // Host mode: the paired remote desktop presents itself as the machine
41
+ // owner, so the official UI keeps its full configuration surface (the
42
+ // settings mirror, document controller, and deliverables open actions
43
+ // all branch on connection.isLoopback). Must run before any boot entry.
44
+ 'try{if(w.__DSH_TRANSPORT__===undefined)w.__DSH_TRANSPORT__={};w.__DSH_TRANSPORT__.ownsHost=true}catch(e){}' +
33
45
  'var R=' + json + ';' +
46
+ // The cookieless device credential: read lazily per call - the
47
+ // /pair-app capture script sets it in head AFTER this boot script ran,
48
+ // so a parse-time read would always see null.
49
+ 'function rdv(){try{return w.sessionStorage.getItem(R.deviceKey)}catch(e){return null}}' +
50
+ 'function att(init){' +
51
+ 'var dv=rdv();' +
52
+ 'if(dv===null)return init;' +
53
+ 'var h=init&&init.headers;' +
54
+ 'if(typeof Headers!=="undefined"&&h instanceof Headers){try{h.set(R.deviceHeader,dv)}catch(e){}return init}' +
55
+ '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})}' +
56
+ 'return init}' +
34
57
  'function sf(p){' +
35
58
  'if(p.indexOf(R.pairPrefix)===0)return false;' +
36
59
  'if(p.indexOf(R.updatePrefix)===0)return false;' +
@@ -78,7 +101,7 @@ export function buildRemoteChannelBootScript(rules = REMOTE_CHANNEL_RULES) {
78
101
  'var next=new URL(url);' +
79
102
  'next.pathname=rp(url.pathname);' +
80
103
  'var target=typeof input==="string"||input instanceof URL?next.toString():new Request(next,input);' +
81
- 'return Promise.resolve(of.call(w,target,init)).then(function(res){' +
104
+ 'return Promise.resolve(of.call(w,target,att(init))).then(function(res){' +
82
105
  // denied() is sync-false for non-403 and a promise otherwise.
83
106
  'void Promise.resolve(denied(res)).then(signal);' +
84
107
  'return res})}' +
@@ -90,6 +113,8 @@ export function buildRemoteChannelBootScript(rules = REMOTE_CHANNEL_RULES) {
90
113
  'if(o!==""&&o===loc.origin&&sw(p.pathname)){' +
91
114
  'var nx=new URL(p);' +
92
115
  'nx.pathname=rp(p.pathname);' +
116
+ 'var dvv=rdv();' +
117
+ 'if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);' +
93
118
  'return protocols!==undefined?new OW(nx,protocols):new OW(nx)}' +
94
119
  'return protocols!==undefined?new OW(url,protocols):new OW(url)};' +
95
120
  'w.WebSocket.prototype=OW.prototype;' +
@@ -101,6 +126,8 @@ export function buildRemoteChannelBootScript(rules = REMOTE_CHANNEL_RULES) {
101
126
  'if(so(p)&&sf(p.pathname)){' +
102
127
  'var nx=new URL(p);' +
103
128
  'nx.pathname=rp(p.pathname);' +
129
+ 'var dvv=rdv();' +
130
+ 'if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);' +
104
131
  'return new OE(nx,cfg)}' +
105
132
  'return new OE(url,cfg)};' +
106
133
  'w.EventSource.prototype=OE.prototype}' +
@@ -121,8 +148,43 @@ export function buildRemoteChannelBootScript(rules = REMOTE_CHANNEL_RULES) {
121
148
  'for(var i=0;i<restores.length;i++)restores[i]();' +
122
149
  'try{delete w[' + seat + ']}catch(e){w[' + seat + ']=undefined}};' +
123
150
  'w[' + seat + ']=seat' +
151
+ // The boot watchdog rides the same IIFE and skip conditions: it needs
152
+ // the non-loopback gate above (the desktop boots over loopback without
153
+ // a tunnel in the path) and the try/catch fail-closed envelope.
154
+ buildBootWatchdogScript() +
124
155
  '}catch(e){}' +
125
156
  '})();';
126
157
  }
158
+ /** Boot-watchdog latch: one self-reload per session while the boot is broken. */
159
+ export const BOOT_WATCHDOG_KEY = 'dsh-remote-boot-reload';
160
+ /** Total wait for the app's first conversation surface before the reload. */
161
+ const BOOT_WATCHDOG_WAIT_MS = 15_000;
162
+ /** Watchdog poll cadence. */
163
+ const BOOT_WATCHDOG_POLL_MS = 1_000;
164
+ /**
165
+ * Build the boot-watchdog script fragment. A remote boot has no recovery
166
+ * path of its own: the SPA mounts nothing when a boot-critical request dies
167
+ * (a tunnel-edge 429 under burst, a dropped stream, a boot-order race), and
168
+ * the phone then shows a permanently blank shell. The watchdog polls for the
169
+ * app's conversation surface and reloads once when it never appears; the
170
+ * sessionStorage latch keeps a genuinely broken deployment from looping,
171
+ * and a successful boot clears the latch so a later failure can recover.
172
+ */
173
+ export function buildBootWatchdogScript(waitMs = BOOT_WATCHDOG_WAIT_MS, pollMs = BOOT_WATCHDOG_POLL_MS) {
174
+ const key = JSON.stringify(BOOT_WATCHDOG_KEY);
175
+ return (
176
+ // Leading semicolon: the preceding seat assignment ends without one,
177
+ // and the fragment must parse when spliced into the IIFE.
178
+ ';function wBoot(){try{return !!(w.document&&w.document.querySelector&&(w.document.querySelector("[data-conversation-scroll]")||w.document.querySelector("[data-slot=\\"conversation\\"]")))}catch(e){return false}}' +
179
+ 'function wTick(n){try{' +
180
+ 'if(wBoot()){try{w.sessionStorage.removeItem(' + key + ')}catch(e){}return}' +
181
+ 'if(n<' + waitMs + '){w.setTimeout(function(){wTick(n+' + pollMs + ')},' + pollMs + ');return}' +
182
+ 'var done=false;try{done=w.sessionStorage.getItem(' + key + ')==="1"}catch(e){}' +
183
+ 'if(done)return;' +
184
+ 'try{w.sessionStorage.setItem(' + key + ',"1")}catch(e){}' +
185
+ 'w.location.reload()' +
186
+ '}catch(e){}}' +
187
+ 'if(typeof w.setTimeout==="function")w.setTimeout(function(){wTick(0)},' + pollMs + ');');
188
+ }
127
189
  /** The script this plugin contributes; built once from the live rules. */
128
190
  export const REMOTE_CHANNEL_BOOT_SCRIPT = buildRemoteChannelBootScript();
@@ -7,6 +7,8 @@
7
7
  */
8
8
  /** The gated mirror prefix (must match src/remote-methods.ts). */
9
9
  export declare const REMOTE_PREFIX = "/remote";
10
+ import { REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY } from './remote-methods.ts';
11
+ export { REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY };
10
12
  /** Connection-plugin method prefix under the gated channel. */
11
13
  export declare const REMOTE_API_PREFIX = "/remote/api";
12
14
  /** Every decision input of the remote-channel rewrite, JSON-serializable. */
@@ -21,6 +23,12 @@ export interface RemoteChannelRules {
21
23
  readonly gitPrefix: string;
22
24
  readonly petPrefix: string;
23
25
  readonly wsPaths: readonly string[];
26
+ /** Header carrying the cookieless device credential on gated fetches. */
27
+ readonly deviceHeader: string;
28
+ /** sessionStorage key holding the device credential (set by /pair-app). */
29
+ readonly deviceKey: string;
30
+ /** Query parameter carrying it on WebSocket upgrades. */
31
+ readonly deviceQuery: string;
24
32
  }
25
33
  /** The live rule set. */
26
34
  export declare const REMOTE_CHANNEL_RULES: RemoteChannelRules;
@@ -1 +1 @@
1
- {"version":3,"file":"remote-channel-rules.d.ts","sourceRoot":"","sources":["../../src/remote-channel-rules.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,kEAAkE;AAClE,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,gBAAyB,CAAA;AAEvD,6EAA6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAA;IACtC,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAA;IACrC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CACpC;AAED,yBAAyB;AACzB,eAAO,MAAM,oBAAoB,EAAE,kBAiBlC,CAAA;AAED,iEAAiE;AACjE,eAAO,MAAM,0BAA0B,gCAAgC,CAAA;AAEvE;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAC/B,QAAQ,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAC7B,eAAe,EAAE,OAAO,CAAA;IACxB,OAAO,IAAI,IAAI,CAAA;CAChB"}
1
+ {"version":3,"file":"remote-channel-rules.d.ts","sourceRoot":"","sources":["../../src/remote-channel-rules.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,kEAAkE;AAClE,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAE/E,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,CAAA;AAEpD,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,gBAAyB,CAAA;AAEvD,6EAA6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAA;IACtC,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAA;IACrC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,yEAAyE;IACzE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,yDAAyD;IACzD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AAED,yBAAyB;AACzB,eAAO,MAAM,oBAAoB,EAAE,kBAsBlC,CAAA;AAED,iEAAiE;AACjE,eAAO,MAAM,0BAA0B,gCAAgC,CAAA;AAEvE;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAC/B,QAAQ,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAC7B,eAAe,EAAE,OAAO,CAAA;IACxB,OAAO,IAAI,IAAI,CAAA;CAChB"}
@@ -7,6 +7,8 @@
7
7
  */
8
8
  /** The gated mirror prefix (must match src/remote-methods.ts). */
9
9
  export const REMOTE_PREFIX = '/remote';
10
+ import { REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY } from "./remote-methods.js";
11
+ export { REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY };
10
12
  /** Connection-plugin method prefix under the gated channel. */
11
13
  export const REMOTE_API_PREFIX = `${REMOTE_PREFIX}/api`;
12
14
  /** The live rule set. */
@@ -20,13 +22,18 @@ export const REMOTE_CHANNEL_RULES = {
20
22
  sidebarPrefix: '/sidebar/',
21
23
  gitPrefix: '/git/',
22
24
  petPrefix: '/pet/',
25
+ // The official 0.1.2-alpha.2 line opens exactly one stream socket (the
26
+ // Typert gateway mux, /api/remote.mux); the legacy /api/events.* paths
27
+ // were removed by the SDK and stay absent here.
23
28
  wsPaths: [
24
- '/api/events.mux',
25
- '/api/events.host',
29
+ '/api/remote.mux',
26
30
  '/sidebar/ws/terminal',
27
31
  '/sidebar/ws/agent-terminals',
28
32
  '/api/dsh-ssh/terminal',
29
33
  ],
34
+ deviceHeader: REMOTE_DEVICE_HEADER,
35
+ deviceKey: 'dsh-remote-device',
36
+ deviceQuery: REMOTE_DEVICE_QUERY,
30
37
  };
31
38
  /** The window global the boot patch publishes its seat under. */
32
39
  export const REMOTE_CHANNEL_BOOT_GLOBAL = '__DSH_REMOTE_CHANNEL_BOOT__';
@@ -1,33 +1,67 @@
1
1
  /**
2
2
  * Remote desktop channel constants — SDK-independent so tests and the
3
3
  * client half can pin them without importing the host SDK graph.
4
+ *
5
+ * Access model on the 0.1.2-alpha.2 line: the host /api surface has no
6
+ * per-method privilege pin — the "configuration plane is local" behavior
7
+ * lives in the browser (client plugins branch on connection.isLoopback), and
8
+ * the paired remote desktop flips into host mode via the transport hook
9
+ * (ownsHost) while every call rides this gated channel as a loopback-shaped
10
+ * request. A paired device is therefore a full-control credential by
11
+ * design; the only paths that stay physically local are the control planes
12
+ * below (pairing control, self-update, plugin install/remove, host power).
4
13
  */
5
14
  /** Gated mirror of same-origin fenced paths (`/remote` + original pathname). */
6
15
  export declare const REMOTE_PREFIX = "/remote";
7
16
  /** Connection-plugin method prefix under the gated channel. */
8
17
  export declare const REMOTE_API_PREFIX = "/remote/api";
9
- /** WebSocket event-stream paths served by the channel (client rewrites to these). */
18
+ /**
19
+ * The gated mirrors of the official client WebSocket paths. On the pinned
20
+ * 0.1.2-alpha.2 line the client opens ONE persistent stream socket — the
21
+ * Typert gateway mux at `/api/remote.mux` — and every Remote stream
22
+ * (workspace follow, session feed, ...) rides it. If that socket is not
23
+ * rewritten onto the gated channel the phone's streams die at the connection
24
+ * fence and the UI shows an empty workspace/session mirror, so the mux is
25
+ * the one path that must never be missing here.
26
+ */
10
27
  export declare const REMOTE_API_PATHS: {
11
- readonly mux: "/remote/api/events.mux";
12
- readonly host: "/remote/api/events.host";
28
+ readonly mux: "/remote/api/remote.mux";
13
29
  };
14
30
  /**
15
31
  * Exact upgrade paths registered on webServer (the SDK matches upgrades by
16
32
  * exact path, not prefix). Query strings ride on the request URL.
17
33
  */
18
- export declare const REMOTE_UPGRADE_PATHS: readonly ["/remote/api/events.mux", "/remote/api/events.host", "/remote/sidebar/ws/terminal", "/remote/sidebar/ws/agent-terminals", "/remote/api/dsh-ssh/terminal"];
34
+ export declare const REMOTE_UPGRADE_PATHS: readonly ["/remote/api/remote.mux", "/remote/sidebar/ws/terminal", "/remote/sidebar/ws/agent-terminals", "/remote/api/dsh-ssh/terminal"];
19
35
  /** Plugin-manager HTTP prefix: install/remove stay physically local. */
20
36
  export declare const PLUGIN_MANAGER_PATH = "/api/plugin-manager";
21
37
  /** Desktop-launcher HTTP prefix: shortcut create and host shutdown stay physically local. */
22
38
  export declare const DESKTOP_LAUNCHER_PATH = "/api/dsh-desktop-launcher";
23
- /** Family settings-bridge HTTP prefix: describe/mutate stay physically local. */
39
+ /** Family settings-bridge HTTP prefix — re-exposed to paired devices (plain settings parity). */
24
40
  export declare const WEB_UI_SETTINGS_BRIDGE_PATH = "/api/dsh-web-ui-settings";
25
41
  /**
26
- * Loopback-only methods of the host API surface, mirrored from
27
- * client-connection's `PRIVILEGED_METHODS` (pinned by
28
- * tests/remote-contract.spec.ts against the installed SDK). They stay
29
- * unreachable from a paired remote desktop, matching the SDK's own stance
30
- * that the configuration plane is loopback-same-origin only.
42
+ * The cookieless device credential: the boot patch reads the device id from
43
+ * the /pair-app URL, keeps it in sessionStorage, and attaches it to every
44
+ * gated HTTP call as this header (and to WebSocket upgrades as the `device`
45
+ * query parameter - WS handshakes cannot carry headers from the Web API).
46
+ * The channel gate accepts it exactly like the device cookie, so the mobile
47
+ * flow works even with browser cookies fully blocked; the cookie remains the
48
+ * primary credential on normal browsers.
49
+ */
50
+ export declare const REMOTE_DEVICE_HEADER = "x-dsh-remote-device";
51
+ export declare const REMOTE_DEVICE_QUERY = "device";
52
+ /**
53
+ * Path prefixes that stay physically local even for a paired device. A
54
+ * paired remote desktop may use the full host API (chat, sessions,
55
+ * settings, credentials, presets — it is a full-control credential), but it
56
+ * must not reach the machine-control planes: pairing control itself, the
57
+ * dsh-web self-update installer, plugin install/remove, and desktop
58
+ * launcher actions (host shutdown, shortcuts).
59
+ */
60
+ export declare const LOCAL_ONLY_PREFIXES: readonly string[];
61
+ /**
62
+ * Whether a paired inner path must stay physically local.
63
+ * @param innerPath - the rewritten inner path (e.g. `/api/session.list`).
64
+ * @returns a denial message, or undefined when the path may be proxied.
31
65
  */
32
- export declare const LOOPBACK_ONLY_METHODS: Set<string>;
66
+ export declare function localOnlyDenial(innerPath: string): string | undefined;
33
67
  //# sourceMappingURL=remote-methods.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"remote-methods.d.ts","sourceRoot":"","sources":["../../src/remote-methods.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,gFAAgF;AAChF,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,gBAAyB,CAAA;AAEvD,qFAAqF;AACrF,eAAO,MAAM,gBAAgB;;;CAGnB,CAAA;AAEV;;;GAGG;AACH,eAAO,MAAM,oBAAoB,qKAMvB,CAAA;AAEV,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,wBAAwB,CAAA;AAExD,6FAA6F;AAC7F,eAAO,MAAM,qBAAqB,8BAA8B,CAAA;AAEhE,iFAAiF;AACjF,eAAO,MAAM,2BAA2B,6BAA6B,CAAA;AAErE;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,aAgBhC,CAAA"}
1
+ {"version":3,"file":"remote-methods.d.ts","sourceRoot":"","sources":["../../src/remote-methods.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,gFAAgF;AAChF,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,gBAAyB,CAAA;AAEvD;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB;;CAEnB,CAAA;AAEV;;;GAGG;AACH,eAAO,MAAM,oBAAoB,0IAKvB,CAAA;AAEV,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,wBAAwB,CAAA;AAExD,6FAA6F;AAC7F,eAAO,MAAM,qBAAqB,8BAA8B,CAAA;AAEhE,iGAAiG;AACjG,eAAO,MAAM,2BAA2B,6BAA6B,CAAA;AAErE;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,wBAAwB,CAAA;AACzD,eAAO,MAAM,mBAAmB,WAAW,CAAA;AAE3C;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAKvC,CAAA;AAEV;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOrE"}
@@ -1,15 +1,31 @@
1
1
  /**
2
2
  * Remote desktop channel constants — SDK-independent so tests and the
3
3
  * client half can pin them without importing the host SDK graph.
4
+ *
5
+ * Access model on the 0.1.2-alpha.2 line: the host /api surface has no
6
+ * per-method privilege pin — the "configuration plane is local" behavior
7
+ * lives in the browser (client plugins branch on connection.isLoopback), and
8
+ * the paired remote desktop flips into host mode via the transport hook
9
+ * (ownsHost) while every call rides this gated channel as a loopback-shaped
10
+ * request. A paired device is therefore a full-control credential by
11
+ * design; the only paths that stay physically local are the control planes
12
+ * below (pairing control, self-update, plugin install/remove, host power).
4
13
  */
5
14
  /** Gated mirror of same-origin fenced paths (`/remote` + original pathname). */
6
15
  export const REMOTE_PREFIX = '/remote';
7
16
  /** Connection-plugin method prefix under the gated channel. */
8
17
  export const REMOTE_API_PREFIX = `${REMOTE_PREFIX}/api`;
9
- /** WebSocket event-stream paths served by the channel (client rewrites to these). */
18
+ /**
19
+ * The gated mirrors of the official client WebSocket paths. On the pinned
20
+ * 0.1.2-alpha.2 line the client opens ONE persistent stream socket — the
21
+ * Typert gateway mux at `/api/remote.mux` — and every Remote stream
22
+ * (workspace follow, session feed, ...) rides it. If that socket is not
23
+ * rewritten onto the gated channel the phone's streams die at the connection
24
+ * fence and the UI shows an empty workspace/session mirror, so the mux is
25
+ * the one path that must never be missing here.
26
+ */
10
27
  export const REMOTE_API_PATHS = {
11
- mux: `${REMOTE_API_PREFIX}/events.mux`,
12
- host: `${REMOTE_API_PREFIX}/events.host`,
28
+ mux: `${REMOTE_API_PREFIX}/remote.mux`,
13
29
  };
14
30
  /**
15
31
  * Exact upgrade paths registered on webServer (the SDK matches upgrades by
@@ -17,7 +33,6 @@ export const REMOTE_API_PATHS = {
17
33
  */
18
34
  export const REMOTE_UPGRADE_PATHS = [
19
35
  REMOTE_API_PATHS.mux,
20
- REMOTE_API_PATHS.host,
21
36
  `${REMOTE_PREFIX}/sidebar/ws/terminal`,
22
37
  `${REMOTE_PREFIX}/sidebar/ws/agent-terminals`,
23
38
  `${REMOTE_API_PREFIX}/dsh-ssh/terminal`,
@@ -26,29 +41,43 @@ export const REMOTE_UPGRADE_PATHS = [
26
41
  export const PLUGIN_MANAGER_PATH = '/api/plugin-manager';
27
42
  /** Desktop-launcher HTTP prefix: shortcut create and host shutdown stay physically local. */
28
43
  export const DESKTOP_LAUNCHER_PATH = '/api/dsh-desktop-launcher';
29
- /** Family settings-bridge HTTP prefix: describe/mutate stay physically local. */
44
+ /** Family settings-bridge HTTP prefix — re-exposed to paired devices (plain settings parity). */
30
45
  export const WEB_UI_SETTINGS_BRIDGE_PATH = '/api/dsh-web-ui-settings';
31
46
  /**
32
- * Loopback-only methods of the host API surface, mirrored from
33
- * client-connection's `PRIVILEGED_METHODS` (pinned by
34
- * tests/remote-contract.spec.ts against the installed SDK). They stay
35
- * unreachable from a paired remote desktop, matching the SDK's own stance
36
- * that the configuration plane is loopback-same-origin only.
47
+ * The cookieless device credential: the boot patch reads the device id from
48
+ * the /pair-app URL, keeps it in sessionStorage, and attaches it to every
49
+ * gated HTTP call as this header (and to WebSocket upgrades as the `device`
50
+ * query parameter - WS handshakes cannot carry headers from the Web API).
51
+ * The channel gate accepts it exactly like the device cookie, so the mobile
52
+ * flow works even with browser cookies fully blocked; the cookie remains the
53
+ * primary credential on normal browsers.
54
+ */
55
+ export const REMOTE_DEVICE_HEADER = 'x-dsh-remote-device';
56
+ export const REMOTE_DEVICE_QUERY = 'device';
57
+ /**
58
+ * Path prefixes that stay physically local even for a paired device. A
59
+ * paired remote desktop may use the full host API (chat, sessions,
60
+ * settings, credentials, presets — it is a full-control credential), but it
61
+ * must not reach the machine-control planes: pairing control itself, the
62
+ * dsh-web self-update installer, plugin install/remove, and desktop
63
+ * launcher actions (host shutdown, shortcuts).
64
+ */
65
+ export const LOCAL_ONLY_PREFIXES = [
66
+ '/api/pair',
67
+ '/api/update',
68
+ PLUGIN_MANAGER_PATH,
69
+ DESKTOP_LAUNCHER_PATH,
70
+ ];
71
+ /**
72
+ * Whether a paired inner path must stay physically local.
73
+ * @param innerPath - the rewritten inner path (e.g. `/api/session.list`).
74
+ * @returns a denial message, or undefined when the path may be proxied.
37
75
  */
38
- export const LOOPBACK_ONLY_METHODS = new Set([
39
- 'agentPreset.read',
40
- 'agentPreset.copy',
41
- 'agentPreset.openDocument',
42
- 'agentPreset.remove',
43
- 'host.pickDirectory',
44
- 'host.openPath',
45
- 'settings.describe',
46
- 'settings.openDocument',
47
- 'settings.update',
48
- 'settings.replace',
49
- 'settings.mutate',
50
- 'credentials.describe',
51
- 'credentials.set',
52
- 'credentials.unset',
53
- 'llm.discoverModels',
54
- ]);
76
+ export function localOnlyDenial(innerPath) {
77
+ for (const prefix of LOCAL_ONLY_PREFIXES) {
78
+ if (innerPath === prefix || innerPath.startsWith(`${prefix}/`)) {
79
+ return `${prefix.slice(1)} stays physically local and stays unreachable from a paired remote desktop`;
80
+ }
81
+ }
82
+ return undefined;
83
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Remote-presence to pet-visibility link (user requirement): while at least
3
+ * one paired device is online (an active phone mirror session), the
4
+ * host-global pet is hidden through the pet's OWN hide switch (the
5
+ * PetService.setVisible RPC, the same one the pet's hide button uses);
6
+ * once the last device stays offline past a grace window, the pet is
7
+ * restored. The pet plugin is optional: without it (or on any pet failure)
8
+ * the link is a no-op - the remote-control feature never depends on the
9
+ * pet being installed.
10
+ *
11
+ * The visibility is host-global by design (the pet is host-global, no
12
+ * session dimension): the desktop and the phone share one pet, so the link
13
+ * hides it while the operator is away on a phone mirror and restores it
14
+ * when the mirror ends. A manual user hide is respected: if the pet is
15
+ * already hidden when the first device comes online, nothing is recorded
16
+ * and nothing is restored later.
17
+ */
18
+ /** The pet service seam the link needs (structural slice, no package edge). */
19
+ export interface PresencePetSeam {
20
+ /** RPC: show or hide the pet. */
21
+ setVisible(visible: boolean): Promise<unknown>;
22
+ /** Read the current display state (the visibility the user sees). */
23
+ state(): Promise<{
24
+ display?: {
25
+ visible?: boolean;
26
+ };
27
+ }>;
28
+ }
29
+ /** Dependencies of the presence link (all injectable for tests). */
30
+ export interface PresencePetDeps {
31
+ /** Subscribe to pairing snapshot changes (fires with every state change). */
32
+ onState(listener: (snapshot: {
33
+ phase: string;
34
+ onlineCount: number;
35
+ }) => void): () => void;
36
+ /** Resolve the pet service seam (undefined when the pet plugin is absent). */
37
+ pet(): PresencePetSeam | undefined;
38
+ /** Grace before restoring the pet after the last device went offline. */
39
+ restoreAfterMs?: number;
40
+ timers?: {
41
+ setTimeout(fn: () => void, ms: number): unknown;
42
+ clearTimeout(t: unknown): void;
43
+ };
44
+ }
45
+ /**
46
+ * Start the presence link. Returns the disposer (withdraws the state
47
+ * subscription and cancels a pending restore).
48
+ * @param deps - pairing stream + pet seam (+ test seams).
49
+ */
50
+ export declare function startRemotePresencePet(deps: PresencePetDeps): () => void;
51
+ //# sourceMappingURL=remote-presence-pet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-presence-pet.d.ts","sourceRoot":"","sources":["../../src/remote-presence-pet.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,+EAA+E;AAC/E,MAAM,WAAW,eAAe;IAC9B,iCAAiC;IACjC,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC9C,qEAAqE;IACrE,KAAK,IAAI,OAAO,CAAC;QAAE,OAAO,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,OAAO,CAAA;SAAE,CAAA;KAAE,CAAC,CAAA;CACtD;AAED,oEAAoE;AACpE,MAAM,WAAW,eAAe;IAC9B,6EAA6E;IAC7E,OAAO,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,GAAG,MAAM,IAAI,CAAA;IACzF,8EAA8E;IAC9E,GAAG,IAAI,eAAe,GAAG,SAAS,CAAA;IAClC,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE;QACP,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAA;QAC/C,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;KAC/B,CAAA;CACF;AASD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,IAAI,CA4ExE"}