@songsid/agend 2.1.12-beta.9 → 2.2.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/README.md +2 -2
  2. package/README.zh-TW.md +2 -2
  3. package/dist/auth/one-time-code.d.ts +19 -0
  4. package/dist/auth/one-time-code.js +49 -0
  5. package/dist/auth/one-time-code.js.map +1 -0
  6. package/dist/auth-api.d.ts +52 -0
  7. package/dist/auth-api.js +346 -0
  8. package/dist/auth-api.js.map +1 -0
  9. package/dist/backend/claude-code.d.ts +4 -0
  10. package/dist/backend/claude-code.js +97 -0
  11. package/dist/backend/claude-code.js.map +1 -1
  12. package/dist/backend/codex.js +59 -30
  13. package/dist/backend/codex.js.map +1 -1
  14. package/dist/backend/kiro-agent.d.ts +66 -0
  15. package/dist/backend/kiro-agent.js +302 -0
  16. package/dist/backend/kiro-agent.js.map +1 -0
  17. package/dist/backend/kiro-identity.d.ts +80 -0
  18. package/dist/backend/kiro-identity.js +537 -0
  19. package/dist/backend/kiro-identity.js.map +1 -0
  20. package/dist/backend/kiro-v2-store-worker.d.ts +1 -0
  21. package/dist/backend/kiro-v2-store-worker.js +87 -0
  22. package/dist/backend/kiro-v2-store-worker.js.map +1 -0
  23. package/dist/backend/kiro-v2-store.d.ts +54 -0
  24. package/dist/backend/kiro-v2-store.js +131 -0
  25. package/dist/backend/kiro-v2-store.js.map +1 -0
  26. package/dist/backend/kiro-v3-identity.d.ts +5 -0
  27. package/dist/backend/kiro-v3-identity.js +22 -0
  28. package/dist/backend/kiro-v3-identity.js.map +1 -1
  29. package/dist/backend/kiro.d.ts +83 -7
  30. package/dist/backend/kiro.js +418 -91
  31. package/dist/backend/kiro.js.map +1 -1
  32. package/dist/backend/types.d.ts +45 -3
  33. package/dist/backend/types.js.map +1 -1
  34. package/dist/channel/adapters/discord.d.ts +1 -0
  35. package/dist/channel/adapters/discord.js +29 -10
  36. package/dist/channel/adapters/discord.js.map +1 -1
  37. package/dist/channel/adapters/telegram.d.ts +1 -0
  38. package/dist/channel/adapters/telegram.js +13 -1
  39. package/dist/channel/adapters/telegram.js.map +1 -1
  40. package/dist/channel/types.d.ts +9 -1
  41. package/dist/classic-channel-manager.d.ts +18 -10
  42. package/dist/classic-channel-manager.js +45 -18
  43. package/dist/classic-channel-manager.js.map +1 -1
  44. package/dist/cli.js +225 -127
  45. package/dist/cli.js.map +1 -1
  46. package/dist/command-table.d.ts +5 -3
  47. package/dist/command-table.js +29 -27
  48. package/dist/command-table.js.map +1 -1
  49. package/dist/config-validator.js +46 -0
  50. package/dist/config-validator.js.map +1 -1
  51. package/dist/config.js +12 -0
  52. package/dist/config.js.map +1 -1
  53. package/dist/connection-secrets.d.ts +6 -0
  54. package/dist/connection-secrets.js +4 -0
  55. package/dist/connection-secrets.js.map +1 -1
  56. package/dist/daemon.d.ts +89 -4
  57. package/dist/daemon.js +661 -77
  58. package/dist/daemon.js.map +1 -1
  59. package/dist/darwin-memory.d.ts +6 -1
  60. package/dist/darwin-memory.js +17 -6
  61. package/dist/darwin-memory.js.map +1 -1
  62. package/dist/delivery-consumed-watch.d.ts +85 -0
  63. package/dist/delivery-consumed-watch.js +244 -0
  64. package/dist/delivery-consumed-watch.js.map +1 -0
  65. package/dist/delivery-outbox.d.ts +61 -0
  66. package/dist/delivery-outbox.js +120 -2
  67. package/dist/delivery-outbox.js.map +1 -1
  68. package/dist/delivery-reconciliation.d.ts +14 -5
  69. package/dist/delivery-reconciliation.js +27 -4
  70. package/dist/delivery-reconciliation.js.map +1 -1
  71. package/dist/delivery-reconciliation.test.js +35 -1
  72. package/dist/delivery-reconciliation.test.js.map +1 -1
  73. package/dist/event-loop-watch.d.ts +28 -0
  74. package/dist/event-loop-watch.js +75 -3
  75. package/dist/event-loop-watch.js.map +1 -1
  76. package/dist/fleet-context.d.ts +22 -2
  77. package/dist/fleet-level-config.d.ts +2 -2
  78. package/dist/fleet-level-config.js +6 -1
  79. package/dist/fleet-level-config.js.map +1 -1
  80. package/dist/fleet-manager.d.ts +293 -12
  81. package/dist/fleet-manager.js +2364 -449
  82. package/dist/fleet-manager.js.map +1 -1
  83. package/dist/host-memory.d.ts +4 -0
  84. package/dist/host-memory.js.map +1 -1
  85. package/dist/instance-config-impact.js +5 -0
  86. package/dist/instance-config-impact.js.map +1 -1
  87. package/dist/instance-lifecycle.d.ts +10 -5
  88. package/dist/instance-lifecycle.js +162 -53
  89. package/dist/instance-lifecycle.js.map +1 -1
  90. package/dist/locale.js +150 -16
  91. package/dist/locale.js.map +1 -1
  92. package/dist/login-controller.d.ts +2 -0
  93. package/dist/login-controller.js +17 -5
  94. package/dist/login-controller.js.map +1 -1
  95. package/dist/memory-pressure.d.ts +7 -8
  96. package/dist/memory-pressure.js +76 -15
  97. package/dist/memory-pressure.js.map +1 -1
  98. package/dist/monotonic-budget.d.ts +2 -0
  99. package/dist/monotonic-budget.js +28 -0
  100. package/dist/monotonic-budget.js.map +1 -0
  101. package/dist/needs-you-hub.d.ts +114 -0
  102. package/dist/needs-you-hub.js +370 -0
  103. package/dist/needs-you-hub.js.map +1 -0
  104. package/dist/needs-you-live.d.ts +116 -0
  105. package/dist/needs-you-live.js +228 -0
  106. package/dist/needs-you-live.js.map +1 -0
  107. package/dist/needs-you.d.ts +132 -0
  108. package/dist/needs-you.js +164 -0
  109. package/dist/needs-you.js.map +1 -0
  110. package/dist/node-version-guard.d.ts +30 -0
  111. package/dist/node-version-guard.js +50 -0
  112. package/dist/node-version-guard.js.map +1 -0
  113. package/dist/operator-control-paths.d.ts +4 -0
  114. package/dist/operator-control-paths.js +24 -0
  115. package/dist/operator-control-paths.js.map +1 -0
  116. package/dist/outbound-handlers.d.ts +2 -0
  117. package/dist/outbound-handlers.js +9 -2
  118. package/dist/outbound-handlers.js.map +1 -1
  119. package/dist/outbound-schemas.d.ts +5 -5
  120. package/dist/profile-control.js +4 -20
  121. package/dist/profile-control.js.map +1 -1
  122. package/dist/public-web-gateway.d.ts +18 -0
  123. package/dist/public-web-gateway.js +141 -0
  124. package/dist/public-web-gateway.js.map +1 -0
  125. package/dist/public-web-link.d.ts +53 -0
  126. package/dist/public-web-link.js +175 -0
  127. package/dist/public-web-link.js.map +1 -0
  128. package/dist/quickstart-api.d.ts +8 -1
  129. package/dist/quickstart-api.js +86 -48
  130. package/dist/quickstart-api.js.map +1 -1
  131. package/dist/quickstart.d.ts +1 -0
  132. package/dist/quickstart.js +6 -2
  133. package/dist/quickstart.js.map +1 -1
  134. package/dist/scheduler/db.d.ts +7 -1
  135. package/dist/scheduler/db.js +38 -0
  136. package/dist/scheduler/db.js.map +1 -1
  137. package/dist/scheduler/index.d.ts +1 -1
  138. package/dist/scheduler/retry-label.d.ts +11 -0
  139. package/dist/scheduler/retry-label.js +24 -0
  140. package/dist/scheduler/retry-label.js.map +1 -0
  141. package/dist/scheduler/scheduler.d.ts +55 -2
  142. package/dist/scheduler/scheduler.js +220 -8
  143. package/dist/scheduler/scheduler.js.map +1 -1
  144. package/dist/scheduler/types.d.ts +22 -0
  145. package/dist/scheduler/types.js.map +1 -1
  146. package/dist/secret-store.d.ts +5 -0
  147. package/dist/secret-store.js +20 -0
  148. package/dist/secret-store.js.map +1 -1
  149. package/dist/service-activation.d.ts +110 -0
  150. package/dist/service-activation.js +302 -0
  151. package/dist/service-activation.js.map +1 -0
  152. package/dist/service-installer.d.ts +7 -0
  153. package/dist/service-installer.js +7 -4
  154. package/dist/service-installer.js.map +1 -1
  155. package/dist/service-restart-selection.d.ts +6 -5
  156. package/dist/service-restart-selection.js +6 -5
  157. package/dist/service-restart-selection.js.map +1 -1
  158. package/dist/settings-api.d.ts +10 -2
  159. package/dist/settings-api.js +175 -72
  160. package/dist/settings-api.js.map +1 -1
  161. package/dist/settings-baseline.d.ts +20 -0
  162. package/dist/settings-baseline.js +104 -0
  163. package/dist/settings-baseline.js.map +1 -0
  164. package/dist/settings-change.d.ts +21 -0
  165. package/dist/settings-change.js +133 -0
  166. package/dist/settings-change.js.map +1 -0
  167. package/dist/settings-confirmation.d.ts +105 -0
  168. package/dist/settings-confirmation.js +232 -0
  169. package/dist/settings-confirmation.js.map +1 -0
  170. package/dist/settings-control.d.ts +41 -0
  171. package/dist/settings-control.js +294 -0
  172. package/dist/settings-control.js.map +1 -0
  173. package/dist/settings-effect.d.ts +24 -0
  174. package/dist/settings-effect.js +169 -0
  175. package/dist/settings-effect.js.map +1 -0
  176. package/dist/settings-http-confirmation.d.ts +29 -0
  177. package/dist/settings-http-confirmation.js +123 -0
  178. package/dist/settings-http-confirmation.js.map +1 -0
  179. package/dist/settings-instance-patch.d.ts +4 -0
  180. package/dist/settings-instance-patch.js +26 -0
  181. package/dist/settings-instance-patch.js.map +1 -0
  182. package/dist/settings-request-capability.d.ts +16 -0
  183. package/dist/settings-request-capability.js +32 -0
  184. package/dist/settings-request-capability.js.map +1 -0
  185. package/dist/settings-transaction.d.ts +65 -0
  186. package/dist/settings-transaction.js +223 -0
  187. package/dist/settings-transaction.js.map +1 -0
  188. package/dist/setup-auth.d.ts +10 -18
  189. package/dist/setup-auth.js +14 -38
  190. package/dist/setup-auth.js.map +1 -1
  191. package/dist/setup-form.d.ts +1 -1
  192. package/dist/setup-form.js +75 -18
  193. package/dist/setup-form.js.map +1 -1
  194. package/dist/setup-guide.d.ts +13 -0
  195. package/dist/setup-guide.js +25 -0
  196. package/dist/setup-guide.js.map +1 -0
  197. package/dist/setup-host.d.ts +16 -0
  198. package/dist/setup-host.js +197 -32
  199. package/dist/setup-host.js.map +1 -1
  200. package/dist/setup-wizard.js +3 -0
  201. package/dist/setup-wizard.js.map +1 -1
  202. package/dist/slash-authz.d.ts +17 -3
  203. package/dist/slash-authz.js +5 -0
  204. package/dist/slash-authz.js.map +1 -1
  205. package/dist/spawn-gate.js +3 -3
  206. package/dist/spawn-gate.js.map +1 -1
  207. package/dist/statusline-watcher.d.ts +5 -0
  208. package/dist/statusline-watcher.js +2 -0
  209. package/dist/statusline-watcher.js.map +1 -1
  210. package/dist/steer-capability.d.ts +6 -0
  211. package/dist/steer-capability.js +8 -0
  212. package/dist/steer-capability.js.map +1 -1
  213. package/dist/sync-work-attribution.d.ts +14 -0
  214. package/dist/sync-work-attribution.js +43 -8
  215. package/dist/sync-work-attribution.js.map +1 -1
  216. package/dist/tips.js +10 -5
  217. package/dist/tips.js.map +1 -1
  218. package/dist/tmux-control.d.ts +35 -5
  219. package/dist/tmux-control.js +291 -57
  220. package/dist/tmux-control.js.map +1 -1
  221. package/dist/tmux-manager.d.ts +20 -6
  222. package/dist/tmux-manager.js +90 -47
  223. package/dist/tmux-manager.js.map +1 -1
  224. package/dist/tmux-read.d.ts +59 -0
  225. package/dist/tmux-read.js +213 -0
  226. package/dist/tmux-read.js.map +1 -0
  227. package/dist/topic-commands.d.ts +21 -10
  228. package/dist/topic-commands.js +191 -58
  229. package/dist/topic-commands.js.map +1 -1
  230. package/dist/transcript-delta-reader.js +2 -1
  231. package/dist/transcript-delta-reader.js.map +1 -1
  232. package/dist/tunnel/cloudflared-install.d.ts +2 -0
  233. package/dist/tunnel/cloudflared-install.js +1 -1
  234. package/dist/tunnel/cloudflared-install.js.map +1 -1
  235. package/dist/tunnel/purpose-lane.d.ts +14 -0
  236. package/dist/tunnel/purpose-lane.js +43 -0
  237. package/dist/tunnel/purpose-lane.js.map +1 -0
  238. package/dist/turn-reply-guard.d.ts +2 -0
  239. package/dist/turn-reply-guard.js +10 -1
  240. package/dist/turn-reply-guard.js.map +1 -1
  241. package/dist/types.d.ts +33 -0
  242. package/dist/ui/app.html +18 -0
  243. package/dist/ui/chat-render.js +478 -0
  244. package/dist/ui/chat-store.js +289 -0
  245. package/dist/ui/chat-strings.js +69 -0
  246. package/dist/ui/chat-thread.js +209 -0
  247. package/dist/ui/chat-tour.js +116 -0
  248. package/dist/ui/fleet-org.js +142 -0
  249. package/dist/ui/panel-chat.js +418 -0
  250. package/dist/ui/panel-fleet.js +413 -0
  251. package/dist/ui/panel-needs.js +202 -0
  252. package/dist/ui/panel-settings.js +750 -0
  253. package/dist/ui/preview.js +266 -0
  254. package/dist/ui/settings-apply.js +239 -0
  255. package/dist/ui/settings-confirm.js +186 -0
  256. package/dist/ui/settings-dialogs.js +546 -0
  257. package/dist/ui/settings-model.js +277 -0
  258. package/dist/ui/settings-strings.js +652 -0
  259. package/dist/ui/settings-wizard.js +140 -0
  260. package/dist/ui/shared/agend-auth.js +93 -0
  261. package/dist/ui/shared/app-ctx.js +80 -0
  262. package/dist/ui/shared/app-html.js +9 -0
  263. package/dist/ui/shared/app-i18n.js +95 -0
  264. package/dist/ui/shared/app-nav.js +73 -0
  265. package/dist/ui/shared/app-needs.js +114 -0
  266. package/dist/ui/shared/app-route.js +73 -0
  267. package/dist/ui/shared/app-session.js +66 -0
  268. package/dist/ui/shared/app-shell.js +344 -0
  269. package/dist/ui/shared/app-store.js +51 -0
  270. package/dist/ui/shared/app-stream.js +201 -0
  271. package/dist/ui/shared/app.css +676 -0
  272. package/dist/ui/shared/app.js +88 -0
  273. package/dist/ui/shared/fonts/OFL.txt +92 -0
  274. package/dist/ui/shared/fonts/README.md +21 -0
  275. package/dist/ui/shared/fonts/inter.woff2 +0 -0
  276. package/dist/ui/shared/htm.module.js +1 -0
  277. package/dist/ui/shared/panel-view.js +655 -0
  278. package/dist/ui/shared/preact-hooks.module.js +2 -0
  279. package/dist/ui/shared/preact.module.js +2 -0
  280. package/dist/ui/shared/signin.js +130 -0
  281. package/dist/ui/shared/theme.js +29 -0
  282. package/dist/ui/shared/tokens.css +153 -0
  283. package/dist/ui/shared/ui-confirm.js +43 -0
  284. package/dist/ui/shared/ui-dialog.js +45 -0
  285. package/dist/ui/shared/ui-icons.js +61 -0
  286. package/dist/ui/shared/ui-menu.js +39 -0
  287. package/dist/ui/shared/ui-states.js +20 -0
  288. package/dist/ui/shared/ui-toast.js +18 -0
  289. package/dist/ui/shared/vendor/htm.LICENSE +202 -0
  290. package/dist/ui/shared/vendor/preact.LICENSE +21 -0
  291. package/dist/ui/shared/vendor/vendor.json +39 -0
  292. package/dist/ui/shared/view-strings.js +309 -0
  293. package/dist/ui/signin.html +51 -0
  294. package/dist/update-check.d.ts +6 -2
  295. package/dist/update-check.js +12 -3
  296. package/dist/update-check.js.map +1 -1
  297. package/dist/update-install.d.ts +71 -0
  298. package/dist/update-install.js +162 -0
  299. package/dist/update-install.js.map +1 -0
  300. package/dist/upgrade-notices.d.ts +17 -0
  301. package/dist/upgrade-notices.js +93 -0
  302. package/dist/upgrade-notices.js.map +1 -0
  303. package/dist/usage/statusline-usage.d.ts +2 -0
  304. package/dist/usage/statusline-usage.js +14 -10
  305. package/dist/usage/statusline-usage.js.map +1 -1
  306. package/dist/view-api.d.ts +6 -1
  307. package/dist/view-api.js +62 -51
  308. package/dist/view-api.js.map +1 -1
  309. package/dist/web-api.d.ts +71 -14
  310. package/dist/web-api.js +492 -77
  311. package/dist/web-api.js.map +1 -1
  312. package/dist/web-auth.d.ts +71 -27
  313. package/dist/web-auth.js +169 -69
  314. package/dist/web-auth.js.map +1 -1
  315. package/dist/web-body.d.ts +3 -0
  316. package/dist/web-body.js +55 -0
  317. package/dist/web-body.js.map +1 -0
  318. package/dist/web-channel-echo.d.ts +9 -0
  319. package/dist/web-channel-echo.js +55 -0
  320. package/dist/web-channel-echo.js.map +1 -0
  321. package/dist/web-chat-history.d.ts +125 -0
  322. package/dist/web-chat-history.js +184 -0
  323. package/dist/web-chat-history.js.map +1 -0
  324. package/dist/web-continuation.d.ts +7 -0
  325. package/dist/web-continuation.js +22 -0
  326. package/dist/web-continuation.js.map +1 -0
  327. package/dist/web-host-guard.d.ts +34 -0
  328. package/dist/web-host-guard.js +57 -1
  329. package/dist/web-host-guard.js.map +1 -1
  330. package/dist/web-login.d.ts +72 -0
  331. package/dist/web-login.js +132 -0
  332. package/dist/web-login.js.map +1 -0
  333. package/dist/web-org.d.ts +55 -0
  334. package/dist/web-org.js +67 -0
  335. package/dist/web-org.js.map +1 -0
  336. package/dist/web-preview.d.ts +77 -0
  337. package/dist/web-preview.js +240 -0
  338. package/dist/web-preview.js.map +1 -0
  339. package/dist/web-request-context.d.ts +10 -0
  340. package/dist/web-request-context.js +13 -0
  341. package/dist/web-request-context.js.map +1 -0
  342. package/dist/web-session.d.ts +167 -0
  343. package/dist/web-session.js +389 -0
  344. package/dist/web-session.js.map +1 -0
  345. package/dist/web-shell-routes.d.ts +59 -0
  346. package/dist/web-shell-routes.js +96 -0
  347. package/dist/web-shell-routes.js.map +1 -0
  348. package/dist/web-upload.d.ts +183 -0
  349. package/dist/web-upload.js +468 -0
  350. package/dist/web-upload.js.map +1 -0
  351. package/dist/website-tips-renderer.d.ts +2 -2
  352. package/dist/website-tips-renderer.js +3 -3
  353. package/dist/website-tips-renderer.js.map +1 -1
  354. package/package.json +8 -5
  355. package/scripts/preinstall-guard.cjs +36 -0
  356. package/dist/ui/dashboard.html +0 -774
  357. package/dist/ui/settings.html +0 -2165
  358. package/dist/ui/view.html +0 -1139
@@ -0,0 +1,141 @@
1
+ import { createServer } from "node:http";
2
+ import { bindGatewayRequest } from "./web-request-context.js";
3
+ import { applyWebSecurityHeaders } from "./web-host-guard.js";
4
+ import { isServedAsset } from "./auth-api.js";
5
+ import { shellRoute } from "./web-shell-routes.js";
6
+ /** Reviewed panel routes. No health, agent, issue-code, preview, legacy restart or SSE. */
7
+ export function isPublicWebRoute(method, path) {
8
+ const reads = method === "GET" || method === "HEAD";
9
+ if (reads && ["/", "/signin", "/ui", "/view", "/auth/session", "/auth/sessions", "/api/fleet", "/api/profiles", "/api/sort-order", "/api/ai-usage"].includes(path))
10
+ return true;
11
+ if (method === "GET" && path.startsWith("/assets/") && isServedAsset(path.slice("/assets/".length)))
12
+ return true;
13
+ // #1408: the app shell's pages, exactly as the classifier names them (a malformed one is answered 400 behind it).
14
+ if (reads && shellRoute(method, path) !== null)
15
+ return true;
16
+ if (method === "POST" && ["/auth/login", "/auth/logout"].includes(path))
17
+ return true;
18
+ if (method === "DELETE" && /^\/auth\/sessions(?:\/[0-9a-f]{16})?$/.test(path))
19
+ return true;
20
+ if (reads && /^\/api\/(pane|profile|avatar)\/[^/]+$/.test(path))
21
+ return true;
22
+ if (method === "POST" && (path === "/api/sort-order" || /^\/api\/(profile|avatar)\/[^/]+$/.test(path)))
23
+ return true;
24
+ if (method === "POST" && (/^\/stop\/[^/]+$/.test(path) || /^\/api\/instance\/[^/]+\/start$/.test(path)))
25
+ return true;
26
+ if (method === "GET" && /^\/ui\/js\/[a-z0-9_-]+\.js$/.test(path))
27
+ return true;
28
+ if (method === "GET" && /^\/ui\/(backends|poll|history|file\/[^/]+|prompts|instance\/[^/]+|instances|tasks(?:\/[^/]+)?|schedules(?:\/[^/]+)?|teams(?:\/[^/]+)?|config|org)$/.test(path))
29
+ return true;
30
+ if (method === "POST" && /^\/ui\/(send|upload|prompt|needs\/ack|cancel\/[^/]+|stop\/[^/]+|start\/[^/]+|instances\/[^/]+\/delete|restart\/[^/]+|instances|tasks|schedules|teams|config)$/.test(path))
31
+ return true;
32
+ if (((method === "POST" && /^\/ui\/tasks\/[^/]+$/.test(path))) || (method === "DELETE" && /^\/ui\/(schedules|teams)\/[^/]+$/.test(path)))
33
+ return true;
34
+ // Settings has an explicit method/subroute manifest (not an arbitrary /api prefix).
35
+ return isPublicSettingsRoute(method, path);
36
+ }
37
+ function isPublicSettingsRoute(method, path) {
38
+ if (method === "GET" && /^\/api\/settings\/pending(?:\/[0-9a-f]{32})?$/.test(path))
39
+ return true;
40
+ if (method === "DELETE" && /^\/api\/settings\/pending\/[0-9a-f]{32}$/.test(path))
41
+ return true;
42
+ if (method === "GET")
43
+ return /^\/api\/settings\/(schema|fleet|fleet\/raw|classic|status-emojis|status-emojis\/guild-emojis|connections|provider-secrets|secrets|apply\/[^/]+|quickstart\/environment)$/.test(path)
44
+ || /^\/api\/settings\/(provider-secrets|secrets)\/[^/]+\/apply\/[^/]+$/.test(path)
45
+ || /^\/api\/settings\/connections\/[^/]+\/(secret|binding)\/apply\/[^/]+$/.test(path);
46
+ if (method === "POST")
47
+ return /^\/api\/settings\/(status-emojis\/preview|apply|restart-fleet|reload|fleet\/instances|quickstart\/(probe|plan|commit))$/.test(path)
48
+ || /^\/api\/settings\/(provider-secrets|secrets)\/[^/]+\/(verify|apply)$/.test(path)
49
+ || /^\/api\/settings\/connections\/[^/]+\/(secret|binding)\/(verify|apply)$/.test(path)
50
+ || /^\/api\/settings\/instances\/[^/]+\/(pause|wake)$/.test(path);
51
+ if (method === "PUT")
52
+ return /^\/api\/settings\/(fleet\/(web|defaults|channels)|classic\/defaults)$/.test(path);
53
+ if (method === "PATCH" && /^\/api\/settings\/classic\/channels\/[^/]+$/.test(path))
54
+ return true;
55
+ return ["POST", "PATCH", "DELETE"].includes(method) && /^\/api\/settings\/fleet\/instances\/[^/]+$/.test(path);
56
+ }
57
+ export function createPublicWebGateway(opts) {
58
+ let host = null;
59
+ const marker = `agend-public-${opts.exposureId}`;
60
+ const sockets = new Set();
61
+ const reject = (res, status) => { res.writeHead(status); res.end(JSON.stringify({ error: "public web request refused" })); };
62
+ const server = (opts.create ?? createServer)((req, res) => {
63
+ applyWebSecurityHeaders(res);
64
+ res.setHeader("Cache-Control", "no-store");
65
+ res.setHeader("Referrer-Policy", "no-referrer");
66
+ res.setHeader("Vary", "Cookie");
67
+ const raw = req.url ?? "/";
68
+ try {
69
+ if (!opts.isCurrent() || !host || req.headers.host !== host)
70
+ return reject(res, 403);
71
+ if (!raw.startsWith("/") || raw.startsWith("//") || /[\\\u0000-\u0020\u007f]/.test(raw))
72
+ return reject(res, 400);
73
+ const url = new URL(raw, `https://${host}`);
74
+ // Decode once now; reject malformed encodings before any shared route can throw.
75
+ const decoded = decodeURIComponent(url.pathname);
76
+ if (decoded.includes("\\") || decoded.includes("\0") || /%2f|%5c/i.test(url.pathname))
77
+ return reject(res, 400);
78
+ if (!isPublicWebRoute(req.method ?? "GET", url.pathname))
79
+ return reject(res, 404);
80
+ if (!opts.isOpen()) {
81
+ if ((req.method === "GET" || req.method === "HEAD") && url.pathname === "/signin") {
82
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
83
+ res.end(`<html><body>${marker}</body></html>`);
84
+ return;
85
+ }
86
+ return reject(res, 503);
87
+ }
88
+ bindGatewayRequest(req, { surface: "gateway", exposureId: opts.exposureId, expectedOrigin: `https://${host}`, isCurrent: () => opts.isCurrent() && opts.isOpen() });
89
+ opts.dispatch(req, res);
90
+ }
91
+ catch {
92
+ if (!res.headersSent)
93
+ reject(res, 400);
94
+ else
95
+ res.destroy();
96
+ }
97
+ });
98
+ server.maxHeadersCount = 64;
99
+ server.headersTimeout = 5_000;
100
+ server.requestTimeout = 10_000;
101
+ server.keepAliveTimeout = 5_000;
102
+ server.setTimeout(15_000, socket => socket.destroy());
103
+ server.on("connection", socket => { if (sockets.size >= 32) {
104
+ socket.destroy();
105
+ return;
106
+ } sockets.add(socket); socket.on("close", () => sockets.delete(socket)); });
107
+ // Upgrades are never accepted, but they still leave HTTP's ordinary connection accounting.
108
+ server.on("upgrade", (_req, socket) => socket.destroy());
109
+ server.on("connect", (_req, socket) => socket.destroy());
110
+ server.on("clientError", (_err, socket) => socket.destroy());
111
+ server.on("error", () => { host = null; opts.onError?.(); });
112
+ return {
113
+ server, readinessMarker: marker,
114
+ listen: () => new Promise((resolve, rejectListen) => {
115
+ server.once("error", rejectListen);
116
+ server.listen(0, "127.0.0.1", () => {
117
+ server.off("error", rejectListen);
118
+ const address = server.address();
119
+ if (!address || typeof address === "string") {
120
+ rejectListen(new Error("gateway not bound"));
121
+ return;
122
+ }
123
+ resolve(new URL(`http://127.0.0.1:${address.port}`));
124
+ });
125
+ }),
126
+ setHost: value => {
127
+ // Provider-validated only, pinned again at this boundary.
128
+ if (value !== null && !/^[a-z0-9-]+\.trycloudflare\.com$/.test(value))
129
+ throw new Error("invalid tunnel host");
130
+ host = value;
131
+ },
132
+ close: () => {
133
+ host = null;
134
+ server.close();
135
+ server.closeAllConnections();
136
+ for (const socket of sockets)
137
+ socket.destroy();
138
+ },
139
+ };
140
+ }
141
+ //# sourceMappingURL=public-web-gateway.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-web-gateway.js","sourceRoot":"","sources":["../src/public-web-gateway.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ import { CloudflaredProvider } from "./tunnel/cloudflared.js";
2
+ import { ensureCloudflared } from "./tunnel/cloudflared-install.js";
3
+ import type { TunnelReservation } from "./tunnel/purpose-lane.js";
4
+ import type { TunnelStopResult } from "./tunnel/types.js";
5
+ import type { PublicGateway } from "./public-web-gateway.js";
6
+ import type { LoginCodeOwner } from "./web-login.js";
7
+ import type { WebConfig } from "./types.js";
8
+ export declare const PUBLIC_LINK_DEFAULT_MINUTES = 120;
9
+ export declare const PUBLIC_LINK_MAX_MINUTES = 480;
10
+ export declare function publicLinkSettings(web: WebConfig | undefined): {
11
+ allowed: boolean;
12
+ ttlMs: number;
13
+ protocol: "http2" | "quic" | "auto";
14
+ };
15
+ export declare function validPublicLinkPatch(value: unknown): value is NonNullable<WebConfig["public_link"]>;
16
+ export interface PublicLinkDelivery {
17
+ readonly exposureId: string;
18
+ readonly url: string;
19
+ readonly expiresAt: number;
20
+ readonly isCurrent: () => boolean;
21
+ }
22
+ /** Owns access before child cleanup: losing a tunnel cannot leave a public web session alive. */
23
+ export declare class PublicWebLink {
24
+ private readonly deps;
25
+ private entry;
26
+ private readonly now;
27
+ constructor(deps: {
28
+ dataDir: string;
29
+ web(): WebConfig | undefined;
30
+ permitted(owner: LoginCodeOwner): boolean;
31
+ reserve(id: string): TunnelReservation | null;
32
+ createGateway(id: string, current: () => boolean, open: () => boolean, failed: () => void): PublicGateway;
33
+ revoke(id: string): void;
34
+ log(event: string, id: string): void;
35
+ onCleanupUnconfirmed?(result: TunnelStopResult, owner: LoginCodeOwner): void;
36
+ now?: () => number;
37
+ wallNow?: () => number;
38
+ ensure?: typeof ensureCloudflared;
39
+ provider?: (binary: string, protocol: "http2" | "quic" | "auto") => CloudflaredProvider;
40
+ });
41
+ get exposureId(): string | undefined;
42
+ status(): {
43
+ state: string;
44
+ expiresAt?: number;
45
+ remainingSeconds?: number;
46
+ };
47
+ private current;
48
+ refresh(): void;
49
+ /** Multiple same-start requests share delivery proof: failed A may never close confirmed B. */
50
+ deliver(owner: LoginCodeOwner, send: (link: PublicLinkDelivery) => Promise<boolean>): Promise<boolean>;
51
+ private start;
52
+ close(reason: string, expectedId?: string): Promise<TunnelStopResult>;
53
+ }
@@ -0,0 +1,175 @@
1
+ import { performance } from "node:perf_hooks";
2
+ import { newTunnelSid } from "./tunnel/manager.js";
3
+ import { CloudflaredProvider } from "./tunnel/cloudflared.js";
4
+ import { ensureCloudflared } from "./tunnel/cloudflared-install.js";
5
+ export const PUBLIC_LINK_DEFAULT_MINUTES = 120;
6
+ export const PUBLIC_LINK_MAX_MINUTES = 480;
7
+ export function publicLinkSettings(web) {
8
+ // Raw YAML startup does not go through the Settings validator. Enforce the
9
+ // fixed cap here too, at the boundary every runtime consumer shares.
10
+ const settings = web?.public_link;
11
+ const minutes = typeof settings?.ttl_minutes === "number" && Number.isInteger(settings.ttl_minutes) && settings.ttl_minutes > 0
12
+ ? Math.min(settings.ttl_minutes, PUBLIC_LINK_MAX_MINUTES) : PUBLIC_LINK_DEFAULT_MINUTES;
13
+ const protocol = settings?.protocol;
14
+ return { allowed: settings?.allow_public === undefined || settings.allow_public === true, ttlMs: minutes * 60_000,
15
+ protocol: typeof protocol === "string" && ["http2", "quic", "auto"].includes(protocol) ? protocol : "http2" };
16
+ }
17
+ export function validPublicLinkPatch(value) {
18
+ if (!value || typeof value !== "object" || Array.isArray(value))
19
+ return false;
20
+ const v = value;
21
+ return Object.keys(v).every(k => ["allow_public", "ttl_minutes", "protocol"].includes(k))
22
+ && (v.allow_public === undefined || typeof v.allow_public === "boolean")
23
+ && (v.ttl_minutes === undefined || (Number.isInteger(v.ttl_minutes) && Number(v.ttl_minutes) >= 1 && Number(v.ttl_minutes) <= PUBLIC_LINK_MAX_MINUTES))
24
+ && (v.protocol === undefined || (typeof v.protocol === "string" && ["http2", "quic", "auto"].includes(v.protocol)));
25
+ }
26
+ /** Owns access before child cleanup: losing a tunnel cannot leave a public web session alive. */
27
+ export class PublicWebLink {
28
+ deps;
29
+ entry = null;
30
+ now;
31
+ constructor(deps) {
32
+ this.deps = deps;
33
+ this.now = deps.now ?? (() => performance.now());
34
+ }
35
+ get exposureId() { return this.entry?.id; }
36
+ status() {
37
+ const e = this.entry;
38
+ if (e && this.now() >= e.deadline && !e.closing)
39
+ void this.close("expired");
40
+ return e ? { state: e.phase, expiresAt: e.expiresAt, remainingSeconds: Math.max(0, Math.ceil((e.deadline - this.now()) / 1000)) } : { state: "closed" };
41
+ }
42
+ current(e) {
43
+ return this.entry === e && !e.closing && !e.abort.signal.aborted && this.now() < e.deadline
44
+ && publicLinkSettings(this.deps.web()).allowed && this.deps.permitted(e.owner);
45
+ }
46
+ refresh() {
47
+ const e = this.entry;
48
+ if (e && !this.current(e))
49
+ void this.close("policy or binding changed");
50
+ }
51
+ /** Multiple same-start requests share delivery proof: failed A may never close confirmed B. */
52
+ async deliver(owner, send) {
53
+ if (!this.deps.permitted(owner) || !publicLinkSettings(this.deps.web()).allowed)
54
+ return false;
55
+ let e = this.entry;
56
+ if (e && (e.owner.adapterId !== owner.adapterId || e.owner.chatId !== owner.chatId || e.owner.threadId !== owner.threadId || !this.current(e)))
57
+ return false;
58
+ if (!e) {
59
+ const settings = publicLinkSettings(this.deps.web());
60
+ const id = newTunnelSid();
61
+ const reservation = this.deps.reserve(id); // Before installer, listener, or any await.
62
+ if (!reservation)
63
+ return false;
64
+ e = { id, owner: { ...owner }, reservation, abort: new AbortController(), deadline: this.now() + settings.ttlMs,
65
+ expiresAt: (this.deps.wallNow ?? Date.now)() + settings.ttlMs, phase: "starting", gateway: null, starting: Promise.resolve(), requests: 0, delivered: false };
66
+ this.entry = e;
67
+ const captured = e;
68
+ const expire = () => {
69
+ if (this.entry !== captured || captured.closing)
70
+ return;
71
+ const remaining = captured.deadline - this.now();
72
+ if (remaining <= 0)
73
+ void this.close("expired");
74
+ else {
75
+ captured.timer = setTimeout(expire, Math.ceil(remaining));
76
+ captured.timer.unref?.();
77
+ }
78
+ };
79
+ expire();
80
+ e.starting = this.start(e, settings.protocol);
81
+ this.deps.log("requested", id);
82
+ }
83
+ e.requests++;
84
+ try {
85
+ await e.starting;
86
+ if (!this.current(e) || !this.deps.permitted(owner) || !e.url)
87
+ return false;
88
+ const delivered = await send({ exposureId: e.id, url: e.url, expiresAt: e.expiresAt, isCurrent: () => this.current(e) && this.deps.permitted(owner) });
89
+ if (!delivered || !this.current(e) || !this.deps.permitted(owner))
90
+ return false;
91
+ e.delivered = true;
92
+ this.deps.log("privately delivered", e.id);
93
+ return true;
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ finally {
99
+ e.requests--;
100
+ if (!e.delivered && e.requests === 0 && this.entry === e)
101
+ await this.close("no confirmed private recipient");
102
+ }
103
+ }
104
+ async start(e, protocol) {
105
+ try {
106
+ const binary = await (this.deps.ensure ?? ensureCloudflared)({ dataDir: this.deps.dataDir, pinnedOnly: true, signal: e.abort.signal });
107
+ if (!this.current(e))
108
+ throw new Error("closed");
109
+ const gateway = this.deps.createGateway(e.id, () => this.current(e), () => e.phase === "open", () => { void this.close("gateway failed", e.id); });
110
+ e.gateway = gateway;
111
+ const origin = await gateway.listen();
112
+ if (!this.current(e))
113
+ throw new Error("closed");
114
+ const provider = (this.deps.provider ?? ((path, p) => new CloudflaredProvider({ binaryName: path, protocol: p })))(binary.path, protocol);
115
+ const result = await e.reservation.start(provider, {
116
+ sid: e.id, origin, pagePath: "/signin", readinessMarker: gateway.readinessMarker, expiresAt: e.expiresAt,
117
+ signal: e.abort.signal, onCandidateHost: host => { if (this.current(e))
118
+ gateway.setHost(host); },
119
+ });
120
+ if (!result.ok || !this.current(e))
121
+ throw new Error("tunnel unavailable");
122
+ const published = new URL(result.handle.pageUrl);
123
+ if (published.protocol !== "https:" || published.pathname !== "/signin" || published.username || published.password || published.search || published.hash)
124
+ throw new Error("invalid public page");
125
+ gateway.setHost(published.host);
126
+ e.url = published.href;
127
+ e.unsubscribe = result.handle.onUnexpectedExit(() => { void this.close("tunnel exited", e.id); });
128
+ if (!this.current(e))
129
+ throw new Error("closed");
130
+ e.phase = "open";
131
+ this.deps.log("opened", e.id);
132
+ }
133
+ catch {
134
+ // deliver's finally joins close after this promise settles; no self-await.
135
+ e.abort.abort();
136
+ e.gateway?.setHost(null);
137
+ e.gateway?.close();
138
+ this.deps.log("startup failed", e.id);
139
+ throw new Error("public link unavailable");
140
+ }
141
+ }
142
+ close(reason, expectedId) {
143
+ const e = this.entry;
144
+ if (!e || (expectedId && expectedId !== e.id))
145
+ return Promise.resolve({ confirmed: true });
146
+ if (e.closing)
147
+ return e.closing;
148
+ // Every access fence is synchronous, before the first await and before child cleanup.
149
+ e.phase = "closing";
150
+ e.abort.abort();
151
+ if (e.timer)
152
+ clearTimeout(e.timer);
153
+ e.unsubscribe?.();
154
+ this.deps.revoke(e.id);
155
+ e.gateway?.setHost(null);
156
+ e.gateway?.close();
157
+ e.closing = (async () => {
158
+ await e.starting.catch(() => { });
159
+ e.gateway?.close(); // listen may have finished after the first close.
160
+ const result = await e.reservation.stop(reason);
161
+ if (this.entry === e) {
162
+ if (result.confirmed)
163
+ this.entry = null;
164
+ else {
165
+ e.phase = "cleanup_unconfirmed";
166
+ this.deps.onCleanupUnconfirmed?.(result, e.owner);
167
+ }
168
+ }
169
+ this.deps.log(result.confirmed ? "closed" : "cleanup unconfirmed", e.id);
170
+ return result;
171
+ })();
172
+ return e.closing;
173
+ }
174
+ }
175
+ //# sourceMappingURL=public-web-link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-web-link.js","sourceRoot":"","sources":["../src/public-web-link.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { type SettingsExecution } from "./settings-transaction.js";
1
2
  /**
2
3
  * The Settings setup wizard's server side.
3
4
  *
@@ -145,14 +146,20 @@ export declare function detectWizardBackends(): string[];
145
146
  * asserts this module's import graph reaches neither fleet-manager, daemon nor
146
147
  * instance-lifecycle, so the host cannot grow a path to them by accident.
147
148
  */
149
+ /** One pure draft shared by the writer and the authoritative confirmation diff. */
150
+ export declare function draftQuickstart(cfg: FleetConfig, body: WizardPlanInput, plan: WizardPlan): FleetConfig;
148
151
  export interface QuickstartApiContext {
152
+ readonly webToken?: string | null;
153
+ readonly webSessions?: import("./web-session.js").WebSessionStore | null;
149
154
  fleetConfig: FleetConfig | null;
150
155
  dataDir: string;
151
156
  logger: {
152
157
  info(obj: unknown, msg?: string): void;
153
158
  warn(obj: unknown, msg?: string): void;
154
159
  };
155
- saveFleetConfig(): void;
160
+ /** SetupHost returns an undo for its exact persisted write; ordinary fleet writers keep their existing contract. */
161
+ saveFleetConfig(): void | (() => boolean);
162
+ settingsCommitted?(execution?: SettingsExecution): void;
156
163
  /** Only a running fleet can answer this; without one, nothing is polling. */
157
164
  isBotTokenInUse?(token: string): boolean;
158
165
  }
@@ -1,3 +1,7 @@
1
+ import { settingsRequestExecution, settingsWrite } from "./settings-request-capability.js";
2
+ import { SettingsExecutionError, settingsFileResource, trySettingsLease } from "./settings-transaction.js";
3
+ import { readBoundedWebBody } from "./web-body.js";
4
+ import { permitWebContinuation } from "./web-continuation.js";
1
5
  import { join } from "node:path";
2
6
  import { SecretStore } from "./secret-store.js";
3
7
  import { KNOWN_BACKENDS, validateFleetConfig } from "./config-validator.js";
@@ -145,27 +149,41 @@ export async function runProviderProbe(request, opts = {}) {
145
149
  export function detectWizardBackends() {
146
150
  return detectInstalledBackends(WIZARD_BACKENDS);
147
151
  }
152
+ // ── HTTP routes ─────────────────────────────────────────────────────────────
153
+ /**
154
+ * What the wizard's routes need from their host.
155
+ *
156
+ * Five fields, and none of them is a fleet: this is the whole "ConfigVerbs +
157
+ * ProviderProbeVerbs" surface. A running fleet supplies it from FleetManager; a
158
+ * pre-fleet setup host supplies it from a file. `tests/prefleet-host.test.ts`
159
+ * asserts this module's import graph reaches neither fleet-manager, daemon nor
160
+ * instance-lifecycle, so the host cannot grow a path to them by accident.
161
+ */
162
+ /** One pure draft shared by the writer and the authoritative confirmation diff. */
163
+ export function draftQuickstart(cfg, body, plan) {
164
+ const draft = structuredClone(cfg), summary = wizardChannels(cfg);
165
+ const channels = draft.channels ?? (draft.channel ? [draft.channel] : []);
166
+ const existingIndex = channels.findIndex(channel => channel.bot_token_env === body.token_env);
167
+ const entry = { id: nextChannelId(body.platform, body.token_env, summary), ...plan.channel };
168
+ if (existingIndex >= 0)
169
+ channels[existingIndex] = { ...channels[existingIndex], ...entry };
170
+ else
171
+ channels.push(entry);
172
+ draft.channels = channels;
173
+ delete draft.channel;
174
+ draft.instances = { ...draft.instances, [body.instance_name]: {
175
+ ...(Object.hasOwn(draft.instances, body.instance_name) ? draft.instances[body.instance_name] : {}),
176
+ working_directory: body.working_directory, backend: body.backend,
177
+ } };
178
+ return draft;
179
+ }
148
180
  function json(res, status, data) {
149
181
  res.setHeader("Content-Type", "application/json");
150
182
  res.writeHead(status);
151
183
  res.end(JSON.stringify(data));
152
184
  }
153
185
  function readBody(req, maxBytes) {
154
- return new Promise((resolve, reject) => {
155
- const chunks = [];
156
- let size = 0;
157
- req.on("data", (chunk) => {
158
- size += chunk.length;
159
- if (size > maxBytes) {
160
- reject(new Error("body too large"));
161
- req.destroy();
162
- return;
163
- }
164
- chunks.push(chunk);
165
- });
166
- req.on("end", () => resolve(Buffer.concat(chunks)));
167
- req.on("error", reject);
168
- });
186
+ return readBoundedWebBody(req, maxBytes);
169
187
  }
170
188
  /** The channels as the wizard sees them, from either config shape. */
171
189
  export function wizardChannels(cfg) {
@@ -216,6 +234,8 @@ export function handleQuickstartRequest(req, res, url, ctx) {
216
234
  }
217
235
  if (method === "POST" && path === "/api/settings/quickstart/probe") {
218
236
  readBody(req, 16 * 1024).then(async (buf) => {
237
+ if (!permitWebContinuation(req, res, ctx))
238
+ return;
219
239
  let body;
220
240
  try {
221
241
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -245,6 +265,8 @@ export function handleQuickstartRequest(req, res, url, ctx) {
245
265
  return true;
246
266
  }
247
267
  readBody(req, 16 * 1024).then(buf => {
268
+ if (!permitWebContinuation(req, res, ctx))
269
+ return;
248
270
  let body;
249
271
  try {
250
272
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -269,6 +291,8 @@ export function handleQuickstartRequest(req, res, url, ctx) {
269
291
  return true;
270
292
  }
271
293
  readBody(req, 16 * 1024).then(buf => {
294
+ if (!permitWebContinuation(req, res, ctx))
295
+ return;
272
296
  let body;
273
297
  try {
274
298
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -291,58 +315,72 @@ export function handleQuickstartRequest(req, res, url, ctx) {
291
315
  // nothing in memory moves until the whole result is known to be valid —
292
316
  // the previous order wrote the token first and left the running config
293
317
  // rewritten when validation then failed.
294
- const draft = structuredClone(cfg);
295
- const channels = (draft.channels ?? (draft.channel ? [draft.channel] : []));
296
- const existingIndex = channels.findIndex(channel => channel.bot_token_env === body.token_env);
297
- const entry = { id: nextChannelId(body.platform, body.token_env, summary), ...plan.channel };
298
- if (existingIndex >= 0)
299
- channels[existingIndex] = { ...channels[existingIndex], ...entry };
300
- else
301
- channels.push(entry);
302
- draft.channels = channels;
303
- delete draft.channel;
304
- draft.instances[body.instance_name] = {
305
- ...(draft.instances[body.instance_name] ?? {}),
306
- working_directory: body.working_directory,
307
- backend: body.backend,
308
- };
318
+ const draft = draftQuickstart(cfg, body, plan);
309
319
  const validation = validateFleetConfig(draft);
310
320
  if (!validation.valid) {
311
321
  return json(res, 400, { error: validation.errors.map(e => `${e.path}: ${e.message}`).join("; ") });
312
322
  }
313
- const secret = writeQuickstartSecret(ctx.dataDir, body.token_env, body.token);
314
- if (!secret.ok) {
315
- return json(res, 500, { error: "secret could not be stored securely" });
316
- }
317
- const before = {
318
- channels: cfg.channels,
319
- channel: cfg.channel,
320
- instance: cfg.instances[body.instance_name],
321
- hadInstance: Object.prototype.hasOwnProperty.call(cfg.instances, body.instance_name),
322
- };
323
- cfg.channels = draft.channels;
324
- delete cfg.channel;
325
- cfg.instances[body.instance_name] = draft.instances[body.instance_name];
323
+ const execution = settingsRequestExecution(req), envPath = join(ctx.dataDir, ".env");
324
+ const lease = trySettingsLease([settingsFileResource(envPath)], execution?.owner);
325
+ if (!lease)
326
+ return json(res, 409, { error: "another secret operation is still running" });
327
+ let stored = false, secretBefore;
328
+ const before = { channels: cfg.channels, channel: cfg.channel, instance: cfg.instances[body.instance_name],
329
+ hadInstance: Object.hasOwn(cfg.instances, body.instance_name) };
330
+ let store;
331
+ const rollback = {};
326
332
  try {
327
- ctx.saveFleetConfig();
333
+ execution?.assert();
334
+ store = new SecretStore(envPath, new Set([body.token_env]), { owner: lease.owner });
335
+ settingsWrite(req, () => {
336
+ secretBefore = store.write(body.token_env, body.token);
337
+ stored = true;
338
+ cfg.channels = draft.channels;
339
+ delete cfg.channel;
340
+ Object.defineProperty(cfg.instances, body.instance_name, { value: draft.instances[body.instance_name], enumerable: true, writable: true, configurable: true });
341
+ const undo = ctx.saveFleetConfig();
342
+ if (typeof undo === "function")
343
+ rollback.config = undo;
344
+ ctx.settingsCommitted?.(execution);
345
+ });
328
346
  }
329
347
  catch (err) {
330
- // The file is the authority. If it did not take the change, the running
331
- // config must not keep it either.
332
348
  cfg.channels = before.channels;
333
349
  if (before.channel !== undefined)
334
350
  cfg.channel = before.channel;
351
+ else
352
+ delete cfg.channel;
335
353
  if (before.hadInstance)
336
354
  cfg.instances[body.instance_name] = before.instance;
337
355
  else
338
356
  delete cfg.instances[body.instance_name];
339
- return json(res, 500, { error: err.message });
357
+ let cleanupFailed = false;
358
+ try {
359
+ if (rollback.config && !rollback.config())
360
+ cleanupFailed = true;
361
+ }
362
+ catch {
363
+ cleanupFailed = true;
364
+ }
365
+ try {
366
+ if (stored && secretBefore)
367
+ store.restoreIfCurrent(secretBefore);
368
+ }
369
+ catch {
370
+ cleanupFailed = true;
371
+ }
372
+ if (cleanupFailed)
373
+ return json(res, 500, { error: "setup cleanup failed; inspect configuration on the host" });
374
+ return json(res, err instanceof SettingsExecutionError ? 409 : 500, { error: "setup was not committed" });
375
+ }
376
+ finally {
377
+ lease.release();
340
378
  }
341
379
  ctx.logger.info({ instance: body.instance_name, platform: body.platform }, "settings: quickstart committed");
342
380
  // No apply here: the page starts the ordinary job so the wizard's last
343
381
  // step has the same progress, deadline and restart recovery as everything
344
382
  // else the panel applies.
345
- json(res, 200, { ok: true, plan, secret_mode_ok: secret.ok, warnings: plan.warnings });
383
+ json(res, 200, { ok: true, plan, secret_mode_ok: true, warnings: plan.warnings });
346
384
  }).catch(() => json(res, 400, { error: "bad request" }));
347
385
  return true;
348
386
  }