@agimon-ai/doompi-web 0.0.1-alpha.0 → 0.0.1-alpha.10

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 (435) hide show
  1. package/README.md +261 -44
  2. package/dist/bin/serve.cjs +1 -1
  3. package/dist/bin/serve.cjs.map +1 -1
  4. package/dist/bin/serve.mjs +1 -1
  5. package/dist/bin/serve.mjs.map +1 -1
  6. package/dist/index.d.cts +2 -2
  7. package/dist/index.d.mts +2 -2
  8. package/dist/pwa/icon-192.png +0 -0
  9. package/dist/pwa/icon-512.png +0 -0
  10. package/dist/pwa/index.html +104 -0
  11. package/dist/pwa/manifest.webmanifest +23 -0
  12. package/dist/pwa/pwa.js +21 -0
  13. package/dist/pwa/sw.js +2 -0
  14. package/dist/src/adapters/bundlePublication.cjs +3 -0
  15. package/dist/src/adapters/bundlePublication.cjs.map +1 -0
  16. package/dist/src/adapters/bundlePublication.mjs +3 -0
  17. package/dist/src/adapters/bundlePublication.mjs.map +1 -0
  18. package/dist/src/adapters/cockpitContainer.cjs +2 -0
  19. package/dist/src/adapters/cockpitContainer.cjs.map +1 -0
  20. package/dist/src/adapters/cockpitContainer.mjs +2 -0
  21. package/dist/src/adapters/cockpitContainer.mjs.map +1 -0
  22. package/dist/src/adapters/cockpitHandoff.cjs +2 -0
  23. package/dist/src/adapters/cockpitHandoff.cjs.map +1 -0
  24. package/dist/src/adapters/cockpitHandoff.mjs +2 -0
  25. package/dist/src/adapters/cockpitHandoff.mjs.map +1 -0
  26. package/dist/src/adapters/deviceAuth.cjs +2 -0
  27. package/dist/src/adapters/deviceAuth.cjs.map +1 -0
  28. package/dist/src/adapters/deviceAuth.mjs +2 -0
  29. package/dist/src/adapters/deviceAuth.mjs.map +1 -0
  30. package/dist/src/adapters/directoryListing.cjs +1 -1
  31. package/dist/src/adapters/directoryListing.cjs.map +1 -1
  32. package/dist/src/adapters/directoryListing.mjs +1 -1
  33. package/dist/src/adapters/directoryListing.mjs.map +1 -1
  34. package/dist/src/adapters/doomInitialization.cjs +2 -0
  35. package/dist/src/adapters/doomInitialization.cjs.map +1 -0
  36. package/dist/src/adapters/doomInitialization.mjs +2 -0
  37. package/dist/src/adapters/doomInitialization.mjs.map +1 -0
  38. package/dist/src/adapters/httpServer.cjs +2 -1
  39. package/dist/src/adapters/httpServer.cjs.map +1 -1
  40. package/dist/src/adapters/httpServer.d.cts +2 -0
  41. package/dist/src/adapters/httpServer.d.cts.map +1 -1
  42. package/dist/src/adapters/httpServer.d.mts +2 -0
  43. package/dist/src/adapters/httpServer.d.mts.map +1 -1
  44. package/dist/src/adapters/httpServer.mjs +2 -1
  45. package/dist/src/adapters/httpServer.mjs.map +1 -1
  46. package/dist/src/adapters/hubProbe.cjs +1 -1
  47. package/dist/src/adapters/hubProbe.cjs.map +1 -1
  48. package/dist/src/adapters/hubProbe.mjs +1 -1
  49. package/dist/src/adapters/hubProbe.mjs.map +1 -1
  50. package/dist/src/adapters/packageApiProxy.cjs +1 -1
  51. package/dist/src/adapters/packageApiProxy.cjs.map +1 -1
  52. package/dist/src/adapters/packageApiProxy.mjs +1 -1
  53. package/dist/src/adapters/packageApiProxy.mjs.map +1 -1
  54. package/dist/src/adapters/providerAuth.cjs +1 -1
  55. package/dist/src/adapters/providerAuth.cjs.map +1 -1
  56. package/dist/src/adapters/providerAuth.mjs +1 -1
  57. package/dist/src/adapters/providerAuth.mjs.map +1 -1
  58. package/dist/src/adapters/registryWatcher.cjs +1 -1
  59. package/dist/src/adapters/registryWatcher.cjs.map +1 -1
  60. package/dist/src/adapters/registryWatcher.d.cts +1 -0
  61. package/dist/src/adapters/registryWatcher.d.mts +1 -0
  62. package/dist/src/adapters/registryWatcher.mjs +1 -1
  63. package/dist/src/adapters/registryWatcher.mjs.map +1 -1
  64. package/dist/src/adapters/remoteAccess.cjs +2 -0
  65. package/dist/src/adapters/remoteAccess.cjs.map +1 -0
  66. package/dist/src/adapters/remoteAccess.mjs +2 -0
  67. package/dist/src/adapters/remoteAccess.mjs.map +1 -0
  68. package/dist/src/adapters/remoteAccessStore.cjs +2 -0
  69. package/dist/src/adapters/remoteAccessStore.cjs.map +1 -0
  70. package/dist/src/adapters/remoteAccessStore.mjs +2 -0
  71. package/dist/src/adapters/remoteAccessStore.mjs.map +1 -0
  72. package/dist/src/adapters/remoteGuard.cjs +2 -0
  73. package/dist/src/adapters/remoteGuard.cjs.map +1 -0
  74. package/dist/src/adapters/remoteGuard.mjs +2 -0
  75. package/dist/src/adapters/remoteGuard.mjs.map +1 -0
  76. package/dist/src/adapters/remoteRoutes.cjs +2 -0
  77. package/dist/src/adapters/remoteRoutes.cjs.map +1 -0
  78. package/dist/src/adapters/remoteRoutes.mjs +2 -0
  79. package/dist/src/adapters/remoteRoutes.mjs.map +1 -0
  80. package/dist/src/adapters/serverSpawner.cjs +1 -1
  81. package/dist/src/adapters/serverSpawner.cjs.map +1 -1
  82. package/dist/src/adapters/serverSpawner.d.cts +1 -0
  83. package/dist/src/adapters/serverSpawner.d.mts +1 -0
  84. package/dist/src/adapters/serverSpawner.mjs +1 -1
  85. package/dist/src/adapters/serverSpawner.mjs.map +1 -1
  86. package/dist/src/adapters/sessionFiles.cjs +1 -1
  87. package/dist/src/adapters/sessionFiles.cjs.map +1 -1
  88. package/dist/src/adapters/sessionFiles.mjs +1 -1
  89. package/dist/src/adapters/sessionFiles.mjs.map +1 -1
  90. package/dist/src/adapters/sessionHub.cjs +1 -1
  91. package/dist/src/adapters/sessionHub.cjs.map +1 -1
  92. package/dist/src/adapters/sessionHub.d.cts +6 -0
  93. package/dist/src/adapters/sessionHub.d.mts +6 -0
  94. package/dist/src/adapters/sessionHub.mjs +1 -1
  95. package/dist/src/adapters/sessionHub.mjs.map +1 -1
  96. package/dist/src/adapters/sessionRelaunch.cjs +2 -0
  97. package/dist/src/adapters/sessionRelaunch.cjs.map +1 -0
  98. package/dist/src/adapters/sessionRelaunch.mjs +2 -0
  99. package/dist/src/adapters/sessionRelaunch.mjs.map +1 -0
  100. package/dist/src/adapters/sessionSocketClient.cjs +1 -1
  101. package/dist/src/adapters/sessionSocketClient.cjs.map +1 -1
  102. package/dist/src/adapters/sessionSocketClient.mjs +1 -1
  103. package/dist/src/adapters/sessionSocketClient.mjs.map +1 -1
  104. package/dist/src/adapters/settingsRoutes.cjs +2 -0
  105. package/dist/src/adapters/settingsRoutes.cjs.map +1 -0
  106. package/dist/src/adapters/settingsRoutes.mjs +2 -0
  107. package/dist/src/adapters/settingsRoutes.mjs.map +1 -0
  108. package/dist/src/adapters/syncGuard.cjs +1 -1
  109. package/dist/src/adapters/syncGuard.cjs.map +1 -1
  110. package/dist/src/adapters/syncGuard.mjs +1 -1
  111. package/dist/src/adapters/syncGuard.mjs.map +1 -1
  112. package/dist/src/adapters/tunnelProcess.cjs +2 -0
  113. package/dist/src/adapters/tunnelProcess.cjs.map +1 -0
  114. package/dist/src/adapters/tunnelProcess.mjs +2 -0
  115. package/dist/src/adapters/tunnelProcess.mjs.map +1 -0
  116. package/dist/src/adapters/webBundler.cjs +1 -1
  117. package/dist/src/adapters/webBundler.cjs.map +1 -1
  118. package/dist/src/adapters/webBundler.d.cts +1 -1
  119. package/dist/src/adapters/webBundler.d.cts.map +1 -1
  120. package/dist/src/adapters/webBundler.d.mts +1 -1
  121. package/dist/src/adapters/webBundler.d.mts.map +1 -1
  122. package/dist/src/adapters/webBundler.mjs +1 -1
  123. package/dist/src/adapters/webBundler.mjs.map +1 -1
  124. package/dist/src/adapters/webComposition.cjs +3 -0
  125. package/dist/src/adapters/webComposition.cjs.map +1 -0
  126. package/dist/src/adapters/webComposition.mjs +3 -0
  127. package/dist/src/adapters/webComposition.mjs.map +1 -0
  128. package/dist/src/adapters/webHubPluginLoader.cjs +1 -1
  129. package/dist/src/adapters/webHubPluginLoader.cjs.map +1 -1
  130. package/dist/src/adapters/webHubPluginLoader.mjs +1 -1
  131. package/dist/src/adapters/webHubPluginLoader.mjs.map +1 -1
  132. package/dist/src/adapters/webPluginGenerate.cjs +2 -2
  133. package/dist/src/adapters/webPluginGenerate.cjs.map +1 -1
  134. package/dist/src/adapters/webPluginGenerate.mjs +2 -2
  135. package/dist/src/adapters/webPluginGenerate.mjs.map +1 -1
  136. package/dist/src/adapters/webPluginScan.cjs +1 -1
  137. package/dist/src/adapters/webPluginScan.cjs.map +1 -1
  138. package/dist/src/adapters/webPluginScan.mjs +1 -1
  139. package/dist/src/adapters/webPluginScan.mjs.map +1 -1
  140. package/dist/src/adapters/webPluginVite.cjs +1 -1
  141. package/dist/src/adapters/webPluginVite.cjs.map +1 -1
  142. package/dist/src/adapters/webPluginVite.mjs +1 -1
  143. package/dist/src/adapters/webPluginVite.mjs.map +1 -1
  144. package/dist/src/adapters/webPush.cjs +2 -0
  145. package/dist/src/adapters/webPush.cjs.map +1 -0
  146. package/dist/src/adapters/webPush.mjs +2 -0
  147. package/dist/src/adapters/webPush.mjs.map +1 -0
  148. package/dist/src/adapters/webTelemetry.cjs +2 -0
  149. package/dist/src/adapters/webTelemetry.cjs.map +1 -0
  150. package/dist/src/adapters/webTelemetry.mjs +2 -0
  151. package/dist/src/adapters/webTelemetry.mjs.map +1 -0
  152. package/dist/src/adapters/webauthn.cjs +2 -0
  153. package/dist/src/adapters/webauthn.cjs.map +1 -0
  154. package/dist/src/adapters/webauthn.mjs +2 -0
  155. package/dist/src/adapters/webauthn.mjs.map +1 -0
  156. package/dist/src/services/deviceSessions.cjs +2 -0
  157. package/dist/src/services/deviceSessions.cjs.map +1 -0
  158. package/dist/src/services/deviceSessions.mjs +2 -0
  159. package/dist/src/services/deviceSessions.mjs.map +1 -0
  160. package/dist/src/services/fileMatch.cjs +1 -1
  161. package/dist/src/services/fileMatch.cjs.map +1 -1
  162. package/dist/src/services/fileMatch.mjs +1 -1
  163. package/dist/src/services/fileMatch.mjs.map +1 -1
  164. package/dist/src/services/fileMedia.cjs +1 -1
  165. package/dist/src/services/fileMedia.cjs.map +1 -1
  166. package/dist/src/services/fileMedia.mjs +1 -1
  167. package/dist/src/services/fileMedia.mjs.map +1 -1
  168. package/dist/src/services/pairingFlow.cjs +2 -0
  169. package/dist/src/services/pairingFlow.cjs.map +1 -0
  170. package/dist/src/services/pairingFlow.mjs +2 -0
  171. package/dist/src/services/pairingFlow.mjs.map +1 -0
  172. package/dist/src/services/pairingPage.cjs +385 -0
  173. package/dist/src/services/pairingPage.cjs.map +1 -0
  174. package/dist/src/services/pairingPage.mjs +385 -0
  175. package/dist/src/services/pairingPage.mjs.map +1 -0
  176. package/dist/src/services/pathScope.cjs +2 -0
  177. package/dist/src/services/pathScope.cjs.map +1 -0
  178. package/dist/src/services/pathScope.mjs +2 -0
  179. package/dist/src/services/pathScope.mjs.map +1 -0
  180. package/dist/src/services/remoteAccessSettings.cjs +2 -0
  181. package/dist/src/services/remoteAccessSettings.cjs.map +1 -0
  182. package/dist/src/services/remoteAccessSettings.mjs +2 -0
  183. package/dist/src/services/remoteAccessSettings.mjs.map +1 -0
  184. package/dist/src/services/remoteGuardPolicy.cjs +2 -0
  185. package/dist/src/services/remoteGuardPolicy.cjs.map +1 -0
  186. package/dist/src/services/remoteGuardPolicy.mjs +2 -0
  187. package/dist/src/services/remoteGuardPolicy.mjs.map +1 -0
  188. package/dist/src/services/serveOptions.cjs +14 -1
  189. package/dist/src/services/serveOptions.cjs.map +1 -1
  190. package/dist/src/services/serveOptions.d.cts +8 -0
  191. package/dist/src/services/serveOptions.d.cts.map +1 -1
  192. package/dist/src/services/serveOptions.d.mts +8 -0
  193. package/dist/src/services/serveOptions.d.mts.map +1 -1
  194. package/dist/src/services/serveOptions.mjs +14 -1
  195. package/dist/src/services/serveOptions.mjs.map +1 -1
  196. package/dist/src/services/sessionMigration.cjs +2 -0
  197. package/dist/src/services/sessionMigration.cjs.map +1 -0
  198. package/dist/src/services/sessionMigration.mjs +2 -0
  199. package/dist/src/services/sessionMigration.mjs.map +1 -0
  200. package/dist/src/services/staticAssets.cjs +1 -1
  201. package/dist/src/services/staticAssets.cjs.map +1 -1
  202. package/dist/src/services/staticAssets.mjs +1 -1
  203. package/dist/src/services/staticAssets.mjs.map +1 -1
  204. package/dist/src/services/tunnelOutput.cjs +2 -0
  205. package/dist/src/services/tunnelOutput.cjs.map +1 -0
  206. package/dist/src/services/tunnelOutput.mjs +2 -0
  207. package/dist/src/services/tunnelOutput.mjs.map +1 -0
  208. package/dist/src/services/webauthnPolicy.cjs +2 -0
  209. package/dist/src/services/webauthnPolicy.cjs.map +1 -0
  210. package/dist/src/services/webauthnPolicy.mjs +2 -0
  211. package/dist/src/services/webauthnPolicy.mjs.map +1 -0
  212. package/dist/src/types/auth.cjs.map +1 -1
  213. package/dist/src/types/auth.d.cts +6 -0
  214. package/dist/src/types/auth.d.cts.map +1 -1
  215. package/dist/src/types/auth.d.mts +6 -0
  216. package/dist/src/types/auth.d.mts.map +1 -1
  217. package/dist/src/types/auth.mjs.map +1 -1
  218. package/dist/src/types/bridge.d.cts +31 -0
  219. package/dist/src/types/bridge.d.cts.map +1 -1
  220. package/dist/src/types/bridge.d.mts +31 -0
  221. package/dist/src/types/bridge.d.mts.map +1 -1
  222. package/dist/src/types/hub.cjs +1 -1
  223. package/dist/src/types/hub.cjs.map +1 -1
  224. package/dist/src/types/hub.d.cts +1 -1
  225. package/dist/src/types/hub.d.cts.map +1 -1
  226. package/dist/src/types/hub.d.mts +1 -1
  227. package/dist/src/types/hub.d.mts.map +1 -1
  228. package/dist/src/types/hub.mjs +1 -1
  229. package/dist/src/types/hub.mjs.map +1 -1
  230. package/dist/src/types/notification.cjs +2 -0
  231. package/dist/src/types/notification.cjs.map +1 -0
  232. package/dist/src/types/notification.mjs +2 -0
  233. package/dist/src/types/notification.mjs.map +1 -0
  234. package/dist/src/types/remoteAccess.cjs +2 -0
  235. package/dist/src/types/remoteAccess.cjs.map +1 -0
  236. package/dist/src/types/remoteAccess.d.cts +88 -0
  237. package/dist/src/types/remoteAccess.d.cts.map +1 -0
  238. package/dist/src/types/remoteAccess.d.mts +88 -0
  239. package/dist/src/types/remoteAccess.d.mts.map +1 -0
  240. package/dist/src/types/remoteAccess.mjs +2 -0
  241. package/dist/src/types/remoteAccess.mjs.map +1 -0
  242. package/dist/src/types/session.cjs +1 -1
  243. package/dist/src/types/session.mjs +1 -1
  244. package/dist/src/types/session.mjs.map +1 -1
  245. package/dist/src/types/settings.cjs +2 -0
  246. package/dist/src/types/settings.cjs.map +1 -0
  247. package/dist/src/types/settings.d.cts +1 -0
  248. package/dist/src/types/settings.d.mts +1 -0
  249. package/dist/src/types/settings.mjs +2 -0
  250. package/dist/src/types/settings.mjs.map +1 -0
  251. package/dist/web/assets/CodeEditorView-Drbj5Jmq.js +15 -0
  252. package/dist/web/assets/CodeEditorView-Drbj5Jmq.js.map +1 -0
  253. package/dist/web/assets/dist-7iNxebeg.js +2 -0
  254. package/dist/web/assets/dist-7iNxebeg.js.map +1 -0
  255. package/dist/web/assets/dist-BCRyBbdR.js +2 -0
  256. package/dist/web/assets/dist-BCRyBbdR.js.map +1 -0
  257. package/dist/web/assets/dist-BiFP6LQz.js +24 -0
  258. package/dist/web/assets/dist-BiFP6LQz.js.map +1 -0
  259. package/dist/web/assets/dist-C2KhFiEs.js +1 -0
  260. package/dist/web/assets/dist-CM-9xFgG.js +2 -0
  261. package/dist/web/assets/dist-CM-9xFgG.js.map +1 -0
  262. package/dist/web/assets/dist-ClQi4oJ4.js +10 -0
  263. package/dist/web/assets/dist-ClQi4oJ4.js.map +1 -0
  264. package/dist/web/assets/dist-CyY9Rnb9.js +2 -0
  265. package/dist/web/assets/dist-CyY9Rnb9.js.map +1 -0
  266. package/dist/web/assets/dist-D_AIHDMX.js +2 -0
  267. package/dist/web/assets/dist-D_AIHDMX.js.map +1 -0
  268. package/dist/web/assets/dist-DjFwY_To.js +2 -0
  269. package/dist/web/assets/dist-DjFwY_To.js.map +1 -0
  270. package/dist/web/assets/dist-joNLZ1a7.js +7 -0
  271. package/dist/web/assets/dist-joNLZ1a7.js.map +1 -0
  272. package/dist/web/assets/dockerfile-BDePf5PZ.js +2 -0
  273. package/dist/web/assets/dockerfile-BDePf5PZ.js.map +1 -0
  274. package/dist/web/assets/index-BuzTi9Gb.css +2 -0
  275. package/dist/web/assets/index-Ek5bNpFT.js +101 -0
  276. package/dist/web/assets/index-Ek5bNpFT.js.map +1 -0
  277. package/dist/web/assets/rolldown-runtime-hePW80VL.js +1 -0
  278. package/dist/web/assets/shell-DwuoZtxw.js +2 -0
  279. package/dist/web/assets/shell-DwuoZtxw.js.map +1 -0
  280. package/dist/web/assets/toml-BPTmHmyx.js +2 -0
  281. package/dist/web/assets/toml-BPTmHmyx.js.map +1 -0
  282. package/dist/web/index.html +5 -3
  283. package/llms.txt +6 -1
  284. package/package.json +26 -17
  285. package/src/adapters/bundlePublication.ts +156 -0
  286. package/src/adapters/cockpitContainer.ts +209 -0
  287. package/src/adapters/cockpitHandoff.ts +66 -0
  288. package/src/adapters/deviceAuth.ts +171 -0
  289. package/src/adapters/directoryListing.ts +25 -8
  290. package/src/adapters/doomInitialization.ts +49 -0
  291. package/src/adapters/httpServer.ts +990 -115
  292. package/src/adapters/hubProbe.ts +34 -5
  293. package/src/adapters/packageApiProxy.ts +14 -3
  294. package/src/adapters/providerAuth.ts +20 -0
  295. package/src/adapters/registryWatcher.ts +33 -30
  296. package/src/adapters/remoteAccess.ts +544 -0
  297. package/src/adapters/remoteAccessStore.ts +161 -0
  298. package/src/adapters/remoteGuard.ts +179 -0
  299. package/src/adapters/remoteRoutes.ts +491 -0
  300. package/src/adapters/serverSpawner.ts +67 -6
  301. package/src/adapters/sessionFiles.ts +26 -5
  302. package/src/adapters/sessionHub.ts +331 -97
  303. package/src/adapters/sessionRelaunch.ts +67 -0
  304. package/src/adapters/sessionSocketClient.ts +3 -1
  305. package/src/adapters/settingsRoutes.ts +383 -0
  306. package/src/adapters/syncGuard.ts +127 -38
  307. package/src/adapters/tunnelProcess.ts +552 -0
  308. package/src/adapters/webBundler.ts +11 -2
  309. package/src/adapters/webComposition.ts +240 -0
  310. package/src/adapters/webHubPluginLoader.ts +6 -2
  311. package/src/adapters/webPluginGenerate.ts +16 -1
  312. package/src/adapters/webPluginScan.ts +6 -2
  313. package/src/adapters/webPluginVite.ts +24 -9
  314. package/src/adapters/webPush.ts +127 -0
  315. package/src/adapters/webTelemetry.ts +131 -0
  316. package/src/adapters/webauthn.ts +297 -0
  317. package/src/bin/serve.ts +169 -12
  318. package/src/pwa/bundleCache.ts +79 -0
  319. package/src/pwa/index.html +104 -0
  320. package/src/pwa/main.ts +163 -0
  321. package/src/pwa/public/icon-192.png +0 -0
  322. package/src/pwa/public/icon-512.png +0 -0
  323. package/src/pwa/public/manifest.webmanifest +23 -0
  324. package/src/pwa/qrScanner.ts +65 -0
  325. package/src/pwa/serviceWorker.ts +314 -0
  326. package/src/pwa/workerClient.ts +60 -0
  327. package/src/services/deviceSessions.ts +135 -0
  328. package/src/services/fileMatch.ts +5 -1
  329. package/src/services/fileMedia.ts +1 -0
  330. package/src/services/pairingFlow.ts +196 -0
  331. package/src/services/pairingPage.ts +467 -0
  332. package/src/services/pathScope.ts +23 -0
  333. package/src/services/remoteAccessSettings.ts +185 -0
  334. package/src/services/remoteGuardPolicy.ts +211 -0
  335. package/src/services/serveOptions.ts +64 -2
  336. package/src/services/sessionMigration.ts +48 -0
  337. package/src/services/staticAssets.ts +1 -0
  338. package/src/services/tunnelOutput.ts +74 -0
  339. package/src/services/webauthnPolicy.ts +135 -0
  340. package/src/types/auth.ts +3 -0
  341. package/src/types/bridge.ts +29 -0
  342. package/src/types/bundle.ts +28 -0
  343. package/src/types/hub.ts +68 -3
  344. package/src/types/notification.ts +30 -0
  345. package/src/types/remoteAccess.ts +205 -0
  346. package/src/types/settings.ts +120 -0
  347. package/src/web/app/Providers.tsx +33 -6
  348. package/src/web/app/protocolRuntime.ts +14 -3
  349. package/src/web/app/sessionRuntime.ts +101 -4
  350. package/src/web/components/QrCode.tsx +53 -0
  351. package/src/web/components/RemoteAccessButton.tsx +46 -0
  352. package/src/web/features/activity/ActivityDock.tsx +121 -86
  353. package/src/web/features/activity/ContextPanel.tsx +152 -0
  354. package/src/web/features/activity/DockTabs.tsx +39 -0
  355. package/src/web/features/dialogs/DialogOverlay.tsx +22 -8
  356. package/src/web/features/leader/CommandPalette.tsx +13 -6
  357. package/src/web/features/remote/HandoverProgress.tsx +25 -0
  358. package/src/web/features/remote/PairedDeviceList.tsx +39 -0
  359. package/src/web/features/remote/PairingApprovalDialog.tsx +58 -0
  360. package/src/web/features/remote/RemoteAccessDialog.tsx +113 -0
  361. package/src/web/features/remote/RemoteAccessOptions.tsx +130 -0
  362. package/src/web/features/remote/RemoteAccessPairing.tsx +75 -0
  363. package/src/web/features/remote/RemoteBanner.tsx +50 -0
  364. package/src/web/features/remote/SandboxWorkspaces.tsx +84 -0
  365. package/src/web/features/selection/SelectionBar.tsx +14 -11
  366. package/src/web/features/session/Composer.tsx +426 -69
  367. package/src/web/features/session/ComposerPrompt.tsx +50 -0
  368. package/src/web/features/session/MentionPreviews.tsx +74 -17
  369. package/src/web/features/session/MessageMarkdown.tsx +31 -0
  370. package/src/web/features/session/QueueSheet.tsx +130 -0
  371. package/src/web/features/session/ThreadView.tsx +15 -1
  372. package/src/web/features/session/Timeline.tsx +221 -104
  373. package/src/web/features/session/ToolCard.tsx +157 -8
  374. package/src/web/features/sessions/NewSessionDialog.tsx +32 -15
  375. package/src/web/features/sessions/ResumeSessionDialog.tsx +150 -0
  376. package/src/web/features/sessions/SessionRail.tsx +143 -36
  377. package/src/web/features/sessions/WelcomePanel.tsx +32 -0
  378. package/src/web/features/settings/AppearanceSettings.tsx +1 -1
  379. package/src/web/features/settings/ContributedSettings.tsx +368 -0
  380. package/src/web/features/settings/LoginFlowDialog.tsx +7 -4
  381. package/src/web/features/settings/NotificationSettings.tsx +74 -0
  382. package/src/web/features/settings/PluginSettings.tsx +6 -2
  383. package/src/web/features/settings/ProviderSettings.tsx +12 -4
  384. package/src/web/features/settings/RemoteControlSettings.tsx +40 -0
  385. package/src/web/features/settings/RepositorySettings.tsx +374 -0
  386. package/src/web/features/settings/RepositoryWorkspace.tsx +155 -0
  387. package/src/web/features/settings/SettingsMenu.tsx +97 -26
  388. package/src/web/features/settings/SettingsPanelHost.tsx +36 -0
  389. package/src/web/features/settings/TunnelSettings.tsx +155 -0
  390. package/src/web/features/status/TopBar.tsx +52 -59
  391. package/src/web/index.html +2 -1
  392. package/src/web/lib/authApi.ts +4 -1
  393. package/src/web/lib/browserNotifications.ts +47 -0
  394. package/src/web/lib/browserTelemetry.ts +95 -0
  395. package/src/web/lib/commands.ts +51 -6
  396. package/src/web/lib/composition.ts +137 -11
  397. package/src/web/lib/contextComposition.ts +200 -0
  398. package/src/web/lib/fileMentions.ts +7 -3
  399. package/src/web/lib/hubApi.ts +90 -5
  400. package/src/web/lib/installStaleChunkRecovery.ts +43 -0
  401. package/src/web/lib/livePush.ts +116 -0
  402. package/src/web/lib/piTransport.ts +18 -7
  403. package/src/web/lib/pluginRegistry.ts +77 -1
  404. package/src/web/lib/pluginSlotProps.ts +8 -2
  405. package/src/web/lib/protocolTimeline.ts +22 -4
  406. package/src/web/lib/qrMatrix.ts +33 -0
  407. package/src/web/lib/remoteApi.ts +127 -0
  408. package/src/web/lib/sealedSession.ts +80 -0
  409. package/src/web/lib/sessionAsset.ts +30 -0
  410. package/src/web/lib/sessionModel.ts +235 -28
  411. package/src/web/lib/settingsApi.ts +158 -0
  412. package/src/web/lib/settingsDraft.ts +58 -0
  413. package/src/web/lib/settingsSections.ts +132 -6
  414. package/src/web/lib/stepUp.ts +43 -0
  415. package/src/web/lib/threadRenderer.ts +4 -3
  416. package/src/web/lib/timelineGroups.ts +91 -0
  417. package/src/web/lib/toolPrompt.ts +68 -0
  418. package/src/web/lib/transport.ts +12 -0
  419. package/src/web/lib/webauthnClient.ts +107 -0
  420. package/src/web/lib/wsClient.ts +43 -11
  421. package/src/web/main.tsx +3 -0
  422. package/src/web/routes/CockpitPage.tsx +86 -11
  423. package/src/web/routes/SettingsPage.tsx +106 -19
  424. package/src/web/stores/composerStore.ts +141 -0
  425. package/src/web/stores/newSessionStore.ts +29 -0
  426. package/src/web/stores/remoteAccessStore.ts +205 -0
  427. package/src/web/stores/sessionStore.ts +185 -24
  428. package/src/web/stores/sessionsStore.ts +17 -0
  429. package/src/web/stores/toolPromptStore.ts +28 -0
  430. package/src/web/stores/uiStore.ts +30 -1
  431. package/src/web/stores/usePluginSlotProps.ts +20 -9
  432. package/src/web/stores/useToolPrompt.ts +32 -0
  433. package/dist/web/assets/index-Cg52ngKv.css +0 -2
  434. package/dist/web/assets/index-DQO7NTIP.js +0 -90
  435. package/dist/web/assets/index-DQO7NTIP.js.map +0 -1
package/README.md CHANGED
@@ -1,22 +1,18 @@
1
1
  # @agimon-ai/doompi-web
2
2
 
3
- Web cockpit for DoomPi: a multi-session hub that serves the browser console, discovers every
4
- running [doompi-server](https://www.npmjs.com/package/@agimon-ai/doompi-server) through the session
5
- registry, and multiplexes their authenticated sockets behind one page connection
3
+ Web cockpit for DoomPi. It discovers running [doompi-server](https://www.npmjs.com/package/@agimon-ai/doompi-server) sessions, multiplexes their authenticated sockets, and serves them through one browser connection.
6
4
 
7
5
  This package is a composable [DoomPi](https://www.npmjs.com/package/@agimon-ai/doompi) subsystem.
8
6
  It registers no Pi extension and adds nothing to an interactive session.
9
7
 
10
- ## Enabling it
8
+ ## How it runs
11
9
 
12
- This package is still in development, so `doompi init` does not reference it and there is nothing
13
- to add to `.doom/modes.yaml`. It is a standalone executable you run yourself, as shown under Run
14
- below, or the process `doompi-server --web` starts for you.
10
+ `doompi-web` is a standalone process, not a Pi extension. Do not add it to `.doom/modes.yaml`. Run it directly, or start it through `doompi-server --web`.
15
11
 
16
12
  ## Requirements
17
13
 
18
14
  - Node.js 22.19.0 or newer
19
- - Running `doompi-server` processes registering their session sockets
15
+ - `doompi-server` sessions to attach to. The cockpit can start them itself.
20
16
 
21
17
  ## Install
22
18
 
@@ -30,37 +26,33 @@ npm install -g @agimon-ai/doompi-web
30
26
  doompi-web
31
27
  ```
32
28
 
33
- Then open `http://127.0.0.1:7433`. That is the whole command: no flag is required, and every one
34
- below is an override. `doompi-web` is the hub. It watches the session registry (`~/.doompi/run` by
35
- default), attaches to every registered session, shows them all in the rail, and can start new ones
36
- from the page. Sessions it starts run the `doompi-server` and the agent from this installation, so
37
- the cockpit and the sessions it creates are always the same build.
29
+ Open `http://127.0.0.1:7433`. With no flags, the hub watches `~/.doompi/run`, attaches to registered sessions, and resolves the union of their synchronized cockpit compositions from `~/.pi/.doom/sync/registrations`. The directory that launches `doompi-web` does not select the bundle. Sessions started by the cockpit use the server and agent from the same installation.
38
30
 
39
- Running it twice is not an error. The second one finds the first over the port, prints its URL, and
40
- exits, because one hub already serves every session.
31
+ If `~/.pi/.doom` does not exist, the command runs its bundled `doompi init` before starting. If a hub already answers on the port, a second `doompi-web` process prints its URL and exits.
41
32
 
42
- Working on this repository, one command builds what it needs and starts it:
33
+ To run this repository's build with its composition pinned and watched:
43
34
 
44
35
  ```bash
45
- pnpm cockpit
36
+ pnpm cockpit:build
37
+ pnpm doompi-web --dir=<PWD>
46
38
  ```
47
39
 
48
- Nx caches the build, so a warm run is effectively just the hub. Sessions created from the page run
49
- this checkout's builds, which matters: a session on some other `doompi` is a different composition,
50
- and its extensions, commands, and minor modes are not the ones being edited here, so a change looks
51
- like it did nothing.
40
+ `--dir` accepts `--dir <path>` too. It synchronizes that repository before launch and watches it for development rebuilds.
52
41
 
53
- | Flag | Default | Meaning |
54
- | ----------------- | --------------- | ------------------------------------------------ |
55
- | `--registry-dir` | `~/.doompi/run` | Registry to watch (also `DOOMPI_RUNTIME_DIR`) |
56
- | `--spawn-command` | this install | Command launching sessions created from the page |
57
- | `--port` | `7433` | HTTP port |
58
- | `--host` | `127.0.0.1` | Bind address |
59
- | `--assets` | bundled | Override the built SPA directory |
42
+ | Flag | Default | Meaning |
43
+ | ----------------- | ------------------------ | ------------------------------------------------ |
44
+ | `--dir` | live-session union | Pin, sync, and watch one repository composition |
45
+ | `--registry-dir` | `~/.doompi/run` | Registry to watch (also `DOOMPI_RUNTIME_DIR`) |
46
+ | `--spawn-command` | this install | Command launching sessions created from the page |
47
+ | `--port` | `7433` | HTTP port |
48
+ | `--host` | `127.0.0.1` | Bind address |
49
+ | `--assets` | synchronized or packaged | Override the built SPA directory |
50
+ | `--state-dir` | `~/.doompi/web` | Remote-access settings and cockpit cache |
51
+ | `--cloudflared` | from `PATH` | Tunnel binary (also `DOOMPI_CLOUDFLARED`) |
52
+ | `-h`, `--help` | | Print command help |
53
+ | `-v`, `--version` | | Print the installed package version |
60
54
 
61
- Installing this package gives you `doompi-web` and nothing else. For a session server you run
62
- yourself, install [doompi-server](https://www.npmjs.com/package/@agimon-ai/doompi-server); the hub
63
- does not need it on PATH to create sessions.
55
+ Install [doompi-server](https://www.npmjs.com/package/@agimon-ai/doompi-server) when you also want to run a session server directly. The hub does not require `doompi-server` on `PATH` to create sessions.
64
56
 
65
57
  ## Security
66
58
 
@@ -69,6 +61,24 @@ browser. The server binds loopback by default, so reaching the cockpit already m
69
61
  account on the machine. Remote access means tunnelling the port, for example over SSH; do not bind
70
62
  a public address and call it done.
71
63
 
64
+ Loopback binding is not on its own an authorization boundary, because two request shapes escape it
65
+ and both reach code execution:
66
+
67
+ - **WebSockets are exempt from CORS.** Any page in any browser on this machine can open a socket to
68
+ the cockpit's port, and `session_command` frames carry prompts straight to an agent holding a
69
+ `bash` tool.
70
+ - **A cross-origin POST need not be preflighted.** Sent as `text/plain`, it still parses as JSON
71
+ server-side, so a page could spawn a session in any directory without ever reading a response.
72
+
73
+ Both are refused. Every request and every socket upgrade is checked against an origin and host
74
+ allowlist derived from the port actually bound, plus the vite dev server. A request carrying no
75
+ `Origin` is allowed, because curl, the health probe, and the test harness send none and a browser
76
+ always sends one where it matters; this defends against a hostile web page, not against a hostile
77
+ local program, which can send whatever headers it likes. `DOOMPI_WEB_ALLOW_ORIGIN` takes a
78
+ comma-separated list for setups this package cannot guess. The host check is also the DNS rebinding
79
+ defence, and it is the only check a socket upgrade gets, since the upgrade path never reaches the
80
+ server's own host parsing.
81
+
72
82
  ## What the cockpit shows
73
83
 
74
84
  Everything comes from Pi's RPC protocol, so a bare Pi session and a DoomPi session both work and
@@ -85,13 +95,17 @@ neither is shown facts it did not report:
85
95
  usage, cost, and session identity.
86
96
  - **Dialogs:** the extension UI sub-protocol (`select`, `confirm`, `input`, `editor`), which is how
87
97
  a permission prompt reaches the browser. Cancelling answers the agent rather than stranding it.
98
+ - **Notifications:** live `doom-notification` entries from every attached session, including sessions
99
+ that are not focused. Browser permission is requested only after the user clicks **allow notifications**
100
+ in Settings. Replayed backlog and historical pages update the timeline but never notify.
88
101
  - **Leader Space:** `ctrl+k`, `ctrl+space`, or space in an empty composer. Keys walk the tree the
89
102
  installed web plugins declared (`SPC w r` opens the workflows tab, `SPC g e` toggles goal mode),
90
103
  backspace climbs, and `/` searches whatever `get_commands` reported to run it as a slash prompt.
91
- - **Settings:** the gear at the foot of the rail opens the settings pages. The providers page lists
92
- every model provider Pi knows with whether it is authenticated, signs in with an API key or OAuth
93
- through the hub, and signs out. The hub uses the same `auth.json` as the sessions, so a login
94
- here is a login for every session on the machine.
104
+ - **Settings:** the gear at the foot of the rail opens the settings pages. Notifications shows this
105
+ browser's permission and provides the user-click permission action. Providers lists every model
106
+ provider Pi knows with whether it is authenticated, signs in with an API key or OAuth through the
107
+ hub, and signs out. The hub uses the same `auth.json` as the sessions, so a login here is a login for
108
+ every session on the machine.
95
109
 
96
110
  ## Attach and reattach
97
111
 
@@ -110,6 +124,205 @@ perform the handshake itself: `attach` frames sent from the browser are refused.
110
124
  them.
111
125
  - Subagent, workflow, and runner surfaces report summaries only; per-run detail has no RPC source
112
126
  to render from yet.
127
+ - Open pages show full live browser notifications. An installed paired PWA can also receive a generic
128
+ zero-TTL Web Push alert while closed. There is no durable subscription database, outbox, replay, or
129
+ historical delivery.
130
+
131
+ ## iPhone Home Screen app
132
+
133
+ Use a stable named HTTPS tunnel such as `doompi.agimon.win`. Quick-tunnel hostnames rotate, so they cannot
134
+ provide durable PWA identity, passkeys, or reliable Push subscriptions. Open `/pair` in Safari, add it to the
135
+ Home Screen, then launch the installed app and scan the QR shown by the host.
136
+
137
+ The install shell, scanner, manifest, and verifier worker are package-owned. The QR pins the host signing key
138
+ and minimum bundle revision. The worker verifies every host and plugin asset before committing it to the
139
+ active cache, and the host never serves its SPA directly. A failed update keeps the last-known-good bundle.
140
+ Signer loss, signer rotation, or a host/container transition requires explicit fingerprint confirmation and
141
+ re-pairing. Session file previews remain sealed `no-store` HTTP Blob URLs and never enter the application
142
+ cache, IndexedDB, a signed manifest, or a Push payload.
143
+
144
+ Closed-app alerts are opt-in under settings. The server keeps subscriptions only in memory and sends fixed
145
+ generic copy with `TTL: 0` only when the device has no connected cockpit socket. Opening the app after a host
146
+ restart re-registers the browser subscription. Disabling alerts, revoking or expiring the device, switching
147
+ off remote access, or rotating process credentials removes it.
148
+
149
+ ## Remote access
150
+
151
+ Working from a phone means putting the cockpit on the internet, and the cockpit's socket carries
152
+ prompts to an agent holding a `bash` tool. Reaching it is therefore equivalent to a shell on this
153
+ machine, and the whole design follows from that.
154
+
155
+ Switching it on binds a **second loopback listener** on an ephemeral port and points `cloudflared`
156
+ at it. Port 7433 is untouched, so working at the keyboard is exactly as before, while everything
157
+ arriving on the tunnel listener has to prove it holds a paired session. The two listeners are the
158
+ mechanism, not an implementation detail: a tunnel connects from `127.0.0.1` like every local client,
159
+ so the socket a connection arrived on is the only thing about it that cannot be forged.
160
+
161
+ Pairing takes **two** things, not one:
162
+
163
+ 1. The host shows a QR encoding `https://<tunnel-host>/pair#c=<code>&k=<channel-key>&s=<signer>&r=<revision>`.
164
+ Every value rides in the URL fragment, which no browser sends to a server, so the relay cannot replace the
165
+ channel or bundle key without changing what was shown on the host. Manual pairing requires comparing the
166
+ signing-key fingerprint shown on both devices.
167
+ 2. Scanning raises an approve or deny prompt **on the host**. Nothing is paired until someone at the machine
168
+ answers. A screen is visible over a shoulder and in a screenshare, so the code alone is one factor for a
169
+ credential that ends in shell access.
170
+
171
+ A paired device then has the same powers as the person at the keyboard, with one exception: it
172
+ cannot mint pairing codes, approve devices, or change these settings. A device able to approve
173
+ devices could make its own access permanent, which is precisely what host confirmation prevents.
174
+ Turning remote access off and revoking a device are allowed remotely, because a panic button is
175
+ worth more from the couch than from the desk.
176
+
177
+ Two optional time limits, both **off by default** and both in the dialog:
178
+
179
+ - Close the tunnel automatically, after 60 minutes.
180
+ - Expire paired sessions, after 30 minutes idle or 12 hours total.
181
+
182
+ Either switch takes effect retroactively in both directions, because expiry is evaluated against the
183
+ setting in force at the moment of the check rather than stamped onto a session when it was made.
184
+ Turning expiry on therefore drops sessions that are already idle, and turning it off brings them
185
+ back.
186
+
187
+ With both off, a paired device keeps its session until it is revoked, remote access is switched off, or
188
+ the hub restarts. Switching remote access off always revokes every paired session, and that is not a
189
+ toggle.
190
+
191
+ Before reporting success, the tunnel is **self-tested through its own public URL**: the pairing page
192
+ must answer 200 and `/api/health` must answer 401. Anything else means the agent is on the public
193
+ internet unguarded, so the tunnel is killed immediately rather than warned about.
194
+
195
+ Requires `cloudflared` on `PATH` (`brew install cloudflared`). A quick tunnel needs no account but gets a new
196
+ hostname on every start. It does not provide a durable PWA, passkey, or Push identity, and Cloudflare does not
197
+ support SSE on quick tunnels, which breaks workflow and runner-log plugin surfaces. Use a stable named tunnel
198
+ for the Home Screen app.
199
+
200
+ If a named tunnel's connector exits unexpectedly, the hub makes three backoff restart attempts and
201
+ self-tests every replacement. The existing listener and paired sessions stay valid while recovery is
202
+ in progress. A quick tunnel cannot recover transparently because its origin changes, so it still fails
203
+ closed immediately. Exhausting named-tunnel recovery also switches remote access off and revokes the
204
+ paired sessions.
205
+
206
+ ### Passkeys and step-up
207
+
208
+ On a named tunnel, a device can enrol a passkey once and then sign in with a gesture instead of
209
+ another QR. Two actions need a fresh gesture even from a live session: writing or clearing a model
210
+ provider's credential, and spawning a session in a directory of the caller's choosing. Both are
211
+ escalation paths out of "drive the agent" and into "own the machine". Ordinary prompting and tool
212
+ approval are deliberately ungated, because a biometric check in that loop gets trained away in a day.
213
+
214
+ A quick tunnel cannot carry a passkey: `rpID` is bound to the hostname and a quick tunnel's rotates
215
+ on every start, so a credential enrolled today would silently stop working tomorrow. The cockpit says
216
+ so rather than letting the ceremony fail in the browser.
217
+
218
+ ### Tunnel-provider trust and payload privacy
219
+
220
+ Cloudflare terminates TLS and delivers the package-owned pairing and verifier bootstrap. A browser cannot
221
+ independently authenticate that first executable shell against the same edge that served it: a malicious
222
+ replacement could omit the worker and steal a QR fragment. DoomPi does not claim protection from a malicious
223
+ code-delivery edge.
224
+
225
+ After that bootstrap, **bundle signing is mandatory**. The QR pins the host ECDSA key and revision floor. The
226
+ worker verifies the signed manifest plus every raw asset before any host JavaScript executes, serves only the
227
+ atomically committed verified cache, and retains the last-known-good revision on failure. Cloudflare may
228
+ alter the raw bytes, but altered bytes do not execute.
229
+
230
+ **The payload is also sealed.** A QR carries an ephemeral P-256 public key in the fragment. A returning
231
+ passkey device restores its channel only after the pinned bundle worker accepts the same signer. Both paths
232
+ complete ECDH and derive separate AES-256-GCM keys per direction. Socket frames and API bodies travel as
233
+ ciphertext.
234
+
235
+ The channel implementation lives in `@agimon-ai/doompi-web-security`, which ships the envelope contract,
236
+ a `node:crypto` half, and a WebCrypto half. **A web plugin that calls `fetch` directly sends plaintext to
237
+ the relay**, so plugins use `sealedTransport.fetch` from that package's `./browser` subpath. The host
238
+ cannot enforce this, which is why the import allowlist admits the helper and why it is said here.
239
+
240
+ Cloudflare can still observe the served page and assets, timing, message sizes, connection patterns, and
241
+ tunnel metadata. Do not enable this transport if Cloudflare is outside your trust boundary. Protect a
242
+ named-tunnel account with a passkey or hardware MFA.
243
+
244
+ ### What a paired device may browse
245
+
246
+ While a tunnel is up, the new-session picker answers a request arriving on it from one subtree only:
247
+ the directory `doompi-web` was started in. Unpinned, that route hands a paired device a map of the
248
+ machine, every project and client name and checkout, which is worth more to an attacker than most of
249
+ what the cockpit shows.
250
+
251
+ Only that request is pinned. The person at the keyboard already has a shell, so pinning them would
252
+ cost the picker its usefulness and buy nothing, and a contained cockpit needs none of it because its
253
+ mounts are already the boundary.
254
+
255
+ Session creation itself is not narrowed by this. `POST /api/sessions` still accepts any absolute path
256
+ a caller already knows, which is why the container below exists: it is the boundary, this is only the
257
+ end of casual enumeration.
258
+
259
+ ### Running the cockpit in a container
260
+
261
+ Off by default. A second switch in the same dialog moves the whole cockpit into a container when
262
+ remote access is turned on: the hub, every session server it spawns, every agent, and `cloudflared`
263
+ itself. This is the answer to the blunt fact that a paired device drives an agent holding a shell,
264
+ which every layer above narrows access to but none of them contains.
265
+
266
+ The list of workspaces is the boundary, and it is the only thing on the host the container can see.
267
+ A hub inside the container cannot create a session in a path that is not mounted, so spawning in an
268
+ arbitrary directory is closed by construction rather than by a check. Not mounted, and deliberately
269
+ so: your home directory, `~/.ssh`, `~/.gitconfig`, the container socket, and every repository you
270
+ did not list.
271
+
272
+ What happens when you turn it on:
273
+
274
+ 1. The image is built if this DoomPi version has not built it before, which takes minutes the first
275
+ time. Progress is printed in the terminal that started the cockpit.
276
+ 2. The host cockpit answers the request, then stops serving and releases its port.
277
+ 3. The container starts, publishing the same port on loopback, and becomes the cockpit. Your browser
278
+ reconnects on its own; there is no new address to remember.
279
+ 4. Sessions working inside a mounted workspace are stopped on the host and recreated inside. Sessions
280
+ working anywhere else are named in the terminal and stay on the host, where the contained cockpit
281
+ cannot see them.
282
+
283
+ If the container fails to start, the host cockpit comes back and its sessions are recreated there, so
284
+ a failed move never leaves you with nothing listening. `Ctrl-C` stops the container. A cockpit killed
285
+ outright leaves the container running; the next start finds it through `~/.doompi/web/cockpit-container.json`
286
+ and stops it.
287
+
288
+ Two things carry across the boundary and nothing else does. Provider credentials go through the
289
+ existing broker, which hands the container a per-session token rather than your keys. Git identity is
290
+ passed as `GIT_AUTHOR_*` and `GIT_COMMITTER_*`, so an agent can commit but cannot push, because no
291
+ key crosses.
292
+
293
+ What this does not defend against, stated plainly: anyone who can talk to the container daemon can
294
+ escape any container it runs, so the engine is part of the trusted base. Sessions share one container
295
+ and can read each other's mounted workspaces. A mounted workspace is fully writable, so the agent can
296
+ still destroy the repository you gave it. Network access is unrestricted, exactly as it already is for
297
+ `doompi --sandbox`. And plugin tabs fall back to the built-in set until a bundle is synced inside.
298
+
299
+ Requires `@agimon-ai/doompi-sandbox` in the composition and a container engine on the host.
300
+
301
+ ## Planned: WebRTC transport
302
+
303
+ Today the page reaches the hub over two WebSockets, and any remote access means putting a tunnel in
304
+ front of them, which puts the tunnel provider in the path of everything. A WebRTC data channel would
305
+ replace that, with the tunnel demoted to carrying signaling only. Two independent reasons want it,
306
+ and the first is the stronger.
307
+
308
+ **Realtime voice.** `@agimon-ai/doompi-voice` now captures on the selected browser client, sends
309
+ mono PCM16 through its authenticated session media transport, keeps VAD and Whisper on the agent
310
+ host, and plays narration with browser speech synthesis. Voice activation is when the page asks for
311
+ microphone permission, never on load. A future WebRTC implementation can replace the HTTP media
312
+ adapter with Opus tracks while preserving the same client device and transport contracts.
313
+
314
+ **Confidentiality.** A QR shown on the host can carry the DTLS fingerprint, which authenticates the
315
+ peer connection out-of-band. The data path is then peer-to-peer DTLS: the tunnel provider usually
316
+ never carries the traffic and can never read it. That is close to VPN-grade confidentiality without
317
+ asking anyone to install a VPN.
318
+
319
+ The cost is real and worth stating: a WebRTC stack on the Node side (`node-datachannel` or
320
+ `werift`), a second client transport alongside `wsClient.ts` and `piTransport.ts`, ICE and NAT
321
+ traversal debugging, and a TURN relay fallback for the connections that cannot go direct. TURN
322
+ reintroduces a relay, though still one that cannot decrypt. The WebSocket transport stays as the
323
+ fallback for when a data channel cannot be established.
324
+
325
+ No date. This is a design note, not a commitment.
113
326
 
114
327
  ## Public API
115
328
 
@@ -159,14 +372,17 @@ package declares a `doompiWeb` block in its package.json naming a client entry (
159
372
  definition: tab, panel, badge, session data channels, tool renderers for the timeline cards of
160
373
  the tools the package registers, slots it opens for other plugins, fills into slots others open,
161
374
  and the host's own slot contributions) and an optional hub entry (`webHubChannels` plus its built
162
- dist). This package's own bundle carries no plugins; the full set is assembled on your machine:
163
- `doompi sync` discovers the installed composition's manifests, generates the import entries, and
164
- rebuilds the SPA through the `./bundler` subpath into `~/.doompi/web/current`, which the server
165
- prefers over the packaged bundle (`--assets` and `DOOMPI_WEB_DIST` override it). The synced bundle
166
- carries `webPlugins.server.json` naming each plugin's built hub entry, imported lazily: a missing
167
- plugin package logs a notice and its tab shows an empty state. The subagents tab in
168
- `@agimon-ai/doompi-team` is the reference plugin; the workflows tab ships with
169
- `@agimon-ai/doompi-workflow`. Contracts come from `@agimon-ai/doompi-web-contracts`.
375
+ dist). This package's own bundle carries no plugins; the full set is assembled on your machine.
376
+ `doompi sync` discovers the installed composition's manifests, generates the import entries, and builds
377
+ the SPA through the `./bundler` subpath inside the repository/worktree's immutable sync generation.
378
+ The shared server uses the validated registration for its startup repository. `--assets` and
379
+ `DOOMPI_WEB_DIST` override registered assets, then packaged assets provide the fallback. Server-only
380
+ `webPlugins.server.json` sits beside the public `web/` directory, never inside the signed or served
381
+ bundle. It names each plugin's built hub entry, imported lazily. A missing plugin package logs a notice
382
+ and its tab is omitted. Session package API routes use the session `cwd` registration, while
383
+ `DOOMPI_API_DIR` remains the explicit API override. `@agimon-ai/doompi-team` is the reference plugin;
384
+ the workflows tab ships with `@agimon-ai/doompi-workflow`. Contracts come from
385
+ `@agimon-ai/doompi-web-contracts`.
170
386
 
171
387
  A tool call's timeline item belongs to the plugin that registers the tool: its `toolRenderers`
172
388
  entry names the tool (or claims it with `matches` when the name is only known at runtime) and
@@ -180,7 +396,8 @@ Plugins are independent. A manifest names no other plugin and `registrationOrder
180
396
  optional tiebreak (default 1000, then plugin id): every relation between two plugins resolves by
181
397
  name once all of them are installed. A plugin opens slots named `<pluginId>.<name>` and renders
182
398
  them with the `renderSlot` and `slotData` props its components receive; any plugin fills them, and
183
- the host's `overlay`, `rail`, `selection-bar`, `activity`, and `activity.<group>` slots, without
399
+ the host's `overlay`, `rail`, `selection-bar`, `composer-actions`, `activity`, and
400
+ `activity.<group>` slots, without
184
401
  knowing whether the owner is installed. A fill into a slot nobody declares, or two plugins wanting
185
402
  one tab id, tool name, group name, or Leader Space leaf, never fails the page: the install resolves
186
403
  it (the first plugin keeps a shared name, a later binding takes a leaf over as the TUI documents)
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../src/services/registryStore.cjs"),n=require("../src/adapters/httpServer.cjs"),r=require("../src/services/serveOptions.cjs"),i=require("../src/adapters/hubProbe.cjs");let a=require("node:os");a=e.__toESM(a,1);function o(e){process.stderr.write(`[doompi-web] ${e}\n`)}async function s(){let e=r.parseServeOptions(process.argv.slice(2)),s=`http://${e.host}:${String(e.port)}`;if(await i.hubAnswers(e.host,e.port)){o(`cockpit already running at ${s}`);return}let c;try{c=await n.serveWeb({port:e.port,host:e.host,assetsDir:e.assetsDir,registryDir:t.resolveRegistryDir({flagValue:e.registryDir,envValue:process.env[t.REGISTRY_DIR_ENV],homeDir:a.default.homedir()}),spawnCommand:e.spawnCommand,onNotice:o})}catch(t){if(t.code!==`EADDRINUSE`)throw t;if(await i.hubAnswers(e.host,e.port)){o(`cockpit already running at ${s}`);return}throw Error(`Port ${String(e.port)} is taken by something that is not a DoomPi cockpit; pass --port.`)}let l=()=>{c.close().then(()=>{process.exit(0)})};process.on(`SIGINT`,l),process.on(`SIGTERM`,l)}s().catch(e=>{o(e instanceof Error?e.message:String(e)),process.exitCode=1});
2
+ const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../src/services/registryStore.cjs"),n=require("../src/adapters/remoteAccessStore.cjs"),r=require("../src/adapters/httpServer.cjs"),i=require("../src/services/serveOptions.cjs"),a=require("../src/adapters/cockpitContainer.cjs"),o=require("../src/adapters/cockpitHandoff.cjs"),s=require("../src/adapters/doomInitialization.cjs"),c=require("../src/adapters/hubProbe.cjs"),l=require("../src/adapters/sessionRelaunch.cjs");let u=require("node:os");u=e.__toESM(u,1);function d(e){process.stderr.write(`[doompi-web] ${e}\n`)}async function f(e,t,n){try{process.kill(e,`SIGTERM`)}catch{}let r=Date.now()+1e4;for(;Date.now()<r;)if(await new Promise(e=>setTimeout(e,100)),!await c.hubAnswers(t,n))return!0;return!1}async function p(){let e=i.parseServeOptions(process.argv.slice(2));if(e.help){process.stdout.write(i.serveHelp());return}if(e.version){process.stdout.write(`${r.packagedVersion()}\n`);return}await s.ensureDoomInitialized({homeDirectory:u.default.homedir(),onNotice:d});let o=`http://${e.host}:${String(e.port)}`,l=e.stateDir??n.defaultRemoteStateDir(),p=a.createCockpitContainer({stateDir:l,onNotice:d});await p.reapStale(),i.isLoopbackHost(e.host)||(d(`WARNING: --host ${e.host} is not loopback, so the cockpit is reachable without pairing.`),d(`WARNING: turn on remote access in settings instead, which requires a paired device.`));let h=await c.probeHub(e.host,e.port);if(h!==void 0){let t=r.packagedVersion(),n=h.version??`an older build`;if(h.version===t){d(`cockpit already running at ${o}`);return}if(h.sessions>0){d(`WARNING: ${n} is already running at ${o} with ${String(h.sessions)} live session(s).`),d(`WARNING: stop it and run again to serve ${t}; refusing to interrupt running work.`);return}if(h.pid===void 0||!i.isLoopbackHost(e.host)){d(`WARNING: ${n} is already running at ${o} and cannot be replaced automatically.`),d(`WARNING: stop it and run again to serve ${t}.`);return}if(d(`replacing ${n} at ${o} with ${t}`),!await f(h.pid,e.host,e.port)){d(`WARNING: the hub at ${o} did not stop; serve ${t} on another port with --port.`);return}}let g=async()=>{},_=async()=>{let n=await r.serveWeb({port:e.port,host:e.host,assetsDir:e.assetsDir,compositionDir:e.directory,registryDir:t.resolveRegistryDir({flagValue:e.registryDir,envValue:process.env[t.REGISTRY_DIR_ENV],homeDir:u.default.homedir()}),spawnCommand:e.spawnCommand,remoteStateDir:l,cloudflaredPath:e.cloudflaredPath,onNotice:d,onHandover:t=>{m({serving:n,restart:_,container:p,handover:t,port:e.port,host:e.host,adopt:e=>{g=e}})}});return g=async()=>{await n.close()},n};try{await _()}catch(t){if(t.code!==`EADDRINUSE`)throw t;if(await c.hubAnswers(e.host,e.port)){d(`cockpit already running at ${o}`);return}throw Error(`Port ${String(e.port)} is taken by something that is not a DoomPi cockpit; pass --port.`)}let v=()=>{g().then(()=>{process.exit(0)})};process.on(`SIGINT`,v),process.on(`SIGTERM`,v)}async function m(e){await e.serving.close();let t=await e.container.start({workspaces:e.handover.settings.sandbox.workspaces.map(e=>({path:e})),port:e.port,onProgress:d});if(!t.ok){d(`the cockpit could not move into a container: ${t.error}`),d(`serving from the host again`),await e.restart(),await l.relaunchSessions({port:e.port,host:e.host,sessions:e.handover.sessions,onNotice:d});return}e.adopt(async()=>{await t.container.stop()}),await o.handOffRemoteAccess({port:e.port,settings:e.handover.settings,onNotice:d}),await l.relaunchSessions({port:e.port,sessions:e.handover.sessions,onNotice:d}),d(`supervising the cockpit container; Ctrl-C stops it`),await t.container.supervise()}p().catch(e=>{d(e instanceof Error?e.message:String(e)),process.exitCode=1});
3
3
  //# sourceMappingURL=serve.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"serve.cjs","names":["parseServeOptions","hubAnswers","serveWeb","resolveRegistryDir","REGISTRY_DIR_ENV","os"],"sources":["../../src/bin/serve.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport os from 'node:os';\nimport { hubAnswers } from '../adapters/hubProbe.ts';\nimport { serveWeb } from '../adapters/httpServer.ts';\nimport { REGISTRY_DIR_ENV, resolveRegistryDir } from '../services/registryStore.ts';\nimport { parseServeOptions } from '../services/serveOptions.ts';\nimport type { WebServer } from '../types/bridge.ts';\n\nconst ADDRESS_IN_USE = 'EADDRINUSE';\n\nfunction notice(message: string): void {\n process.stderr.write(`[doompi-web] ${message}\\n`);\n}\n\nasync function main(): Promise<void> {\n const options = parseServeOptions(process.argv.slice(2));\n const url = `http://${options.host}:${String(options.port)}`;\n\n // One hub serves every session, so a second start is a request to use the\n // running one, not an error worth a stack trace.\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n\n let server: WebServer;\n try {\n server = await serveWeb({\n port: options.port,\n host: options.host,\n assetsDir: options.assetsDir,\n registryDir: resolveRegistryDir({\n flagValue: options.registryDir,\n envValue: process.env[REGISTRY_DIR_ENV],\n homeDir: os.homedir(),\n }),\n spawnCommand: options.spawnCommand,\n onNotice: notice,\n });\n } catch (error) {\n // Two cockpits starting together can both find the port free; the loser\n // settles for the winner rather than reporting a clash nobody caused.\n if ((error as NodeJS.ErrnoException).code !== ADDRESS_IN_USE) throw error;\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n throw new Error(`Port ${String(options.port)} is taken by something that is not a DoomPi cockpit; pass --port.`);\n }\n\n const stop = (): void => {\n void server.close().then(() => {\n process.exit(0);\n });\n };\n process.on('SIGINT', stop);\n process.on('SIGTERM', stop);\n}\n\nmain().catch((error: unknown) => {\n notice(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n"],"mappings":";kRAWA,SAAS,EAAO,EAAuB,CACrC,QAAQ,OAAO,MAAM,gBAAgB,EAAQ,GAAG,CAClD,CAEA,eAAe,GAAsB,CACnC,IAAM,EAAUA,EAAAA,kBAAkB,QAAQ,KAAK,MAAM,CAAC,CAAC,EACjD,EAAM,UAAU,EAAQ,KAAK,GAAG,OAAO,EAAQ,IAAI,IAIzD,GAAI,MAAMC,EAAAA,WAAW,EAAQ,KAAM,EAAQ,IAAI,EAAG,CAChD,EAAO,8BAA8B,GAAK,EAC1C,MACF,CAEA,IAAI,EACJ,GAAI,CACF,EAAS,MAAMC,EAAAA,SAAS,CACtB,KAAM,EAAQ,KACd,KAAM,EAAQ,KACd,UAAW,EAAQ,UACnB,YAAaC,EAAAA,mBAAmB,CAC9B,UAAW,EAAQ,YACnB,SAAU,QAAQ,IAAIC,EAAAA,kBACtB,QAASC,EAAAA,QAAG,QAAQ,CACtB,CAAC,EACD,aAAc,EAAQ,aACtB,SAAU,CACZ,CAAC,CACH,OAAS,EAAO,CAGd,GAAK,EAAgC,OAAS,aAAgB,MAAM,EACpE,GAAI,MAAMJ,EAAAA,WAAW,EAAQ,KAAM,EAAQ,IAAI,EAAG,CAChD,EAAO,8BAA8B,GAAK,EAC1C,MACF,CACA,MAAU,MAAM,QAAQ,OAAO,EAAQ,IAAI,EAAE,kEAAkE,CACjH,CAEA,IAAM,MAAmB,CACvB,EAAY,MAAM,CAAC,CAAC,SAAW,CAC7B,QAAQ,KAAK,CAAC,CAChB,CAAC,CACH,EACA,QAAQ,GAAG,SAAU,CAAI,EACzB,QAAQ,GAAG,UAAW,CAAI,CAC5B,CAEA,EAAK,CAAC,CAAC,MAAO,GAAmB,CAC/B,EAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CAAC,EAC7D,QAAQ,SAAW,CACrB,CAAC"}
1
+ {"version":3,"file":"serve.cjs","names":["hubAnswers","parseServeOptions","serveHelp","packagedVersion","ensureDoomInitialized","os","defaultRemoteStateDir","createCockpitContainer","isLoopbackHost","probeHub","serveWeb","resolveRegistryDir","REGISTRY_DIR_ENV","relaunchSessions","handOffRemoteAccess"],"sources":["../../src/bin/serve.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport os from 'node:os';\nimport { createCockpitContainer } from '../adapters/cockpitContainer.ts';\nimport { handOffRemoteAccess } from '../adapters/cockpitHandoff.ts';\nimport { ensureDoomInitialized } from '../adapters/doomInitialization.ts';\nimport { hubAnswers, probeHub } from '../adapters/hubProbe.ts';\nimport { packagedVersion, serveWeb } from '../adapters/httpServer.ts';\nimport { defaultRemoteStateDir } from '../adapters/remoteAccessStore.ts';\nimport { relaunchSessions } from '../adapters/sessionRelaunch.ts';\nimport { REGISTRY_DIR_ENV, resolveRegistryDir } from '../services/registryStore.ts';\nimport { isLoopbackHost, parseServeOptions, serveHelp } from '../services/serveOptions.ts';\nimport type { WebServer } from '../types/bridge.ts';\nimport type { MigratingSession, RemoteAccessSettings } from '../types/remoteAccess.ts';\n\nconst ADDRESS_IN_USE = 'EADDRINUSE';\nconst STOP_POLL_MS = 100;\nconst STOP_TIMEOUT_MS = 10_000;\n\nfunction notice(message: string): void {\n process.stderr.write(`[doompi-web] ${message}\\n`);\n}\n\n/**\n * Signals the running hub and waits for the address to go quiet.\n *\n * Only ever called for a loopback address, because the pid on the other end of\n * the probe is only this machine's pid when the hub is on this machine.\n */\nasync function stopRunningHub(pid: number, host: string, port: number): Promise<boolean> {\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n // Already gone, or not ours to signal. The probe below settles it either way.\n }\n const deadline = Date.now() + STOP_TIMEOUT_MS;\n while (Date.now() < deadline) {\n await new Promise((resolve) => setTimeout(resolve, STOP_POLL_MS));\n if (!(await hubAnswers(host, port))) return true;\n }\n return false;\n}\n\nasync function main(): Promise<void> {\n const options = parseServeOptions(process.argv.slice(2));\n if (options.help) {\n process.stdout.write(serveHelp());\n return;\n }\n if (options.version) {\n process.stdout.write(`${packagedVersion()}\\n`);\n return;\n }\n await ensureDoomInitialized({ homeDirectory: os.homedir(), onNotice: notice });\n const url = `http://${options.host}:${String(options.port)}`;\n const stateDir = options.stateDir ?? defaultRemoteStateDir();\n const container = createCockpitContainer({ stateDir, onNotice: notice });\n // A cockpit that was killed rather than closed leaves its container holding\n // the published port, so the next start would fail to bind for a reason\n // nothing explains.\n await container.reapStale();\n\n // Binding off loopback publishes an unauthenticated cockpit to whoever can\n // reach the address. Remote access exists so nobody has to do this; a warning\n // rather than a refusal, because an existing setup should not break today.\n if (!isLoopbackHost(options.host)) {\n notice(`WARNING: --host ${options.host} is not loopback, so the cockpit is reachable without pairing.`);\n notice('WARNING: turn on remote access in settings instead, which requires a paired device.');\n }\n\n // One hub serves every session, so a second start is normally a request to\n // use the running one. A second start from a *different* version is not: a\n // long-lived hub signs its asset directory once, so handing back to it would\n // serve the older cockpit for as long as it lives, and a browser that already\n // verified that bundle would never be offered a newer one.\n const running = await probeHub(options.host, options.port);\n if (running !== undefined) {\n const mine = packagedVersion();\n const theirs = running.version ?? 'an older build';\n if (running.version === mine) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n if (running.sessions > 0) {\n notice(`WARNING: ${theirs} is already running at ${url} with ${String(running.sessions)} live session(s).`);\n notice(`WARNING: stop it and run again to serve ${mine}; refusing to interrupt running work.`);\n return;\n }\n if (running.pid === undefined || !isLoopbackHost(options.host)) {\n notice(`WARNING: ${theirs} is already running at ${url} and cannot be replaced automatically.`);\n notice(`WARNING: stop it and run again to serve ${mine}.`);\n return;\n }\n notice(`replacing ${theirs} at ${url} with ${mine}`);\n if (!(await stopRunningHub(running.pid, options.host, options.port))) {\n notice(`WARNING: the hub at ${url} did not stop; serve ${mine} on another port with --port.`);\n return;\n }\n }\n\n /**\n * What Ctrl-C stops, which is not the same thing throughout the run.\n *\n * A single indirection rather than swapping signal handlers, so the handover\n * never has to reach for `removeAllListeners` and take out whatever else this\n * process has registered.\n */\n let shutDown: () => Promise<void> = async () => {};\n\n const start = async (): Promise<WebServer> => {\n const server = await serveWeb({\n port: options.port,\n host: options.host,\n assetsDir: options.assetsDir,\n compositionDir: options.directory,\n registryDir: resolveRegistryDir({\n flagValue: options.registryDir,\n envValue: process.env[REGISTRY_DIR_ENV],\n homeDir: os.homedir(),\n }),\n spawnCommand: options.spawnCommand,\n remoteStateDir: stateDir,\n cloudflaredPath: options.cloudflaredPath,\n onNotice: notice,\n // Already deferred past its own response by the route that asked, so by\n // the time this runs the caller has its answer and the server can go.\n onHandover: (handover) => {\n void handOver({\n serving: server,\n restart: start,\n container,\n handover,\n port: options.port,\n host: options.host,\n adopt: (next) => {\n shutDown = next;\n },\n });\n },\n });\n shutDown = async () => {\n await server.close();\n };\n return server;\n };\n\n try {\n await start();\n } catch (error) {\n // Two cockpits starting together can both find the port free; the loser\n // settles for the winner rather than reporting a clash nobody caused.\n if ((error as NodeJS.ErrnoException).code !== ADDRESS_IN_USE) throw error;\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n throw new Error(`Port ${String(options.port)} is taken by something that is not a DoomPi cockpit; pass --port.`);\n }\n\n const stop = (): void => {\n void shutDown().then(() => {\n process.exit(0);\n });\n };\n process.on('SIGINT', stop);\n process.on('SIGTERM', stop);\n}\n\n/**\n * Hands this cockpit over to one running in a container.\n *\n * The port has to be free before the container can publish onto it, so the\n * order is: stop serving, start the container, and put the host cockpit back if\n * the container does not come up. Leaving the user with nothing listening would\n * be the worst outcome of the three, which is why the rollback also recreates\n * the sessions that were stopped in preparation for a move that did not happen.\n */\nasync function handOver(input: {\n serving: WebServer;\n restart: () => Promise<WebServer>;\n container: ReturnType<typeof createCockpitContainer>;\n handover: { settings: RemoteAccessSettings; sessions: readonly MigratingSession[] };\n port: number;\n host: string;\n adopt: (shutDown: () => Promise<void>) => void;\n}): Promise<void> {\n await input.serving.close();\n const started = await input.container.start({\n workspaces: input.handover.settings.sandbox.workspaces.map((workspace) => ({ path: workspace })),\n port: input.port,\n onProgress: notice,\n });\n if (!started.ok) {\n notice(`the cockpit could not move into a container: ${started.error}`);\n notice('serving from the host again');\n await input.restart();\n await relaunchSessions({\n port: input.port,\n host: input.host,\n sessions: input.handover.sessions,\n onNotice: notice,\n });\n return;\n }\n input.adopt(async () => {\n await started.container.stop();\n });\n // The container answers its own health probe before `start` resolves, so its\n // hub is already there to take the settings and the sessions.\n await handOffRemoteAccess({ port: input.port, settings: input.handover.settings, onNotice: notice });\n await relaunchSessions({ port: input.port, sessions: input.handover.sessions, onNotice: notice });\n // Nothing else holds this process open now that its server is closed, and a\n // supervisor that exits leaves a container nobody will ever stop.\n notice('supervising the cockpit container; Ctrl-C stops it');\n await started.container.supervise();\n}\n\nmain().catch((error: unknown) => {\n notice(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import{REGISTRY_DIR_ENV as e,resolveRegistryDir as t}from"../src/services/registryStore.mjs";import{serveWeb as n}from"../src/adapters/httpServer.mjs";import{parseServeOptions as r}from"../src/services/serveOptions.mjs";import{hubAnswers as i}from"../src/adapters/hubProbe.mjs";import a from"node:os";function o(e){process.stderr.write(`[doompi-web] ${e}\n`)}async function s(){let s=r(process.argv.slice(2)),c=`http://${s.host}:${String(s.port)}`;if(await i(s.host,s.port)){o(`cockpit already running at ${c}`);return}let l;try{l=await n({port:s.port,host:s.host,assetsDir:s.assetsDir,registryDir:t({flagValue:s.registryDir,envValue:process.env[e],homeDir:a.homedir()}),spawnCommand:s.spawnCommand,onNotice:o})}catch(e){if(e.code!==`EADDRINUSE`)throw e;if(await i(s.host,s.port)){o(`cockpit already running at ${c}`);return}throw Error(`Port ${String(s.port)} is taken by something that is not a DoomPi cockpit; pass --port.`)}let u=()=>{l.close().then(()=>{process.exit(0)})};process.on(`SIGINT`,u),process.on(`SIGTERM`,u)}s().catch(e=>{o(e instanceof Error?e.message:String(e)),process.exitCode=1});export{};
2
+ import{REGISTRY_DIR_ENV as e,resolveRegistryDir as t}from"../src/services/registryStore.mjs";import{defaultRemoteStateDir as n}from"../src/adapters/remoteAccessStore.mjs";import{packagedVersion as r,serveWeb as i}from"../src/adapters/httpServer.mjs";import{isLoopbackHost as a,parseServeOptions as o,serveHelp as s}from"../src/services/serveOptions.mjs";import{createCockpitContainer as c}from"../src/adapters/cockpitContainer.mjs";import{handOffRemoteAccess as l}from"../src/adapters/cockpitHandoff.mjs";import{ensureDoomInitialized as u}from"../src/adapters/doomInitialization.mjs";import{hubAnswers as d,probeHub as f}from"../src/adapters/hubProbe.mjs";import{relaunchSessions as p}from"../src/adapters/sessionRelaunch.mjs";import m from"node:os";function h(e){process.stderr.write(`[doompi-web] ${e}\n`)}async function g(e,t,n){try{process.kill(e,`SIGTERM`)}catch{}let r=Date.now()+1e4;for(;Date.now()<r;)if(await new Promise(e=>setTimeout(e,100)),!await d(t,n))return!0;return!1}async function _(){let l=o(process.argv.slice(2));if(l.help){process.stdout.write(s());return}if(l.version){process.stdout.write(`${r()}\n`);return}await u({homeDirectory:m.homedir(),onNotice:h});let p=`http://${l.host}:${String(l.port)}`,_=l.stateDir??n(),y=c({stateDir:_,onNotice:h});await y.reapStale(),a(l.host)||(h(`WARNING: --host ${l.host} is not loopback, so the cockpit is reachable without pairing.`),h(`WARNING: turn on remote access in settings instead, which requires a paired device.`));let b=await f(l.host,l.port);if(b!==void 0){let e=r(),t=b.version??`an older build`;if(b.version===e){h(`cockpit already running at ${p}`);return}if(b.sessions>0){h(`WARNING: ${t} is already running at ${p} with ${String(b.sessions)} live session(s).`),h(`WARNING: stop it and run again to serve ${e}; refusing to interrupt running work.`);return}if(b.pid===void 0||!a(l.host)){h(`WARNING: ${t} is already running at ${p} and cannot be replaced automatically.`),h(`WARNING: stop it and run again to serve ${e}.`);return}if(h(`replacing ${t} at ${p} with ${e}`),!await g(b.pid,l.host,l.port)){h(`WARNING: the hub at ${p} did not stop; serve ${e} on another port with --port.`);return}}let x=async()=>{},S=async()=>{let n=await i({port:l.port,host:l.host,assetsDir:l.assetsDir,compositionDir:l.directory,registryDir:t({flagValue:l.registryDir,envValue:process.env[e],homeDir:m.homedir()}),spawnCommand:l.spawnCommand,remoteStateDir:_,cloudflaredPath:l.cloudflaredPath,onNotice:h,onHandover:e=>{v({serving:n,restart:S,container:y,handover:e,port:l.port,host:l.host,adopt:e=>{x=e}})}});return x=async()=>{await n.close()},n};try{await S()}catch(e){if(e.code!==`EADDRINUSE`)throw e;if(await d(l.host,l.port)){h(`cockpit already running at ${p}`);return}throw Error(`Port ${String(l.port)} is taken by something that is not a DoomPi cockpit; pass --port.`)}let C=()=>{x().then(()=>{process.exit(0)})};process.on(`SIGINT`,C),process.on(`SIGTERM`,C)}async function v(e){await e.serving.close();let t=await e.container.start({workspaces:e.handover.settings.sandbox.workspaces.map(e=>({path:e})),port:e.port,onProgress:h});if(!t.ok){h(`the cockpit could not move into a container: ${t.error}`),h(`serving from the host again`),await e.restart(),await p({port:e.port,host:e.host,sessions:e.handover.sessions,onNotice:h});return}e.adopt(async()=>{await t.container.stop()}),await l({port:e.port,settings:e.handover.settings,onNotice:h}),await p({port:e.port,sessions:e.handover.sessions,onNotice:h}),h(`supervising the cockpit container; Ctrl-C stops it`),await t.container.supervise()}_().catch(e=>{h(e instanceof Error?e.message:String(e)),process.exitCode=1});export{};
3
3
  //# sourceMappingURL=serve.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"serve.mjs","names":[],"sources":["../../src/bin/serve.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport os from 'node:os';\nimport { hubAnswers } from '../adapters/hubProbe.ts';\nimport { serveWeb } from '../adapters/httpServer.ts';\nimport { REGISTRY_DIR_ENV, resolveRegistryDir } from '../services/registryStore.ts';\nimport { parseServeOptions } from '../services/serveOptions.ts';\nimport type { WebServer } from '../types/bridge.ts';\n\nconst ADDRESS_IN_USE = 'EADDRINUSE';\n\nfunction notice(message: string): void {\n process.stderr.write(`[doompi-web] ${message}\\n`);\n}\n\nasync function main(): Promise<void> {\n const options = parseServeOptions(process.argv.slice(2));\n const url = `http://${options.host}:${String(options.port)}`;\n\n // One hub serves every session, so a second start is a request to use the\n // running one, not an error worth a stack trace.\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n\n let server: WebServer;\n try {\n server = await serveWeb({\n port: options.port,\n host: options.host,\n assetsDir: options.assetsDir,\n registryDir: resolveRegistryDir({\n flagValue: options.registryDir,\n envValue: process.env[REGISTRY_DIR_ENV],\n homeDir: os.homedir(),\n }),\n spawnCommand: options.spawnCommand,\n onNotice: notice,\n });\n } catch (error) {\n // Two cockpits starting together can both find the port free; the loser\n // settles for the winner rather than reporting a clash nobody caused.\n if ((error as NodeJS.ErrnoException).code !== ADDRESS_IN_USE) throw error;\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n throw new Error(`Port ${String(options.port)} is taken by something that is not a DoomPi cockpit; pass --port.`);\n }\n\n const stop = (): void => {\n void server.close().then(() => {\n process.exit(0);\n });\n };\n process.on('SIGINT', stop);\n process.on('SIGTERM', stop);\n}\n\nmain().catch((error: unknown) => {\n notice(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n"],"mappings":";6SAWA,SAAS,EAAO,EAAuB,CACrC,QAAQ,OAAO,MAAM,gBAAgB,EAAQ,GAAG,CAClD,CAEA,eAAe,GAAsB,CACnC,IAAM,EAAU,EAAkB,QAAQ,KAAK,MAAM,CAAC,CAAC,EACjD,EAAM,UAAU,EAAQ,KAAK,GAAG,OAAO,EAAQ,IAAI,IAIzD,GAAI,MAAM,EAAW,EAAQ,KAAM,EAAQ,IAAI,EAAG,CAChD,EAAO,8BAA8B,GAAK,EAC1C,MACF,CAEA,IAAI,EACJ,GAAI,CACF,EAAS,MAAM,EAAS,CACtB,KAAM,EAAQ,KACd,KAAM,EAAQ,KACd,UAAW,EAAQ,UACnB,YAAa,EAAmB,CAC9B,UAAW,EAAQ,YACnB,SAAU,QAAQ,IAAI,GACtB,QAAS,EAAG,QAAQ,CACtB,CAAC,EACD,aAAc,EAAQ,aACtB,SAAU,CACZ,CAAC,CACH,OAAS,EAAO,CAGd,GAAK,EAAgC,OAAS,aAAgB,MAAM,EACpE,GAAI,MAAM,EAAW,EAAQ,KAAM,EAAQ,IAAI,EAAG,CAChD,EAAO,8BAA8B,GAAK,EAC1C,MACF,CACA,MAAU,MAAM,QAAQ,OAAO,EAAQ,IAAI,EAAE,kEAAkE,CACjH,CAEA,IAAM,MAAmB,CACvB,EAAY,MAAM,CAAC,CAAC,SAAW,CAC7B,QAAQ,KAAK,CAAC,CAChB,CAAC,CACH,EACA,QAAQ,GAAG,SAAU,CAAI,EACzB,QAAQ,GAAG,UAAW,CAAI,CAC5B,CAEA,EAAK,CAAC,CAAC,MAAO,GAAmB,CAC/B,EAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CAAC,EAC7D,QAAQ,SAAW,CACrB,CAAC"}
1
+ {"version":3,"file":"serve.mjs","names":[],"sources":["../../src/bin/serve.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport os from 'node:os';\nimport { createCockpitContainer } from '../adapters/cockpitContainer.ts';\nimport { handOffRemoteAccess } from '../adapters/cockpitHandoff.ts';\nimport { ensureDoomInitialized } from '../adapters/doomInitialization.ts';\nimport { hubAnswers, probeHub } from '../adapters/hubProbe.ts';\nimport { packagedVersion, serveWeb } from '../adapters/httpServer.ts';\nimport { defaultRemoteStateDir } from '../adapters/remoteAccessStore.ts';\nimport { relaunchSessions } from '../adapters/sessionRelaunch.ts';\nimport { REGISTRY_DIR_ENV, resolveRegistryDir } from '../services/registryStore.ts';\nimport { isLoopbackHost, parseServeOptions, serveHelp } from '../services/serveOptions.ts';\nimport type { WebServer } from '../types/bridge.ts';\nimport type { MigratingSession, RemoteAccessSettings } from '../types/remoteAccess.ts';\n\nconst ADDRESS_IN_USE = 'EADDRINUSE';\nconst STOP_POLL_MS = 100;\nconst STOP_TIMEOUT_MS = 10_000;\n\nfunction notice(message: string): void {\n process.stderr.write(`[doompi-web] ${message}\\n`);\n}\n\n/**\n * Signals the running hub and waits for the address to go quiet.\n *\n * Only ever called for a loopback address, because the pid on the other end of\n * the probe is only this machine's pid when the hub is on this machine.\n */\nasync function stopRunningHub(pid: number, host: string, port: number): Promise<boolean> {\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n // Already gone, or not ours to signal. The probe below settles it either way.\n }\n const deadline = Date.now() + STOP_TIMEOUT_MS;\n while (Date.now() < deadline) {\n await new Promise((resolve) => setTimeout(resolve, STOP_POLL_MS));\n if (!(await hubAnswers(host, port))) return true;\n }\n return false;\n}\n\nasync function main(): Promise<void> {\n const options = parseServeOptions(process.argv.slice(2));\n if (options.help) {\n process.stdout.write(serveHelp());\n return;\n }\n if (options.version) {\n process.stdout.write(`${packagedVersion()}\\n`);\n return;\n }\n await ensureDoomInitialized({ homeDirectory: os.homedir(), onNotice: notice });\n const url = `http://${options.host}:${String(options.port)}`;\n const stateDir = options.stateDir ?? defaultRemoteStateDir();\n const container = createCockpitContainer({ stateDir, onNotice: notice });\n // A cockpit that was killed rather than closed leaves its container holding\n // the published port, so the next start would fail to bind for a reason\n // nothing explains.\n await container.reapStale();\n\n // Binding off loopback publishes an unauthenticated cockpit to whoever can\n // reach the address. Remote access exists so nobody has to do this; a warning\n // rather than a refusal, because an existing setup should not break today.\n if (!isLoopbackHost(options.host)) {\n notice(`WARNING: --host ${options.host} is not loopback, so the cockpit is reachable without pairing.`);\n notice('WARNING: turn on remote access in settings instead, which requires a paired device.');\n }\n\n // One hub serves every session, so a second start is normally a request to\n // use the running one. A second start from a *different* version is not: a\n // long-lived hub signs its asset directory once, so handing back to it would\n // serve the older cockpit for as long as it lives, and a browser that already\n // verified that bundle would never be offered a newer one.\n const running = await probeHub(options.host, options.port);\n if (running !== undefined) {\n const mine = packagedVersion();\n const theirs = running.version ?? 'an older build';\n if (running.version === mine) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n if (running.sessions > 0) {\n notice(`WARNING: ${theirs} is already running at ${url} with ${String(running.sessions)} live session(s).`);\n notice(`WARNING: stop it and run again to serve ${mine}; refusing to interrupt running work.`);\n return;\n }\n if (running.pid === undefined || !isLoopbackHost(options.host)) {\n notice(`WARNING: ${theirs} is already running at ${url} and cannot be replaced automatically.`);\n notice(`WARNING: stop it and run again to serve ${mine}.`);\n return;\n }\n notice(`replacing ${theirs} at ${url} with ${mine}`);\n if (!(await stopRunningHub(running.pid, options.host, options.port))) {\n notice(`WARNING: the hub at ${url} did not stop; serve ${mine} on another port with --port.`);\n return;\n }\n }\n\n /**\n * What Ctrl-C stops, which is not the same thing throughout the run.\n *\n * A single indirection rather than swapping signal handlers, so the handover\n * never has to reach for `removeAllListeners` and take out whatever else this\n * process has registered.\n */\n let shutDown: () => Promise<void> = async () => {};\n\n const start = async (): Promise<WebServer> => {\n const server = await serveWeb({\n port: options.port,\n host: options.host,\n assetsDir: options.assetsDir,\n compositionDir: options.directory,\n registryDir: resolveRegistryDir({\n flagValue: options.registryDir,\n envValue: process.env[REGISTRY_DIR_ENV],\n homeDir: os.homedir(),\n }),\n spawnCommand: options.spawnCommand,\n remoteStateDir: stateDir,\n cloudflaredPath: options.cloudflaredPath,\n onNotice: notice,\n // Already deferred past its own response by the route that asked, so by\n // the time this runs the caller has its answer and the server can go.\n onHandover: (handover) => {\n void handOver({\n serving: server,\n restart: start,\n container,\n handover,\n port: options.port,\n host: options.host,\n adopt: (next) => {\n shutDown = next;\n },\n });\n },\n });\n shutDown = async () => {\n await server.close();\n };\n return server;\n };\n\n try {\n await start();\n } catch (error) {\n // Two cockpits starting together can both find the port free; the loser\n // settles for the winner rather than reporting a clash nobody caused.\n if ((error as NodeJS.ErrnoException).code !== ADDRESS_IN_USE) throw error;\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n throw new Error(`Port ${String(options.port)} is taken by something that is not a DoomPi cockpit; pass --port.`);\n }\n\n const stop = (): void => {\n void shutDown().then(() => {\n process.exit(0);\n });\n };\n process.on('SIGINT', stop);\n process.on('SIGTERM', stop);\n}\n\n/**\n * Hands this cockpit over to one running in a container.\n *\n * The port has to be free before the container can publish onto it, so the\n * order is: stop serving, start the container, and put the host cockpit back if\n * the container does not come up. Leaving the user with nothing listening would\n * be the worst outcome of the three, which is why the rollback also recreates\n * the sessions that were stopped in preparation for a move that did not happen.\n */\nasync function handOver(input: {\n serving: WebServer;\n restart: () => Promise<WebServer>;\n container: ReturnType<typeof createCockpitContainer>;\n handover: { settings: RemoteAccessSettings; sessions: readonly MigratingSession[] };\n port: number;\n host: string;\n adopt: (shutDown: () => Promise<void>) => void;\n}): Promise<void> {\n await input.serving.close();\n const started = await input.container.start({\n workspaces: input.handover.settings.sandbox.workspaces.map((workspace) => ({ path: workspace })),\n port: input.port,\n onProgress: notice,\n });\n if (!started.ok) {\n notice(`the cockpit could not move into a container: ${started.error}`);\n notice('serving from the host again');\n await input.restart();\n await relaunchSessions({\n port: input.port,\n host: input.host,\n sessions: input.handover.sessions,\n onNotice: notice,\n });\n return;\n }\n input.adopt(async () => {\n await started.container.stop();\n });\n // The container answers its own health probe before `start` resolves, so its\n // hub is already there to take the settings and the sessions.\n await handOffRemoteAccess({ port: input.port, settings: input.handover.settings, onNotice: notice });\n await relaunchSessions({ port: input.port, sessions: input.handover.sessions, onNotice: notice });\n // Nothing else holds this process open now that its server is closed, and a\n // supervisor that exits leaves a container nobody will ever stop.\n notice('supervising the cockpit container; Ctrl-C stops it');\n await started.container.supervise();\n}\n\nmain().catch((error: unknown) => {\n notice(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n"],"mappings":"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"}
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { BridgeState, BridgeStatusFrame, SessionFrame } from "./src/types/session.cjs";
2
2
  import { SessionAttachment, WebServer, WebServerOptions } from "./src/types/bridge.cjs";
3
- import { serveWeb } from "./src/adapters/httpServer.cjs";
4
- import { ServeOptions, parseServeOptions } from "./src/services/serveOptions.cjs";
5
3
  import { HUB_PROTOCOL_VERSION, HUB_ROLE, SESSIONS_API_ROUTE, SessionPhase, SessionSummary } from "./src/types/hub.cjs";
6
4
  import { SESSION_RECORD_VERSION, SessionRecord } from "./src/types/registry.cjs";
5
+ import { serveWeb } from "./src/adapters/httpServer.cjs";
6
+ import { ServeOptions, parseServeOptions } from "./src/services/serveOptions.cjs";
7
7
  export { type BridgeState, type BridgeStatusFrame, HUB_PROTOCOL_VERSION, HUB_ROLE, SESSIONS_API_ROUTE, SESSION_RECORD_VERSION, type ServeOptions, type SessionAttachment, type SessionFrame, type SessionPhase, type SessionRecord, type SessionSummary, type WebServer, type WebServerOptions, parseServeOptions, serveWeb };
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { BridgeState, BridgeStatusFrame, SessionFrame } from "./src/types/session.mjs";
2
2
  import { SessionAttachment, WebServer, WebServerOptions } from "./src/types/bridge.mjs";
3
- import { serveWeb } from "./src/adapters/httpServer.mjs";
4
- import { ServeOptions, parseServeOptions } from "./src/services/serveOptions.mjs";
5
3
  import { HUB_PROTOCOL_VERSION, HUB_ROLE, SESSIONS_API_ROUTE, SessionPhase, SessionSummary } from "./src/types/hub.mjs";
6
4
  import { SESSION_RECORD_VERSION, SessionRecord } from "./src/types/registry.mjs";
5
+ import { serveWeb } from "./src/adapters/httpServer.mjs";
6
+ import { ServeOptions, parseServeOptions } from "./src/services/serveOptions.mjs";
7
7
  export { type BridgeState, type BridgeStatusFrame, HUB_PROTOCOL_VERSION, HUB_ROLE, SESSIONS_API_ROUTE, SESSION_RECORD_VERSION, type ServeOptions, type SessionAttachment, type SessionFrame, type SessionPhase, type SessionRecord, type SessionSummary, type WebServer, type WebServerOptions, parseServeOptions, serveWeb };
Binary file
Binary file
@@ -0,0 +1,104 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6
+ <meta name="theme-color" content="#14161a" />
7
+ <meta name="apple-mobile-web-app-capable" content="yes" />
8
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
9
+ <meta name="robots" content="noindex, nofollow" />
10
+ <link rel="manifest" href="/pwa/manifest.webmanifest" />
11
+ <link rel="apple-touch-icon" href="/pwa/icon-192.png" />
12
+ <title>Pair with DoomPi</title>
13
+ <style>
14
+ :root {
15
+ color-scheme: dark;
16
+ }
17
+ * {
18
+ box-sizing: border-box;
19
+ }
20
+ body {
21
+ margin: 0;
22
+ min-height: 100vh;
23
+ min-height: 100dvh;
24
+ display: grid;
25
+ place-items: center;
26
+ padding: max(1.25rem, env(safe-area-inset-top)) max(1.25rem, env(safe-area-inset-right))
27
+ max(1.25rem, env(safe-area-inset-bottom)) max(1.25rem, env(safe-area-inset-left));
28
+ background: #14161a;
29
+ color: #e6e8ec;
30
+ font:
31
+ 16px/1.5 ui-sans-serif,
32
+ system-ui,
33
+ -apple-system,
34
+ sans-serif;
35
+ }
36
+ main {
37
+ width: min(32rem, 100%);
38
+ text-align: center;
39
+ }
40
+ h1 {
41
+ margin: 0 0 0.5rem;
42
+ font-size: 1.35rem;
43
+ }
44
+ p {
45
+ margin: 0 0 1rem;
46
+ color: #a2a8b4;
47
+ }
48
+ #pwa-status[data-tone='good'] {
49
+ color: #6bd68a;
50
+ }
51
+ #pwa-status[data-tone='bad'] {
52
+ color: #ff7b7b;
53
+ }
54
+ video {
55
+ width: 100%;
56
+ max-height: 24rem;
57
+ margin: 0.75rem 0;
58
+ border: 1px solid #2c313a;
59
+ border-radius: 0.75rem;
60
+ background: #090a0c;
61
+ object-fit: cover;
62
+ }
63
+ .actions {
64
+ display: flex;
65
+ gap: 0.75rem;
66
+ margin-top: 1.25rem;
67
+ }
68
+ button {
69
+ flex: 1;
70
+ min-height: 3rem;
71
+ border: 1px solid #2f6feb;
72
+ border-radius: 0.6rem;
73
+ background: #2f6feb;
74
+ color: #fff;
75
+ font: inherit;
76
+ font-weight: 650;
77
+ }
78
+ button.secondary {
79
+ border-color: #2c313a;
80
+ background: #1b1e24;
81
+ }
82
+ button:disabled {
83
+ opacity: 0.55;
84
+ }
85
+ [hidden] {
86
+ display: none !important;
87
+ }
88
+ </style>
89
+ <script type="module" crossorigin src="/pwa/pwa.js"></script>
90
+ </head>
91
+ <body data-marker="doompi-pwa-shell">
92
+ <main>
93
+ <h1>Pair this device</h1>
94
+ <p>This cockpit can run shell commands on the host. Pair only with your own DoomPi.</p>
95
+ <p id="pwa-status" role="status">Checking browser support.</p>
96
+ <video id="camera" playsinline muted hidden></video>
97
+ <div class="actions">
98
+ <button id="scan" type="button">Scan pairing QR</button>
99
+ <button id="cancel" class="secondary" type="button" hidden>Cancel</button>
100
+ </div>
101
+ <noscript><p>Pairing needs JavaScript.</p></noscript>
102
+ </main>
103
+ </body>
104
+ </html>