windows-development-sandbox 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (448) hide show
  1. package/README.md +1 -0
  2. package/config.example.json +17 -1
  3. package/dist/cli/args.d.ts +13 -2
  4. package/dist/cli/args.js +95 -24
  5. package/dist/cli/commands/accounts.js +45 -17
  6. package/dist/cli/commands/context.d.ts +2 -0
  7. package/dist/cli/commands/context.js +9 -2
  8. package/dist/cli/commands/data/clean.js +32 -9
  9. package/dist/cli/commands/index.d.ts +1 -0
  10. package/dist/cli/commands/index.js +1 -0
  11. package/dist/cli/commands/launch/add-root.d.ts +3 -2
  12. package/dist/cli/commands/launch/add-root.js +11 -10
  13. package/dist/cli/commands/launch/launch.js +57 -4
  14. package/dist/cli/commands/launch/prepare.js +3 -1
  15. package/dist/cli/commands/maintenance/audit.js +1 -1
  16. package/dist/cli/commands/maintenance/clean.d.ts +1 -1
  17. package/dist/cli/commands/maintenance/clean.js +17 -3
  18. package/dist/cli/commands/maintenance/config.d.ts +1 -1
  19. package/dist/cli/commands/maintenance/config.js +112 -10
  20. package/dist/cli/commands/maintenance/doctor.d.ts +1 -1
  21. package/dist/cli/commands/maintenance/doctor.js +2 -4
  22. package/dist/cli/commands/maintenance/migrate.d.ts +2 -1
  23. package/dist/cli/commands/maintenance/migrate.js +19 -4
  24. package/dist/cli/commands/maintenance/sync.js +2 -0
  25. package/dist/cli/commands/maintenance/update.d.ts +2 -0
  26. package/dist/cli/commands/maintenance/update.js +27 -4
  27. package/dist/cli/commands/projects/here.js +12 -7
  28. package/dist/cli/commands/projects/projects.js +26 -17
  29. package/dist/cli/commands/registry.js +7 -0
  30. package/dist/cli/commands/repair/checks/agents.d.ts +2 -0
  31. package/dist/cli/commands/repair/checks/agents.js +70 -0
  32. package/dist/cli/commands/repair/checks/common.d.ts +8 -0
  33. package/dist/cli/commands/repair/checks/common.js +22 -0
  34. package/dist/cli/commands/repair/checks/data.d.ts +2 -0
  35. package/dist/cli/commands/repair/checks/data.js +241 -0
  36. package/dist/cli/commands/repair/checks/index.d.ts +2 -0
  37. package/dist/cli/commands/repair/checks/index.js +20 -0
  38. package/dist/cli/commands/repair/checks/install.d.ts +5 -0
  39. package/dist/cli/commands/repair/checks/install.js +318 -0
  40. package/dist/cli/commands/repair/checks/machine.d.ts +2 -0
  41. package/dist/cli/commands/repair/checks/machine.js +232 -0
  42. package/dist/cli/commands/repair/checks/network.d.ts +6 -0
  43. package/dist/cli/commands/repair/checks/network.js +60 -0
  44. package/dist/cli/commands/repair/checks/pc.d.ts +4 -0
  45. package/dist/cli/commands/repair/checks/pc.js +55 -0
  46. package/dist/cli/commands/repair/checks/sandbox.d.ts +2 -0
  47. package/dist/cli/commands/repair/checks/sandbox.js +267 -0
  48. package/dist/cli/commands/repair/checks/settings.d.ts +2 -0
  49. package/dist/cli/commands/repair/checks/settings.js +169 -0
  50. package/dist/cli/commands/repair/checks/vpn.d.ts +2 -0
  51. package/dist/cli/commands/repair/checks/vpn.js +53 -0
  52. package/dist/cli/commands/repair/engine.d.ts +32 -0
  53. package/dist/cli/commands/repair/engine.js +127 -0
  54. package/dist/cli/commands/repair/env.d.ts +7 -0
  55. package/dist/cli/commands/repair/env.js +49 -0
  56. package/dist/cli/commands/repair/index.d.ts +1 -9
  57. package/dist/cli/commands/repair/index.js +2 -54
  58. package/dist/cli/commands/repair/log.d.ts +17 -0
  59. package/dist/cli/commands/repair/log.js +82 -0
  60. package/dist/cli/commands/repair/printer.d.ts +32 -0
  61. package/dist/cli/commands/repair/printer.js +132 -0
  62. package/dist/cli/commands/repair/run.d.ts +33 -0
  63. package/dist/cli/commands/repair/run.js +133 -0
  64. package/dist/cli/commands/repair/types.d.ts +97 -9
  65. package/dist/cli/commands/repair/types.js +11 -7
  66. package/dist/cli/commands/resume.js +2 -2
  67. package/dist/cli/commands/sessions/list.js +5 -2
  68. package/dist/cli/commands/sessions/status.d.ts +2 -0
  69. package/dist/cli/commands/sessions/status.js +27 -6
  70. package/dist/cli/commands/setup/index.d.ts +5 -0
  71. package/dist/cli/commands/setup/index.js +25 -1
  72. package/dist/cli/commands/storage/index.d.ts +2 -0
  73. package/dist/cli/commands/storage/index.js +2 -0
  74. package/dist/cli/commands/storage/offer.d.ts +3 -0
  75. package/dist/cli/commands/storage/offer.js +85 -0
  76. package/dist/cli/commands/storage/storage.d.ts +8 -0
  77. package/dist/cli/commands/storage/storage.js +171 -0
  78. package/dist/cli/commands/user-config.js +1 -1
  79. package/dist/cli/commands/vpn.d.ts +4 -0
  80. package/dist/cli/commands/vpn.js +75 -0
  81. package/dist/cli/commands/web.d.ts +4 -0
  82. package/dist/cli/commands/web.js +95 -0
  83. package/dist/cli/history.js +1 -1
  84. package/dist/cli/index.js +46 -3
  85. package/dist/cli/manifest.d.ts +303 -29
  86. package/dist/cli/manifest.js +173 -43
  87. package/dist/cli/menu/add-agent.d.ts +47 -0
  88. package/dist/cli/menu/add-agent.js +168 -0
  89. package/dist/cli/menu/apps.d.ts +31 -2
  90. package/dist/cli/menu/apps.js +44 -5
  91. package/dist/cli/menu/command-fields.d.ts +18 -0
  92. package/dist/cli/menu/command-fields.js +61 -0
  93. package/dist/cli/menu/command-form.d.ts +22 -0
  94. package/dist/cli/menu/command-form.js +169 -46
  95. package/dist/cli/menu/data-folder.d.ts +18 -0
  96. package/dist/cli/menu/data-folder.js +99 -0
  97. package/dist/cli/menu/menu.d.ts +1 -1
  98. package/dist/cli/menu/menu.js +11 -8
  99. package/dist/cli/menu/pages/accounts-page.js +55 -24
  100. package/dist/cli/menu/pages/agents-page.d.ts +66 -0
  101. package/dist/cli/menu/pages/agents-page.js +338 -0
  102. package/dist/cli/menu/pages/apps-page.js +1 -2
  103. package/dist/cli/menu/pages/list-page.d.ts +16 -3
  104. package/dist/cli/menu/pages/list-page.js +40 -8
  105. package/dist/cli/menu/pages/maintenance-page.d.ts +9 -0
  106. package/dist/cli/menu/pages/maintenance-page.js +161 -72
  107. package/dist/cli/menu/pages/sandbox-page.d.ts +3 -0
  108. package/dist/cli/menu/pages/sandbox-page.js +132 -43
  109. package/dist/cli/menu/pages/sessions-page.d.ts +4 -0
  110. package/dist/cli/menu/pages/sessions-page.js +41 -25
  111. package/dist/cli/menu/pages/settings-page.d.ts +8 -4
  112. package/dist/cli/menu/pages/settings-page.js +36 -13
  113. package/dist/cli/menu/settings/categories.d.ts +2 -0
  114. package/dist/cli/menu/settings/categories.js +19 -44
  115. package/dist/cli/menu/settings/collections/collection-editor.d.ts +7 -1
  116. package/dist/cli/menu/settings/collections/collection-editor.js +12 -8
  117. package/dist/cli/menu/settings/collections/map-editor.js +5 -1
  118. package/dist/cli/menu/settings/collections/object-list-editor.js +2 -2
  119. package/dist/cli/menu/settings/collections/object-map-editor.js +2 -2
  120. package/dist/cli/menu/settings/collections/string-list-editor.d.ts +11 -1
  121. package/dist/cli/menu/settings/collections/string-list-editor.js +23 -0
  122. package/dist/cli/menu/settings/index.d.ts +1 -1
  123. package/dist/cli/menu/settings/index.js +1 -1
  124. package/dist/cli/menu/settings/object-editor.d.ts +5 -1
  125. package/dist/cli/menu/settings/object-editor.js +15 -4
  126. package/dist/cli/menu/settings/path-kind.d.ts +20 -0
  127. package/dist/cli/menu/settings/path-kind.js +44 -0
  128. package/dist/cli/menu/settings/schema-node.d.ts +9 -0
  129. package/dist/cli/menu/settings/schema-node.js +26 -0
  130. package/dist/cli/menu/settings/value-editor.d.ts +5 -0
  131. package/dist/cli/menu/settings/value-editor.js +51 -18
  132. package/dist/cli/menu/several.js +7 -5
  133. package/dist/cli/menu/shell.d.ts +37 -10
  134. package/dist/cli/menu/shell.js +90 -10
  135. package/dist/cli/menu/vpn-login.d.ts +16 -0
  136. package/dist/cli/menu/vpn-login.js +66 -0
  137. package/dist/cli/output/print.d.ts +2 -0
  138. package/dist/cli/output/print.js +8 -1
  139. package/dist/cli/services/accounts-service.d.ts +2 -0
  140. package/dist/cli/services/accounts-service.js +2 -2
  141. package/dist/cli/services/health-service.d.ts +0 -6
  142. package/dist/cli/services/health-service.js +5 -42
  143. package/dist/cli/services/index.d.ts +3 -1
  144. package/dist/cli/services/index.js +3 -1
  145. package/dist/cli/services/migrate-service.js +1 -1
  146. package/dist/cli/services/update-service.d.ts +2 -0
  147. package/dist/cli/services/update-service.js +13 -6
  148. package/dist/cli/services/vpn-service.d.ts +28 -0
  149. package/dist/cli/services/vpn-service.js +46 -0
  150. package/dist/cli/services/web-service.d.ts +22 -0
  151. package/dist/cli/services/web-service.js +111 -0
  152. package/dist/cli/tui/app.d.ts +6 -3
  153. package/dist/cli/tui/app.js +12 -5
  154. package/dist/cli/tui/dialogs/path-listing.d.ts +43 -0
  155. package/dist/cli/tui/dialogs/path-listing.js +127 -0
  156. package/dist/cli/tui/dialogs/path-picker.d.ts +67 -0
  157. package/dist/cli/tui/dialogs/path-picker.js +423 -0
  158. package/dist/cli/tui/dialogs/text-screen.d.ts +6 -0
  159. package/dist/cli/tui/dialogs/text-screen.js +10 -3
  160. package/dist/cli/tui/frame.d.ts +7 -10
  161. package/dist/cli/tui/frame.js +19 -10
  162. package/dist/cli/tui/index.d.ts +4 -3
  163. package/dist/cli/tui/index.js +2 -1
  164. package/dist/cli/tui/list/list-screen.d.ts +14 -2
  165. package/dist/cli/tui/list/list-screen.js +68 -32
  166. package/dist/cli/tui/list/list-types.d.ts +8 -4
  167. package/dist/cli/tui/nav.d.ts +29 -0
  168. package/dist/cli/tui/nav.js +67 -0
  169. package/dist/cli/validate.d.ts +1 -1
  170. package/dist/cli/validate.js +3 -3
  171. package/dist/cli/version.d.ts +2 -0
  172. package/dist/cli/version.js +11 -0
  173. package/dist/core/config/defaults.js +5 -1
  174. package/dist/core/config/loader.js +12 -2
  175. package/dist/core/local-config/overrides.js +1 -0
  176. package/dist/core/prompt.d.ts +19 -0
  177. package/dist/core/prompt.js +43 -3
  178. package/dist/core/schema/common.d.ts +6 -0
  179. package/dist/core/schema/common.js +8 -0
  180. package/dist/core/schema/config.d.ts +23 -0
  181. package/dist/core/schema/config.js +10 -1
  182. package/dist/core/schema/harness.d.ts +1 -0
  183. package/dist/core/schema/harness.js +8 -2
  184. package/dist/core/schema/index.d.ts +3 -1
  185. package/dist/core/schema/index.js +3 -1
  186. package/dist/core/schema/profile.d.ts +1 -0
  187. package/dist/core/schema/profile.js +3 -0
  188. package/dist/core/schema/redact.d.ts +13 -0
  189. package/dist/core/schema/redact.js +75 -0
  190. package/dist/core/schema/sandbox.js +1 -0
  191. package/dist/core/schema/vpn.d.ts +17 -0
  192. package/dist/core/schema/vpn.js +22 -0
  193. package/dist/core/schema/web.d.ts +6 -0
  194. package/dist/core/schema/web.js +14 -0
  195. package/dist/core/types.d.ts +26 -0
  196. package/dist/guest/agents/agent-files.d.ts +49 -0
  197. package/dist/guest/agents/agent-files.js +63 -0
  198. package/dist/guest/agents/agent-installer.js +2 -9
  199. package/dist/guest/agents/local-state.d.ts +12 -0
  200. package/dist/guest/agents/local-state.js +198 -0
  201. package/dist/guest/agents/sbx-agent.d.ts +2 -0
  202. package/dist/guest/agents/sbx-agent.js +35 -0
  203. package/dist/guest/boot/boot.d.ts +4 -0
  204. package/dist/guest/boot/boot.js +50 -11
  205. package/dist/guest/daemon/daemon.js +4 -1
  206. package/dist/guest/daemon/exec.js +2 -1
  207. package/dist/guest/daemon/idle-shutdown.js +2 -0
  208. package/dist/guest/daemon/sessions.js +4 -1
  209. package/dist/guest/entries/agent.d.ts +1 -0
  210. package/dist/guest/entries/agent.js +10 -0
  211. package/dist/guest/entries/daemon.js +2 -0
  212. package/dist/guest/runtime/csc.d.ts +1 -0
  213. package/dist/guest/runtime/csc.js +1 -0
  214. package/dist/guest/runtime/secrets.d.ts +12 -0
  215. package/dist/guest/runtime/secrets.js +63 -0
  216. package/dist/guest/setup/claude-profile.d.ts +2 -1
  217. package/dist/guest/setup/claude-profile.js +12 -1
  218. package/dist/guest/setup/environment.d.ts +6 -0
  219. package/dist/guest/setup/environment.js +56 -6
  220. package/dist/guest/setup/path-plan.d.ts +32 -0
  221. package/dist/guest/setup/path-plan.js +67 -0
  222. package/dist/guest/setup/shell-path.d.ts +1 -0
  223. package/dist/guest/setup/shell-path.js +22 -0
  224. package/dist/guest/toolchains/languages/python.js +1 -1
  225. package/dist/guest/toolchains/runtimes.js +2 -2
  226. package/dist/guest/vpn/index.d.ts +1 -0
  227. package/dist/guest/vpn/index.js +1 -0
  228. package/dist/guest/vpn/openvpn.d.ts +9 -0
  229. package/dist/guest/vpn/openvpn.js +47 -0
  230. package/dist/guest/vpn/tunnel.d.ts +17 -0
  231. package/dist/guest/vpn/tunnel.js +78 -0
  232. package/dist/guest/vpn/vpn.d.ts +6 -0
  233. package/dist/guest/vpn/vpn.js +126 -0
  234. package/dist/host/channel/attach.d.ts +3 -1
  235. package/dist/host/channel/attach.js +1 -1
  236. package/dist/host/channel/channel.d.ts +7 -0
  237. package/dist/host/channel/channel.js +26 -2
  238. package/dist/host/health/acl-read.d.ts +17 -0
  239. package/dist/host/health/acl-read.js +45 -0
  240. package/dist/host/health/config-backup.d.ts +11 -0
  241. package/dist/host/health/config-backup.js +73 -0
  242. package/dist/host/health/index.d.ts +6 -0
  243. package/dist/host/health/index.js +7 -0
  244. package/dist/host/health/install-state.d.ts +33 -0
  245. package/dist/host/health/install-state.js +110 -0
  246. package/dist/host/health/path-copies.d.ts +32 -0
  247. package/dist/host/health/path-copies.js +82 -0
  248. package/dist/host/health/stale.d.ts +45 -0
  249. package/dist/host/health/stale.js +120 -0
  250. package/dist/host/health/system.d.ts +37 -0
  251. package/dist/host/health/system.js +87 -0
  252. package/dist/host/install/copies.d.ts +13 -0
  253. package/dist/host/install/copies.js +63 -0
  254. package/dist/host/install/index.d.ts +1 -0
  255. package/dist/host/install/index.js +1 -0
  256. package/dist/host/launch/generate.js +25 -1
  257. package/dist/host/launch/session-file.js +4 -1
  258. package/dist/host/launch/stage.js +1 -1
  259. package/dist/host/migrate/backup.js +3 -0
  260. package/dist/host/migrate/migrations/config-file.d.ts +2 -0
  261. package/dist/host/migrate/migrations/config-file.js +7 -0
  262. package/dist/host/migrate/migrations/follow-machine-storage.js +9 -1
  263. package/dist/host/migrate/migrations/pin-storage-root.d.ts +4 -0
  264. package/dist/host/migrate/migrations/pin-storage-root.js +46 -0
  265. package/dist/host/migrate/migrations/unpin-config-defaults.js +4 -1
  266. package/dist/host/migrate/registry.js +2 -1
  267. package/dist/host/profile/accounts.d.ts +25 -1
  268. package/dist/host/profile/accounts.js +96 -13
  269. package/dist/host/profile/agent-accounts.d.ts +5 -1
  270. package/dist/host/profile/agent-accounts.js +35 -5
  271. package/dist/host/profile/index.d.ts +2 -1
  272. package/dist/host/profile/index.js +1 -0
  273. package/dist/host/profile/plugin-share.d.ts +26 -0
  274. package/dist/host/profile/plugin-share.js +146 -0
  275. package/dist/host/projects/join.d.ts +7 -1
  276. package/dist/host/projects/join.js +21 -4
  277. package/dist/host/projects/registry.d.ts +16 -10
  278. package/dist/host/projects/registry.js +74 -6
  279. package/dist/host/sandbox/platform.d.ts +2 -0
  280. package/dist/host/sandbox/platform.js +21 -2
  281. package/dist/host/secrets/index.d.ts +2 -0
  282. package/dist/host/secrets/index.js +2 -0
  283. package/dist/host/secrets/launch-secrets.d.ts +17 -0
  284. package/dist/host/secrets/launch-secrets.js +93 -0
  285. package/dist/host/secrets/secret-store.d.ts +25 -0
  286. package/dist/host/secrets/secret-store.js +89 -0
  287. package/dist/host/sharing/acl.d.ts +16 -2
  288. package/dist/host/sharing/acl.js +35 -10
  289. package/dist/host/sharing/current.js +11 -2
  290. package/dist/host/sharing/doctor.d.ts +2 -0
  291. package/dist/host/sharing/doctor.js +44 -0
  292. package/dist/host/sharing/group-access.d.ts +6 -0
  293. package/dist/host/sharing/group-access.js +24 -0
  294. package/dist/host/sharing/index.d.ts +2 -1
  295. package/dist/host/sharing/index.js +2 -1
  296. package/dist/host/sharing/lifecycle.d.ts +8 -2
  297. package/dist/host/sharing/lifecycle.js +16 -7
  298. package/dist/host/storage/cleanup.d.ts +2 -0
  299. package/dist/host/storage/cleanup.js +2 -1
  300. package/dist/host/storage/history.d.ts +21 -0
  301. package/dist/host/storage/history.js +49 -0
  302. package/dist/host/storage/index.d.ts +4 -0
  303. package/dist/host/storage/index.js +4 -0
  304. package/dist/host/storage/move.d.ts +95 -0
  305. package/dist/host/storage/move.js +384 -0
  306. package/dist/host/storage/offer.d.ts +11 -0
  307. package/dist/host/storage/offer.js +36 -0
  308. package/dist/host/storage/root.d.ts +18 -0
  309. package/dist/host/storage/root.js +77 -0
  310. package/dist/host/tools/windows-terminal.d.ts +2 -0
  311. package/dist/host/tools/windows-terminal.js +23 -5
  312. package/dist/host/update/index.d.ts +1 -1
  313. package/dist/host/update/index.js +1 -1
  314. package/dist/host/update/notice.d.ts +2 -0
  315. package/dist/host/update/notice.js +13 -1
  316. package/dist/host/update/update-state.d.ts +6 -0
  317. package/dist/host/update/update-state.js +2 -0
  318. package/dist/host/update/updater.d.ts +19 -0
  319. package/dist/host/update/updater.js +88 -12
  320. package/dist/host/vpn/index.d.ts +4 -0
  321. package/dist/host/vpn/index.js +4 -0
  322. package/dist/host/vpn/login.d.ts +24 -0
  323. package/dist/host/vpn/login.js +79 -0
  324. package/dist/host/vpn/nordvpn.d.ts +21 -0
  325. package/dist/host/vpn/nordvpn.js +77 -0
  326. package/dist/host/vpn/prepare.d.ts +17 -0
  327. package/dist/host/vpn/prepare.js +77 -0
  328. package/dist/host/vpn/status.d.ts +12 -0
  329. package/dist/host/vpn/status.js +53 -0
  330. package/dist/host/web/auth.d.ts +38 -0
  331. package/dist/host/web/auth.js +96 -0
  332. package/dist/host/web/backend.d.ts +14 -0
  333. package/dist/host/web/backend.js +1 -0
  334. package/dist/host/web/bridge.d.ts +27 -0
  335. package/dist/host/web/bridge.js +144 -0
  336. package/dist/host/web/browser.d.ts +1 -0
  337. package/dist/host/web/browser.js +10 -0
  338. package/dist/host/web/control.d.ts +17 -0
  339. package/dist/host/web/control.js +67 -0
  340. package/dist/host/web/index.d.ts +9 -0
  341. package/dist/host/web/index.js +8 -0
  342. package/dist/host/web/runner.d.ts +14 -0
  343. package/dist/host/web/runner.js +45 -0
  344. package/dist/host/web/server.d.ts +40 -0
  345. package/dist/host/web/server.js +218 -0
  346. package/dist/host/web/state.d.ts +20 -0
  347. package/dist/host/web/state.js +41 -0
  348. package/dist/host/web/static-files.d.ts +13 -0
  349. package/dist/host/web/static-files.js +57 -0
  350. package/dist/runtime/agent.js +27614 -0
  351. package/dist/runtime/daemon.js +1012 -618
  352. package/dist/runtime/db.js +236 -106
  353. package/dist/runtime/main.js +1093 -250
  354. package/dist/runtime/netproxy.js +102 -3
  355. package/dist/runtime/view.js +102 -3
  356. package/dist/selftest/accounts.test.js +40 -0
  357. package/dist/selftest/agent-paths.test.d.ts +2 -0
  358. package/dist/selftest/agent-paths.test.js +115 -0
  359. package/dist/selftest/channel.test.js +5 -0
  360. package/dist/selftest/cli.test.js +34 -4
  361. package/dist/selftest/credentials.test.d.ts +2 -0
  362. package/dist/selftest/credentials.test.js +198 -0
  363. package/dist/selftest/generate.test.js +1 -1
  364. package/dist/selftest/health.test.d.ts +2 -0
  365. package/dist/selftest/health.test.js +470 -0
  366. package/dist/selftest/host-sim.test.js +2 -0
  367. package/dist/selftest/index.js +19 -0
  368. package/dist/selftest/layout.test.js +2 -0
  369. package/dist/selftest/menu-screens.d.ts +7 -0
  370. package/dist/selftest/menu-screens.js +65 -16
  371. package/dist/selftest/menu.test.js +574 -21
  372. package/dist/selftest/migrate.test.js +1 -1
  373. package/dist/selftest/personalization.test.js +3 -1
  374. package/dist/selftest/projects.test.js +39 -1
  375. package/dist/selftest/shared-use.test.js +64 -2
  376. package/dist/selftest/sharing.test.js +50 -1
  377. package/dist/selftest/storage.test.d.ts +2 -0
  378. package/dist/selftest/storage.test.js +363 -0
  379. package/dist/selftest/support/fixtures.d.ts +14 -0
  380. package/dist/selftest/support/releases.js +1 -1
  381. package/dist/selftest/toolchains.test.js +21 -0
  382. package/dist/selftest/update-service.test.js +87 -3
  383. package/dist/selftest/vpn.test.d.ts +2 -0
  384. package/dist/selftest/vpn.test.js +279 -0
  385. package/dist/selftest/web.test.d.ts +2 -0
  386. package/dist/selftest/web.test.js +304 -0
  387. package/dist/shared/apps/catalogue.js +31 -0
  388. package/dist/shared/channel/index.d.ts +1 -0
  389. package/dist/shared/channel/index.js +1 -0
  390. package/dist/shared/channel/run-dir.d.ts +6 -1
  391. package/dist/shared/channel/run-dir.js +12 -1
  392. package/dist/shared/channel/schema.d.ts +19 -0
  393. package/dist/shared/channel/schema.js +11 -0
  394. package/dist/shared/channel/secrets.d.ts +28 -0
  395. package/dist/shared/channel/secrets.js +44 -0
  396. package/dist/shared/vpn/download.d.ts +13 -0
  397. package/dist/shared/vpn/download.js +14 -0
  398. package/dist/shared/vpn/kill-switch.d.ts +18 -0
  399. package/dist/shared/vpn/kill-switch.js +81 -0
  400. package/dist/shared/vpn/ovpn.d.ts +23 -0
  401. package/dist/shared/vpn/ovpn.js +244 -0
  402. package/dist/shared/web/index.d.ts +1 -0
  403. package/dist/shared/web/index.js +1 -0
  404. package/dist/shared/web/protocol.d.ts +61 -0
  405. package/dist/shared/web/protocol.js +35 -0
  406. package/dist/sync/sync-cycle.js +5 -1
  407. package/dist/sync/sync-state.d.ts +1 -0
  408. package/dist/sync/sync-state.js +1 -0
  409. package/dist/sync/watcher.js +10 -0
  410. package/dist/web/index.html +13 -0
  411. package/dist/web/main-CXESL347.js +45 -0
  412. package/dist/web/main-VLR7DVQ4.css +1 -0
  413. package/native/csharp/FolderSecurity.cs +29 -0
  414. package/native/scripts/protect-data.ps1 +29 -0
  415. package/native/scripts/read-folder-sddl.ps1 +10 -0
  416. package/native/scripts/set-folder-access.ps1 +49 -7
  417. package/node_modules/ws/LICENSE +20 -0
  418. package/node_modules/ws/README.md +548 -0
  419. package/node_modules/ws/browser.js +8 -0
  420. package/node_modules/ws/index.js +22 -0
  421. package/node_modules/ws/lib/buffer-util.js +131 -0
  422. package/node_modules/ws/lib/constants.js +19 -0
  423. package/node_modules/ws/lib/event-target.js +292 -0
  424. package/node_modules/ws/lib/extension.js +203 -0
  425. package/node_modules/ws/lib/limiter.js +55 -0
  426. package/node_modules/ws/lib/permessage-deflate.js +530 -0
  427. package/node_modules/ws/lib/receiver.js +743 -0
  428. package/node_modules/ws/lib/sender.js +607 -0
  429. package/node_modules/ws/lib/stream.js +161 -0
  430. package/node_modules/ws/lib/subprotocol.js +62 -0
  431. package/node_modules/ws/lib/validation.js +152 -0
  432. package/node_modules/ws/lib/websocket-server.js +562 -0
  433. package/node_modules/ws/lib/websocket.js +1421 -0
  434. package/node_modules/ws/package.json +74 -0
  435. package/node_modules/ws/wrapper.mjs +21 -0
  436. package/package.json +15 -5
  437. package/sandbox/csharp/EnvBroadcast.cs +33 -0
  438. package/sandbox/csharp/PathSync.cs +90 -0
  439. package/sandbox/scripts/install-shell-path.ps1 +73 -0
  440. package/sandbox/scripts/set-machine-environment.ps1 +37 -4
  441. package/schemas/config.schema.json +79 -4
  442. package/schemas/sandbox-config.schema.json +14 -1
  443. package/dist/cli/commands/repair/checks.d.ts +0 -2
  444. package/dist/cli/commands/repair/checks.js +0 -246
  445. package/dist/cli/menu/pages/start-page.d.ts +0 -43
  446. package/dist/cli/menu/pages/start-page.js +0 -192
  447. package/dist/cli/tui/tab-bar.d.ts +0 -9
  448. package/dist/cli/tui/tab-bar.js +0 -33
@@ -0,0 +1,77 @@
1
+ // Picks a NordVPN server at launch from NordVPN's public server API and fetches its OpenVPN profile. Done on this PC,
2
+ // not in the sandbox: the sandbox's firewall then needs no hole for the API or the download host, and what comes back
3
+ // is checked here (bounded size, strict shapes, shared/vpn/ovpn.ts) before the sandbox gets it. Nothing here needs
4
+ // the login: the profiles are public.
5
+ import { z } from 'zod';
6
+ import { fetchRetry } from '../../shared/download/index.js';
7
+ import { fail } from '../../core/util/index.js';
8
+ import { MAX_OVPN_BYTES } from '../../shared/vpn/ovpn.js';
9
+ const API = 'https://api.nordvpn.com/v1';
10
+ const CONFIGS = 'https://downloads.nordcdn.com/configs/files';
11
+ const MAX_JSON_BYTES = 2 * 1024 * 1024;
12
+ export async function fetchBounded(url, max) {
13
+ const response = await fetchRetry(url, {}, 2, 30000);
14
+ const length = Number(response.headers.get('content-length')) || 0;
15
+ if (length > max)
16
+ throw new Error('The answer from ' + new URL(url).host + ' is too big.');
17
+ const reader = response.body?.getReader();
18
+ if (!reader)
19
+ throw new Error('Empty answer from ' + new URL(url).host + '.');
20
+ const chunks = [];
21
+ let size = 0;
22
+ for (;;) {
23
+ const { done, value } = await reader.read();
24
+ if (done)
25
+ break;
26
+ size += value.length;
27
+ if (size > max) {
28
+ await reader.cancel();
29
+ throw new Error('The answer from ' + new URL(url).host + ' is too big.');
30
+ }
31
+ chunks.push(value);
32
+ }
33
+ return Buffer.concat(chunks).toString('utf8');
34
+ }
35
+ const CountriesSchema = z
36
+ .array(z.object({ id: z.number().int(), name: z.string().max(100), code: z.string().max(4) }))
37
+ .max(400);
38
+ const ServersSchema = z
39
+ .array(z.object({
40
+ hostname: z.string().regex(/^[a-z0-9-]{2,40}\.nordvpn\.com$/),
41
+ station: z.string().regex(/^\d{1,3}(\.\d{1,3}){3}$/),
42
+ }))
43
+ .max(50);
44
+ /** Names people type that NordVPN spells differently. */
45
+ const COUNTRY_ALIASES = {
46
+ uk: 'gb',
47
+ 'united kingdom': 'gb',
48
+ usa: 'us',
49
+ america: 'us',
50
+ };
51
+ /** The country's id at NordVPN for what was typed (code or name, any case); fails listing nothing it cannot back up. */
52
+ export function findCountry(countries, typed) {
53
+ const want = typed.trim().toLowerCase();
54
+ const alias = COUNTRY_ALIASES[want] ?? want;
55
+ const found = countries.find((c) => c.code.toLowerCase() === alias || c.name.toLowerCase() === alias);
56
+ if (!found)
57
+ fail('NordVPN has no country "' + typed + '".', 'Use a name like Germany or a code like de (vpn.country in your settings), or leave it empty.');
58
+ return found.id;
59
+ }
60
+ /** The recommended server for the country (or anywhere) and protocol. */
61
+ export async function recommendedServer(protocol, country, fetchText = fetchBounded) {
62
+ const filters = ['filters[servers_technologies][identifier]=openvpn_' + protocol, 'limit=1'];
63
+ if (country.trim()) {
64
+ const countries = CountriesSchema.safeParse(JSON.parse(await fetchText(API + '/servers/countries', MAX_JSON_BYTES)));
65
+ if (!countries.success)
66
+ fail("NordVPN's list of countries was not in the expected form.", 'Try again later.');
67
+ filters.push('filters[country_id]=' + findCountry(countries.data, country));
68
+ }
69
+ const answer = ServersSchema.safeParse(JSON.parse(await fetchText(API + '/servers/recommendations?' + filters.join('&'), MAX_JSON_BYTES)));
70
+ if (!answer.success || answer.data.length === 0)
71
+ fail('NordVPN did not recommend a server' + (country.trim() ? ' in ' + country.trim() : '') + '.', 'Try another country (vpn.country), or leave it empty.');
72
+ return { hostname: answer.data[0].hostname, ip: answer.data[0].station };
73
+ }
74
+ /** The server's OpenVPN profile (not yet checked: shared/vpn/ovpn.ts does that). */
75
+ export function nordProfile(server, protocol, fetchText = fetchBounded) {
76
+ return fetchText(CONFIGS + '/ovpn_' + protocol + '/servers/' + server.hostname + '.' + protocol + '.ovpn', MAX_OVPN_BYTES);
77
+ }
@@ -0,0 +1,17 @@
1
+ import type { Config, PreparedVpn } from '../../core/types.js';
2
+ import { type Fetcher } from './nordvpn.js';
3
+ import { VpnLogin } from './login.js';
4
+ export interface PrepareOptions {
5
+ readonly config: Config;
6
+ readonly storageRoot: string;
7
+ /** A sandbox is running: the login is not handed to anything that is not being started now. */
8
+ readonly sandboxRunning?: boolean;
9
+ readonly login?: VpnLogin;
10
+ readonly fetchText?: Fetcher;
11
+ /** Folder a relative vpn.configFile is relative to. */
12
+ readonly base?: string;
13
+ }
14
+ /** Null when the VPN is off. Throws a UserError that says what to do when it cannot be set up. */
15
+ export declare function prepareVpn(o: PrepareOptions): Promise<PreparedVpn | null>;
16
+ /** What the launch says about the VPN: the warnings the user must see before trusting it. */
17
+ export declare function vpnNotes(vpn: Config['vpn']): string[];
@@ -0,0 +1,77 @@
1
+ // What a launch needs for the VPN: the saved login, a server and its profile, the profile checked. Nothing is written
2
+ // here; generateSandbox puts the profile in the launch folder and the login in the one-shot secrets file.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { expandPath, fail } from '../../core/util/index.js';
6
+ import { assertSafeOvpn, MAX_OVPN_BYTES } from '../../shared/vpn/ovpn.js';
7
+ import { fetchBounded, nordProfile, recommendedServer } from './nordvpn.js';
8
+ import { VpnLogin } from './login.js';
9
+ /** Null when the VPN is off. Throws a UserError that says what to do when it cannot be set up. */
10
+ export async function prepareVpn(o) {
11
+ const { vpn } = o.config;
12
+ if (!vpn.enabled)
13
+ return null;
14
+ if (!o.config.sandbox.networking)
15
+ fail('The VPN is on, but sandbox.networking is off: there is no network to put it on.', 'Turn one of them off.');
16
+ if (o.sandboxRunning)
17
+ fail('A sandbox is running already, so the VPN login is not handed over.', 'Start the VPN sandbox after it stops.');
18
+ const login = o.login ?? new VpnLogin(o.storageRoot);
19
+ const credentials = login.read(vpn.provider) ??
20
+ fail('The VPN is on, but no ' + (vpn.provider === 'nordvpn' ? 'NordVPN' : 'VPN') + ' login is saved.', 'Save it with: sandbox vpn login (or turn the VPN off: sandbox config set vpn.enabled false)');
21
+ let profile;
22
+ let server;
23
+ if (vpn.provider === 'nordvpn') {
24
+ const fetchText = o.fetchText ?? fetchBounded;
25
+ try {
26
+ const picked = await recommendedServer(vpn.protocol, vpn.country, fetchText);
27
+ server = picked.hostname;
28
+ profile = await nordProfile(picked, vpn.protocol, fetchText);
29
+ }
30
+ catch (e) {
31
+ if (e instanceof Error && e.name === 'UserError')
32
+ throw e;
33
+ fail('Could not get a NordVPN server: ' + (e instanceof Error ? e.message : String(e)), 'Check this PC is online, or turn the VPN off for now: sandbox config set vpn.enabled false');
34
+ }
35
+ }
36
+ else {
37
+ if (!vpn.configFile.trim())
38
+ fail('vpn.provider is openvpn-file but vpn.configFile is empty.', 'Set it: sandbox config set vpn.configFile <path>');
39
+ const file = expandPath(vpn.configFile, o.base ?? process.cwd());
40
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile())
41
+ fail('The VPN profile ' + file + ' does not exist.', 'Fix vpn.configFile.');
42
+ if (fs.statSync(file).size > MAX_OVPN_BYTES)
43
+ fail('The VPN profile ' + file + ' is too big.', 'Use a plain .ovpn file.');
44
+ profile = fs.readFileSync(file, 'utf8');
45
+ server = path.basename(file);
46
+ }
47
+ try {
48
+ assertSafeOvpn(profile, server);
49
+ }
50
+ catch (e) {
51
+ fail(e instanceof Error ? e.message : String(e), 'Nothing was started. Use a profile without script or plugin lines.');
52
+ }
53
+ return {
54
+ session: {
55
+ provider: vpn.provider,
56
+ protocol: vpn.protocol,
57
+ killSwitch: vpn.killSwitch,
58
+ server,
59
+ profile: 'vpn\\server.ovpn',
60
+ },
61
+ profile,
62
+ credentials,
63
+ };
64
+ }
65
+ /** What the launch says about the VPN: the warnings the user must see before trusting it. */
66
+ export function vpnNotes(vpn) {
67
+ if (!vpn.enabled)
68
+ return [];
69
+ return [
70
+ 'VPN: ' +
71
+ (vpn.provider === 'nordvpn' ? 'NordVPN' + (vpn.country ? ' (' + vpn.country + ')' : '') : 'your profile') +
72
+ (vpn.killSwitch
73
+ ? ', kill switch on: if the tunnel fails the sandbox stays offline.'
74
+ : ', kill switch OFF: if the tunnel fails the sandbox uses your normal connection.'),
75
+ 'The agent in the sandbox can read the VPN login (a service login you can revoke) and can turn the tunnel off, because it is the administrator there.',
76
+ ];
77
+ }
@@ -0,0 +1,12 @@
1
+ import { type VpnStatus } from '../../shared/channel/index.js';
2
+ import { type Fetcher } from './nordvpn.js';
3
+ export interface VpnLine {
4
+ readonly level: 'ok' | 'warn' | 'bad';
5
+ readonly text: string;
6
+ }
7
+ /** This PC's own public address (one small request), or null when it cannot be told. */
8
+ export declare function hostPublicIp(fetchText?: Fetcher): Promise<string | null>;
9
+ /** One line for the status the sandbox reported (text from the sandbox is made safe first). */
10
+ export declare function vpnLine(status: VpnStatus | null, hostIp?: string | null): VpnLine;
11
+ /** The running sandbox's VPN line, or null when its VPN was never on (no status file). */
12
+ export declare function readVpnLine(dataPath: string, hostIp?: string | null): VpnLine | null;
@@ -0,0 +1,53 @@
1
+ // What the host says about a running sandbox's VPN, from the status the sandbox writes (run\vpn.json, validated).
2
+ import { RunDir } from '../../shared/channel/index.js';
3
+ import { safeText } from '../../core/util/index.js';
4
+ import { fetchBounded } from './nordvpn.js';
5
+ /** This PC's own public address (one small request), or null when it cannot be told. */
6
+ export async function hostPublicIp(fetchText = fetchBounded) {
7
+ try {
8
+ const ip = (await fetchText('https://api.ipify.org', 64)).trim();
9
+ return /^[0-9a-fA-F.:]{3,45}$/.test(ip) ? ip : null;
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ /** One line for the status the sandbox reported (text from the sandbox is made safe first). */
16
+ export function vpnLine(status, hostIp = null) {
17
+ if (!status)
18
+ return { level: 'warn', text: 'The sandbox has not reported on the VPN.' };
19
+ const message = safeText(status.message).slice(0, 300);
20
+ if (status.state === 'up') {
21
+ const same = hostIp !== null && status.publicIp === hostIp;
22
+ return same
23
+ ? {
24
+ level: 'bad',
25
+ text: "VPN reports up, but the sandbox shows this PC's own address (" +
26
+ hostIp +
27
+ '): the traffic is not going through it.',
28
+ }
29
+ : {
30
+ level: 'ok',
31
+ text: 'VPN up' +
32
+ (status.server ? ' (' + safeText(status.server) + ')' : '') +
33
+ (status.publicIp ? ', the sandbox appears as ' + safeText(status.publicIp) : '') +
34
+ (hostIp && status.publicIp ? ' (this PC: ' + hostIp + ')' : '') +
35
+ '.',
36
+ };
37
+ }
38
+ if (status.state === 'failed')
39
+ return {
40
+ level: 'bad',
41
+ text: 'VPN failed: ' +
42
+ message +
43
+ (status.killSwitch
44
+ ? ' The kill switch is on, so the sandbox has no internet.'
45
+ : ' The kill switch is off, so the sandbox uses your normal connection.'),
46
+ };
47
+ return { level: 'warn', text: 'VPN ' + status.state + (message ? ': ' + message : '') };
48
+ }
49
+ /** The running sandbox's VPN line, or null when its VPN was never on (no status file). */
50
+ export function readVpnLine(dataPath, hostIp = null) {
51
+ const status = new RunDir(dataPath).readVpnStatus();
52
+ return status && status.state !== 'off' ? vpnLine(status, hostIp) : null;
53
+ }
@@ -0,0 +1,38 @@
1
+ /** A cookie lives this long without being used again. */
2
+ export declare const COOKIE_LIFETIME_MS: number;
3
+ /** Most signed-in browsers at once. */
4
+ export declare const MAX_COOKIES = 50;
5
+ /** This many wrong tokens within the window locks logging in until the window has passed. */
6
+ export declare const MAX_FAILURES = 8;
7
+ export declare const FAILURE_WINDOW_MS: number;
8
+ export declare function newToken(): string;
9
+ /** Compares two secrets without leaking where they differ (the digests have the same length). */
10
+ export declare function sameSecret(a: string, b: string): boolean;
11
+ /** The Host header must name this PC's loopback and our port: anything else is DNS rebinding or a mistake. */
12
+ export declare function hostAllowed(host: string | undefined, port: number): boolean;
13
+ /** An Origin header (when there is one) must be our own page. */
14
+ export declare function originAllowed(origin: string | undefined, host: string | undefined, port: number): boolean;
15
+ /** State-changing calls need the custom header; a form or a no-CORS request from another site cannot send it. */
16
+ export declare function csrfOk(headers: Readonly<Record<string, string | string[] | undefined>>): boolean;
17
+ export declare function cookieValue(header: string | undefined, name?: string): string | undefined;
18
+ export type LoginResult = {
19
+ readonly ok: true;
20
+ readonly cookie: string;
21
+ } | {
22
+ readonly ok: false;
23
+ readonly status: 401 | 429;
24
+ };
25
+ export declare class WebAuth {
26
+ private readonly token;
27
+ private readonly now;
28
+ private readonly cookies;
29
+ private failures;
30
+ constructor(token: string, now?: () => number);
31
+ /** Trades the token for a new cookie value; wrong tokens are counted, and too many lock this for a minute. */
32
+ login(candidate: unknown): LoginResult;
33
+ /** The request's Cookie header carries a signed-in browser's cookie (and renews it). */
34
+ signedIn(cookieHeader: string | undefined): boolean;
35
+ private expire;
36
+ }
37
+ /** The Set-Cookie value: not readable by scripts, never sent by another site. */
38
+ export declare function cookieHeader(value: string): string;
@@ -0,0 +1,96 @@
1
+ // Who may talk to the web UI's server: the token trade, the session cookie, and the checks that stop other sites (and
2
+ // other names for this PC) from driving it. Pure apart from the clock and the random source (self-test covers it).
3
+ import crypto from 'node:crypto';
4
+ import { COOKIE_NAME, CSRF_HEADER, CSRF_VALUE } from '../../shared/web/index.js';
5
+ /** A cookie lives this long without being used again. */
6
+ export const COOKIE_LIFETIME_MS = 12 * 60 * 60 * 1000;
7
+ /** Most signed-in browsers at once. */
8
+ export const MAX_COOKIES = 50;
9
+ /** This many wrong tokens within the window locks logging in until the window has passed. */
10
+ export const MAX_FAILURES = 8;
11
+ export const FAILURE_WINDOW_MS = 60 * 1000;
12
+ export function newToken() {
13
+ return crypto.randomBytes(32).toString('hex');
14
+ }
15
+ /** Compares two secrets without leaking where they differ (the digests have the same length). */
16
+ export function sameSecret(a, b) {
17
+ const x = crypto.createHash('sha256').update(a).digest();
18
+ const y = crypto.createHash('sha256').update(b).digest();
19
+ return crypto.timingSafeEqual(x, y);
20
+ }
21
+ /** The Host header must name this PC's loopback and our port: anything else is DNS rebinding or a mistake. */
22
+ export function hostAllowed(host, port) {
23
+ return host === '127.0.0.1:' + port || host === 'localhost:' + port;
24
+ }
25
+ /** An Origin header (when there is one) must be our own page. */
26
+ export function originAllowed(origin, host, port) {
27
+ return host !== undefined && hostAllowed(host, port) && origin === 'http://' + host;
28
+ }
29
+ /** State-changing calls need the custom header; a form or a no-CORS request from another site cannot send it. */
30
+ export function csrfOk(headers) {
31
+ return headers[CSRF_HEADER] === CSRF_VALUE;
32
+ }
33
+ export function cookieValue(header, name = COOKIE_NAME) {
34
+ if (!header)
35
+ return undefined;
36
+ for (const part of header.split(';')) {
37
+ const eq = part.indexOf('=');
38
+ if (eq > 0 && part.slice(0, eq).trim() === name)
39
+ return part.slice(eq + 1).trim();
40
+ }
41
+ return undefined;
42
+ }
43
+ export class WebAuth {
44
+ token;
45
+ now;
46
+ cookies = new Map();
47
+ failures = [];
48
+ constructor(token, now = Date.now) {
49
+ this.token = token;
50
+ this.now = now;
51
+ }
52
+ /** Trades the token for a new cookie value; wrong tokens are counted, and too many lock this for a minute. */
53
+ login(candidate) {
54
+ const t = this.now();
55
+ this.failures = this.failures.filter((at) => t - at < FAILURE_WINDOW_MS);
56
+ if (this.failures.length >= MAX_FAILURES)
57
+ return { ok: false, status: 429 };
58
+ if (typeof candidate !== 'string' || candidate.length > 256 || !sameSecret(candidate, this.token)) {
59
+ this.failures.push(t);
60
+ return { ok: false, status: 401 };
61
+ }
62
+ this.expire(t);
63
+ if (this.cookies.size >= MAX_COOKIES) {
64
+ const oldest = [...this.cookies.entries()].sort((a, b) => a[1] - b[1])[0];
65
+ if (oldest)
66
+ this.cookies.delete(oldest[0]);
67
+ }
68
+ const cookie = crypto.randomBytes(32).toString('hex');
69
+ this.cookies.set(cookie, t);
70
+ return { ok: true, cookie };
71
+ }
72
+ /** The request's Cookie header carries a signed-in browser's cookie (and renews it). */
73
+ signedIn(cookieHeader) {
74
+ const value = cookieValue(cookieHeader);
75
+ if (!value)
76
+ return false;
77
+ const t = this.now();
78
+ this.expire(t);
79
+ for (const known of this.cookies.keys()) {
80
+ if (known.length === value.length && sameSecret(known, value)) {
81
+ this.cookies.set(known, t);
82
+ return true;
83
+ }
84
+ }
85
+ return false;
86
+ }
87
+ expire(t) {
88
+ for (const [cookie, last] of this.cookies)
89
+ if (t - last > COOKIE_LIFETIME_MS)
90
+ this.cookies.delete(cookie);
91
+ }
92
+ }
93
+ /** The Set-Cookie value: not readable by scripts, never sent by another site. */
94
+ export function cookieHeader(value) {
95
+ return COOKIE_NAME + '=' + value + '; Path=/; HttpOnly; SameSite=Strict; Max-Age=' + COOKIE_LIFETIME_MS / 1000;
96
+ }
@@ -0,0 +1,14 @@
1
+ import type { Endpoint } from '../../shared/pty/index.js';
2
+ import type { NewSessionRequest, WebInfo, WebSession } from '../../shared/web/index.js';
3
+ export interface WebBackend {
4
+ info(): Promise<WebInfo>;
5
+ /** Sessions as the browser may see them: never an attach token or address. */
6
+ sessions(): Promise<WebSession[]>;
7
+ /**
8
+ * Where to connect to a session's terminal (with its token, which stays on this PC), or null when there is no such
9
+ * running session, the sandbox is not shared with this user, or it has no address on this PC's sandbox network.
10
+ */
11
+ endpoint(sessionId: string): Endpoint | null;
12
+ /** Starts an agent in a project of the running sandbox and waits until it runs. */
13
+ create(request: NewSessionRequest): Promise<WebSession>;
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import type net from 'node:net';
2
+ import type { WebSocket } from 'ws';
3
+ import { type Endpoint } from '../../shared/pty/index.js';
4
+ export declare class TerminalBridge {
5
+ private readonly ws;
6
+ private readonly endpoint;
7
+ private readonly cols;
8
+ private readonly rows;
9
+ private readonly dial;
10
+ private socket;
11
+ private pinger;
12
+ private drain;
13
+ private stalledSince;
14
+ private done;
15
+ private readonly filter;
16
+ private readonly reader;
17
+ constructor(ws: WebSocket, endpoint: Endpoint, cols: number, rows: number, dial?: (endpoint: Endpoint) => Promise<net.Socket>);
18
+ /** Connects to the session and starts passing data both ways. */
19
+ start(): Promise<void>;
20
+ private fromBrowser;
21
+ private fromSession;
22
+ /** Stops reading from the session until the browser has taken what it was sent. */
23
+ private hold;
24
+ private tell;
25
+ /** Closes both sides (once). */
26
+ private finish;
27
+ }
@@ -0,0 +1,144 @@
1
+ import { safeText } from '../../core/util/index.js';
2
+ import { connect, encodeFrame, FrameReader, FrameType, helloFrame, OutputFilter, PING_INTERVAL_MS, resizeFrame, } from '../../shared/pty/index.js';
3
+ import { parseClientMessage } from '../../shared/web/index.js';
4
+ /** The browser is slower than the sandbox: past this much waiting to be sent, output is held back in the socket. */
5
+ const MAX_BUFFERED_BYTES = 4 * 1024 * 1024;
6
+ const DRAIN_CHECK_MS = 50;
7
+ /** A browser that cannot keep up for this long is closed. */
8
+ const MAX_STALLED_MS = 60 * 1000;
9
+ export class TerminalBridge {
10
+ ws;
11
+ endpoint;
12
+ cols;
13
+ rows;
14
+ dial;
15
+ socket = null;
16
+ pinger = null;
17
+ drain = null;
18
+ stalledSince = 0;
19
+ done = false;
20
+ filter = new OutputFilter();
21
+ reader = new FrameReader();
22
+ constructor(ws, endpoint, cols, rows, dial = connect) {
23
+ this.ws = ws;
24
+ this.endpoint = endpoint;
25
+ this.cols = cols;
26
+ this.rows = rows;
27
+ this.dial = dial;
28
+ }
29
+ /** Connects to the session and starts passing data both ways. */
30
+ async start() {
31
+ this.ws.on('message', (data, isBinary) => this.fromBrowser(data, isBinary));
32
+ this.ws.on('close', () => this.finish());
33
+ this.ws.on('error', () => this.finish());
34
+ let socket;
35
+ try {
36
+ socket = await this.dial(this.endpoint);
37
+ }
38
+ catch {
39
+ this.tell({ t: 'error', message: 'Could not reach the session in the sandbox.' });
40
+ this.finish(1011, 'unreachable');
41
+ return;
42
+ }
43
+ if (this.done) {
44
+ socket.destroy();
45
+ return;
46
+ }
47
+ this.socket = socket;
48
+ socket.on('data', (chunk) => this.fromSession(chunk));
49
+ socket.on('error', () => this.finish(1011, 'session connection failed'));
50
+ socket.on('close', () => this.finish(1011, 'session connection closed'));
51
+ socket.write(helloFrame(this.endpoint.token, this.cols, this.rows));
52
+ // The session drops a viewer it has not heard from in 15 s.
53
+ this.pinger = setInterval(() => this.socket?.write(encodeFrame(FrameType.Ping)), PING_INTERVAL_MS);
54
+ this.tell({ t: 'ready' });
55
+ }
56
+ fromBrowser(data, isBinary) {
57
+ if (!this.socket || this.done)
58
+ return;
59
+ const bytes = Array.isArray(data) ? Buffer.concat(data) : Buffer.from(data);
60
+ if (isBinary) {
61
+ if (bytes.length)
62
+ this.socket.write(encodeFrame(FrameType.Input, bytes));
63
+ return;
64
+ }
65
+ const message = parseClientMessage(bytes.toString('utf8'));
66
+ if (!message) {
67
+ this.finish(1008, 'bad message');
68
+ return;
69
+ }
70
+ this.socket.write(resizeFrame(message.cols, message.rows));
71
+ }
72
+ fromSession(chunk) {
73
+ let frames;
74
+ try {
75
+ frames = this.reader.push(chunk);
76
+ }
77
+ catch {
78
+ this.tell({ t: 'error', message: 'The session sent data that could not be read.' });
79
+ this.finish(1011, 'bad frame');
80
+ return;
81
+ }
82
+ for (const frame of frames) {
83
+ if (this.done)
84
+ return;
85
+ switch (frame.type) {
86
+ case FrameType.Output: {
87
+ const clean = this.filter.push(frame.payload);
88
+ if (clean.length)
89
+ this.ws.send(clean, { binary: true });
90
+ break;
91
+ }
92
+ case FrameType.Restarting:
93
+ this.tell({ t: 'restarting' });
94
+ break;
95
+ case FrameType.Exit:
96
+ this.tell({ t: 'exit', code: frame.payload.length >= 4 ? frame.payload.readInt32LE(0) : 1 });
97
+ this.finish(1000, 'exit');
98
+ return;
99
+ case FrameType.Error:
100
+ this.tell({ t: 'error', message: safeText(frame.payload.toString('utf8').slice(0, 500)) });
101
+ this.finish(1000, 'error');
102
+ return;
103
+ default:
104
+ break; // status and console frames are for terminals that draw the screen themselves
105
+ }
106
+ }
107
+ if (this.ws.bufferedAmount > MAX_BUFFERED_BYTES)
108
+ this.hold();
109
+ }
110
+ /** Stops reading from the session until the browser has taken what it was sent. */
111
+ hold() {
112
+ if (this.drain || !this.socket)
113
+ return;
114
+ this.socket.pause();
115
+ this.stalledSince = Date.now();
116
+ this.drain = setInterval(() => {
117
+ if (this.ws.bufferedAmount <= MAX_BUFFERED_BYTES / 4) {
118
+ if (this.drain)
119
+ clearInterval(this.drain);
120
+ this.drain = null;
121
+ this.socket?.resume();
122
+ }
123
+ else if (Date.now() - this.stalledSince > MAX_STALLED_MS)
124
+ this.finish(1013, 'too slow');
125
+ }, DRAIN_CHECK_MS);
126
+ }
127
+ tell(message) {
128
+ if (this.ws.readyState === this.ws.OPEN)
129
+ this.ws.send(JSON.stringify(message));
130
+ }
131
+ /** Closes both sides (once). */
132
+ finish(code = 1000, reason = '') {
133
+ if (this.done)
134
+ return;
135
+ this.done = true;
136
+ if (this.pinger)
137
+ clearInterval(this.pinger);
138
+ if (this.drain)
139
+ clearInterval(this.drain);
140
+ this.socket?.destroy();
141
+ if (this.ws.readyState === this.ws.OPEN || this.ws.readyState === this.ws.CONNECTING)
142
+ this.ws.close(code, reason);
143
+ }
144
+ }
@@ -0,0 +1 @@
1
+ export declare function openBrowser(url: string): void;
@@ -0,0 +1,10 @@
1
+ // Opens an address in the default browser (through cmd's start; the address is one we built, hex and digits only).
2
+ import { spawn } from 'node:child_process';
3
+ import { SYSTEM } from '../../shared/system.js';
4
+ import { refuseInSelfTest } from '../test-guard.js';
5
+ export function openBrowser(url) {
6
+ refuseInSelfTest('open a browser');
7
+ if (!/^http:\/\/127\.0\.0\.1:\d{1,5}\/(#k=[0-9a-f]{64})?$/.test(url))
8
+ throw new Error('Not a web UI address.');
9
+ spawn(SYSTEM.cmd(), ['/c', 'start', '', url], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
10
+ }
@@ -0,0 +1,17 @@
1
+ import type { WebStore, WebState } from './state.js';
2
+ export interface WebStatus {
3
+ readonly running: boolean;
4
+ readonly state: WebState | null;
5
+ }
6
+ export declare class WebControl {
7
+ private readonly store;
8
+ constructor(store: WebStore);
9
+ /** Running means the state file exists and its port answers as our server. */
10
+ status(): Promise<WebStatus>;
11
+ /** http://127.0.0.1:<port>/#k=<token>: the fragment never goes to the server. */
12
+ url(state: WebState): string;
13
+ /** Signs in with the token and asks the server to stop. False when it does not answer. */
14
+ stop(state: WebState): Promise<boolean>;
15
+ /** The state file of a server that is gone. */
16
+ clear(): void;
17
+ }