@songsid/agend 2.1.12 → 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
@@ -21,10 +21,9 @@
21
21
  *
22
22
  * See `docs/design/setup-host-tunnel.zh-TW.md` §3.8 and envelope rule 28.
23
23
  */
24
- import { createHash, randomBytes, randomInt, timingSafeEqual } from "node:crypto";
25
- /** RFC 4648 base32, minus nothing: the digits 0/1 are absent from it already. */
26
- const CODE_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
27
- export const SETUP_CODE_LENGTH = 8;
24
+ import { createHash, randomBytes } from "node:crypto";
25
+ import { constantTimeMatches, formatOneTimeCode, generateOneTimeCode, normalizeOneTimeCode, ONE_TIME_CODE_LENGTH, } from "./auth/one-time-code.js";
26
+ export const SETUP_CODE_LENGTH = ONE_TIME_CODE_LENGTH;
28
27
  /**
29
28
  * Five, not three.
30
29
  *
@@ -34,40 +33,12 @@ export const SETUP_CODE_LENGTH = 8;
34
33
  */
35
34
  export const MAX_SETUP_ATTEMPTS = 5;
36
35
  export const SETUP_COOKIE_NAME = "agend_setup";
37
- export function generateSetupCode() {
38
- let code = "";
39
- for (let i = 0; i < SETUP_CODE_LENGTH; i++)
40
- code += CODE_ALPHABET[randomInt(CODE_ALPHABET.length)];
41
- return code;
42
- }
43
- /** `ABCD-EFGH` — a shape that survives being read aloud and retyped. */
44
- export function formatSetupCode(code) {
45
- return `${code.slice(0, 4)}-${code.slice(4)}`;
46
- }
47
- /**
48
- * What the person typed, as the code it was meant to be.
49
- *
50
- * Case and the dash are presentation. Rejecting `abcd efgh` would be rejecting
51
- * a correct answer, and the person retyping it has no idea which detail was
52
- * wrong — so they spend attempts on formatting.
53
- */
54
- export function normalizeSetupCode(input) {
55
- return input.replace(/[^0-9A-Za-z]/g, "").toUpperCase();
56
- }
57
- /**
58
- * Fixed-length constant-time compare.
59
- *
60
- * Both sides are padded to the same length first: `timingSafeEqual` throws on a
61
- * mismatch, and returning early on one leaks the length through timing.
62
- */
63
- export function constantTimeMatches(provided, expected) {
64
- const width = Math.max(provided.length, expected.length, 1);
65
- const a = Buffer.alloc(width);
66
- const b = Buffer.alloc(width);
67
- a.write(provided, "utf8");
68
- b.write(expected, "utf8");
69
- return timingSafeEqual(a, b) && provided.length === expected.length;
70
- }
36
+ // The code primitives live in `auth/one-time-code.ts`; these names stay because
37
+ // the setup page and its tests already speak them.
38
+ export const generateSetupCode = generateOneTimeCode;
39
+ export const formatSetupCode = formatOneTimeCode;
40
+ export const normalizeSetupCode = normalizeOneTimeCode;
41
+ export { constantTimeMatches };
71
42
  /** The cookie carries a hash of the secret, never the secret itself. */
72
43
  export function setupCookieValue(secret) {
73
44
  return createHash("sha256").update(`agend-setup-session-v1:${secret}`).digest("hex");
@@ -97,6 +68,9 @@ export class SetupCredentials {
97
68
  sid;
98
69
  code;
99
70
  secret = null;
71
+ sessionOwner = null;
72
+ get sessionIdentity() { return this.lockedOut ? null : this.sessionOwner; }
73
+ isSessionCurrent(owner) { return !this.lockedOut && !!this.secret && this.sessionOwner === owner; }
100
74
  failures = 0;
101
75
  constructor(opts = {}) {
102
76
  this.sid = opts.sid ?? randomBytes(16).toString("hex");
@@ -114,6 +88,7 @@ export class SetupCredentials {
114
88
  revoke() {
115
89
  this.revoked = true;
116
90
  this.secret = null;
91
+ this.sessionOwner = null;
117
92
  }
118
93
  get revokedNow() { return this.revoked; }
119
94
  get lockedOut() { return this.revoked || this.failures >= MAX_SETUP_ATTEMPTS; }
@@ -153,6 +128,7 @@ export class SetupCredentials {
153
128
  if (this.secret)
154
129
  return { kind: "rejected", attemptsLeft: this.attemptsLeft };
155
130
  this.secret = randomBytes(32).toString("hex");
131
+ this.sessionOwner = { id: randomBytes(16).toString("hex") };
156
132
  return { kind: "ok", secret: this.secret };
157
133
  }
158
134
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"setup-auth.js","sourceRoot":"","sources":["../src/setup-auth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"setup-auth.js","sourceRoot":"","sources":["../src/setup-auth.ts"],"names":[],"mappings":"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"}
@@ -15,4 +15,4 @@
15
15
  * prove they also have the code they learn nothing from it.
16
16
  */
17
17
  export declare const SETUP_CODE_PAGE_HTML = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<!-- Proof that a request reached THIS listener and not merely something that\n answers 200. The tunnel's readiness probe looks for exactly this. It is the\n sid, which is already in the URL, so it discloses nothing new. -->\n<meta name=\"agend-setup\" content=\"__AGEND_SETUP_MARKER__\">\n<title>Set up AgEnD</title>\n<style>\n body { font-family: Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif; margin: 0; background: #ffffff; color: #111827; }\n main { max-width: 420px; margin: 0 auto; padding: 64px 20px; }\n h1 { font-size: 20px; margin: 0 0 8px; }\n p { color: #6b7280; font-size: 13px; margin: 0 0 20px; }\n input { width: 100%; padding: 12px; border: 1px solid #e5e7eb; border-radius: 6px; font-size: 20px; letter-spacing: 3px; text-align: center; text-transform: uppercase; box-sizing: border-box; }\n button { width: 100%; margin-top: 12px; padding: 12px; border: 1px solid #2563eb; border-radius: 6px; background: #2563eb; color: #fff; font-size: 15px; cursor: pointer; }\n button:disabled { opacity: .5; cursor: default; }\n .err { color: #dc2626; font-size: 13px; margin-top: 12px; min-height: 18px; }\n</style>\n</head>\n<body>\n<main>\n <h1>Set up AgEnD</h1>\n <p>Enter the setup code shown in the terminal where you ran <code>agend setup</code>.</p>\n <form id=\"f\">\n <input id=\"code\" autocomplete=\"off\" autocapitalize=\"characters\" spellcheck=\"false\" placeholder=\"ABCD-EFGH\" maxlength=\"12\" autofocus>\n <button id=\"go\" type=\"submit\">Continue</button>\n </form>\n <div class=\"err\" id=\"msg\"></div>\n</main>\n<script>\n(() => {\n \"use strict\";\n const msg = document.getElementById(\"msg\");\n const go = document.getElementById(\"go\");\n document.getElementById(\"f\").addEventListener(\"submit\", async (event) => {\n event.preventDefault();\n go.disabled = true;\n msg.textContent = \"\";\n let res;\n try {\n res = await fetch(\"open\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ code: document.getElementById(\"code\").value }),\n });\n } catch {\n msg.textContent = \"Could not reach the setup page.\";\n go.disabled = false;\n return;\n }\n if (res.ok) { location.reload(); return; }\n let body = null; try { body = await res.json(); } catch {}\n msg.textContent = (body && body.error) || \"That code was not accepted.\";\n // A spent budget ends the page; there is nothing useful left to type into.\n if (res.status !== 410) go.disabled = false;\n });\n})();\n</script>\n</body>\n</html>\n";
18
- export declare const SETUP_FORM_HTML = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Set up AgEnD</title>\n<style>\n * { box-sizing: border-box; }\n body { font-family: Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif; margin: 0; background: #ffffff; color: #111827; }\n main { max-width: 640px; margin: 0 auto; padding: 32px 20px 64px; }\n h1 { font-size: 20px; margin: 0 0 4px; }\n .sub { color: #6b7280; font-size: 13px; }\n .step { border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; margin-top: 16px; }\n .step h2 { font-size: 14px; margin: 0 0 12px; }\n label { display: block; font-size: 13px; font-weight: 600; margin: 12px 0 4px; }\n input, select { width: 100%; padding: 8px 10px; border: 1px solid #e5e7eb; border-radius: 6px; font-size: 14px; }\n button { padding: 8px 14px; border: 1px solid #e5e7eb; border-radius: 6px; background: #fff; font-size: 14px; cursor: pointer; }\n button.primary { background: #2563eb; color: #fff; border-color: #2563eb; }\n button:disabled { opacity: .5; cursor: default; }\n .row { display: flex; gap: 8px; align-items: center; margin-top: 12px; flex-wrap: wrap; }\n .msg { font-size: 13px; }\n .ok { color: #059669; } .err { color: #dc2626; }\n pre { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 12px; font-size: 12px; overflow: auto; }\n</style>\n</head>\n<body>\n<main>\n <h1>Set up AgEnD</h1>\n <div class=\"sub\" id=\"intro\">This page is open only while setup is running. It closes itself when you finish, or after 15 minutes.</div>\n\n <div class=\"step\">\n <h2>1 \u00B7 Agent</h2>\n <label for=\"backend\">Backend</label>\n <select id=\"backend\"></select>\n <div class=\"sub\" id=\"backendHint\"></div>\n <label for=\"dir\">Working directory (absolute path)</label>\n <input id=\"dir\" placeholder=\"/home/you/projects/app\">\n <label for=\"name\">First agent name</label>\n <input id=\"name\" value=\"agent-1\">\n </div>\n\n <div class=\"step\">\n <h2>2 \u00B7 Platform</h2>\n <div class=\"row\">\n <button id=\"pickTelegram\" class=\"primary\">Telegram</button>\n <button id=\"pickDiscord\">Discord</button>\n </div>\n <div class=\"sub\" id=\"platformHint\">Create a bot with @BotFather, then add it to a group.</div>\n </div>\n\n <div class=\"step\">\n <h2>3 \u00B7 Credentials</h2>\n <label for=\"token\">Bot token</label>\n <input id=\"token\" type=\"password\" placeholder=\"123456:ABC-DEF\u2026\">\n <div class=\"sub\">Written to ~/.agend/.env and never shown again.</div>\n <label for=\"tokenEnv\">Environment variable</label>\n <input id=\"tokenEnv\" value=\"AGEND_BOT_TOKEN\">\n <div class=\"row\"><button id=\"verify\">Verify</button><span class=\"msg\" id=\"verifyMsg\"></span></div>\n <div id=\"platformFields\"></div>\n </div>\n\n <div class=\"step\">\n <h2>4 \u00B7 Review and start</h2>\n <div class=\"row\"><button id=\"preview\">Show what will be written</button></div>\n <pre id=\"planOut\" hidden></pre>\n <div class=\"row\"><button id=\"finish\" class=\"primary\" disabled>Create and start AgEnD</button><span class=\"msg\" id=\"finishMsg\"></span></div>\n </div>\n</main>\n<script>\n(() => {\n \"use strict\";\n const $ = (id) => document.getElementById(id);\n const state = { platform: \"telegram\", identity: null, guilds: [], offset: 0, plan: null };\n // Relative to the page's own directory, which is /s/<sid>/. An absolute path\n // would leave that namespace and 404 \u2014 the sid is where this page lives, not\n // a prefix bolted on to it.\n const api = async (path, opts) => {\n const res = await fetch(path, Object.assign({ headers: { \"Content-Type\": \"application/json\" } }, opts || {}));\n let body = null; try { body = await res.json(); } catch {}\n return { ok: res.ok, status: res.status, body };\n };\n\n async function loadEnvironment() {\n const res = await api(\"api/settings/quickstart/environment\");\n const backends = res.body?.backends || [];\n $(\"backend\").innerHTML = \"\";\n for (const name of backends.length ? backends : [\"claude-code\"]) {\n const opt = document.createElement(\"option\"); opt.value = name; opt.textContent = name; $(\"backend\").append(opt);\n }\n $(\"backendHint\").textContent = backends.length\n ? \"Found on this host: \" + backends.join(\", \")\n : \"No backend CLI found on this host \u2014 install one first.\";\n }\n\n function setPlatform(platform) {\n state.platform = platform;\n state.identity = null; $(\"verifyMsg\").textContent = \"\";\n $(\"pickTelegram\").className = platform === \"telegram\" ? \"primary\" : \"\";\n $(\"pickDiscord\").className = platform === \"discord\" ? \"primary\" : \"\";\n $(\"platformHint\").textContent = platform === \"telegram\"\n ? \"Create a bot with @BotFather, then add it to a group.\"\n : \"Create an application in the Discord developer portal and invite the bot to your server.\";\n renderPlatformFields();\n }\n\n // Only the ids that platform has \u2014 the same question the CLI asks.\n function renderPlatformFields() {\n const host = $(\"platformFields\"); host.innerHTML = \"\";\n const field = (id, labelText, hint) => {\n const label = document.createElement(\"label\"); label.textContent = labelText; label.htmlFor = id;\n const input = document.createElement(\"input\"); input.id = id;\n host.append(label, input);\n if (hint) { const h = document.createElement(\"div\"); h.className = \"sub\"; h.textContent = hint; host.append(h); }\n return input;\n };\n if (state.platform === \"discord\") {\n const guild = document.createElement(\"select\"); guild.id = \"guild\";\n const label = document.createElement(\"label\"); label.textContent = \"Server (guild)\"; label.htmlFor = \"guild\";\n host.append(label, guild);\n for (const g of state.guilds) { const opt = document.createElement(\"option\"); opt.value = g.id; opt.textContent = g.name + \" (\" + g.id + \")\"; guild.append(opt); }\n field(\"general\", \"General channel id\");\n field(\"admin\", \"Your Discord user id\", \"Becomes the first allowed user.\");\n } else {\n field(\"group\", \"Group id\", \"Or post any message in the group and press Detect.\");\n const row = document.createElement(\"div\"); row.className = \"row\";\n const detect = document.createElement(\"button\"); detect.textContent = \"Detect from a message\";\n detect.onclick = detectGroup; row.append(detect);\n const msg = document.createElement(\"span\"); msg.className = \"msg\"; msg.id = \"detectMsg\"; row.append(msg);\n host.append(row);\n field(\"admin\", \"Your Telegram user id\", \"Becomes the first allowed user.\");\n }\n }\n\n async function detectGroup() {\n $(\"detectMsg\").textContent = \"Waiting for a message in the group\u2026\";\n const res = await api(\"api/settings/quickstart/probe\", { method: \"POST\", body: JSON.stringify({ action: \"await-telegram-start\", token: $(\"token\").value.trim(), offset: state.offset }) });\n if (!res.ok) { $(\"detectMsg\").textContent = res.body?.error || \"Failed.\"; return; }\n state.offset = res.body.offset;\n if (res.body.found) {\n $(\"group\").value = String(res.body.found.groupId);\n $(\"admin\").value = String(res.body.found.userId);\n $(\"detectMsg\").textContent = \"Found.\";\n } else $(\"detectMsg\").textContent = \"No group message yet \u2014 post one and press Detect again.\";\n }\n\n function input() {\n return {\n platform: state.platform,\n token_env: $(\"tokenEnv\").value.trim(),\n backend: $(\"backend\").value,\n working_directory: $(\"dir\").value.trim(),\n instance_name: $(\"name\").value.trim(),\n group_id: $(\"group\") ? $(\"group\").value.trim() || undefined : undefined,\n guild_id: $(\"guild\") ? $(\"guild\").value || undefined : undefined,\n general_channel_id: $(\"general\") ? $(\"general\").value.trim() || undefined : undefined,\n admin_user_id: $(\"admin\") ? $(\"admin\").value.trim() || undefined : undefined,\n };\n }\n\n $(\"verify\").onclick = async () => {\n $(\"verifyMsg\").textContent = \"Asking the provider\u2026\";\n const token = $(\"token\").value.trim();\n const res = await api(\"api/settings/quickstart/probe\", { method: \"POST\", body: JSON.stringify({ action: \"verify\", platform: state.platform, token }) });\n state.identity = res.body?.identity || { valid: false };\n $(\"verifyMsg\").className = \"msg \" + (state.identity.valid ? \"ok\" : \"err\");\n $(\"verifyMsg\").textContent = state.identity.valid ? \"\u2713 \" + (state.identity.username || \"verified\") : \"\u2717 \" + (state.identity.reason || \"rejected\");\n if (state.identity.valid && state.platform === \"discord\") {\n const guilds = await api(\"api/settings/quickstart/probe\", { method: \"POST\", body: JSON.stringify({ action: \"guilds\", token }) });\n state.guilds = guilds.body?.guilds || [];\n renderPlatformFields();\n }\n };\n\n $(\"preview\").onclick = async () => {\n const res = await api(\"api/settings/quickstart/plan\", { method: \"POST\", body: JSON.stringify(input()) });\n if (!res.ok) { $(\"planOut\").hidden = false; $(\"planOut\").textContent = res.body?.error || \"Failed.\"; return; }\n state.plan = res.body;\n $(\"planOut\").hidden = false;\n $(\"planOut\").textContent = JSON.stringify(res.body, null, 2);\n $(\"finish\").disabled = !state.identity?.valid;\n };\n\n $(\"finish\").onclick = async () => {\n $(\"finish\").disabled = true;\n $(\"finishMsg\").textContent = \"Writing configuration\u2026\";\n const commit = await api(\"api/settings/quickstart/commit\", { method: \"POST\", body: JSON.stringify(Object.assign({}, input(), { token: $(\"token\").value.trim() })) });\n if (!commit.ok) { $(\"finishMsg\").className = \"msg err\"; $(\"finishMsg\").textContent = commit.body?.error || \"Failed.\"; $(\"finish\").disabled = false; return; }\n const finished = await api(\"setup/finish\", { method: \"POST\" });\n $(\"finishMsg\").className = \"msg\";\n if (finished.body && finished.body.watch === false) {\n // Behind a tunnel this page and its hostname are both about to stop\n // existing, so there is nothing here to watch. And the dashboard is not\n // the thing to promise: it binds loopback, so a link to it is one this\n // phone cannot open. The channel is what was just set up, and it is\n // where the agent will be.\n $(\"finishMsg\").className = \"msg ok\";\n $(\"finishMsg\").textContent = \"AgEnD is starting. Talk to it in the channel you just set up \u2014 this page is done.\";\n return;\n }\n $(\"finishMsg\").textContent = \"Starting AgEnD\u2026 this page will stop responding while the port changes hands.\";\n waitForFleet();\n };\n\n // The host is gone; the fleet is binding the same port. Connection refused is\n // the expected state in between, so it is not an error until the cap.\n function waitForFleet() {\n const deadline = Date.now() + 60_000;\n const tick = async () => {\n try {\n const res = await fetch(\"/health\", { cache: \"no-store\" });\n if (res.status) {\n $(\"finishMsg\").className = \"msg ok\";\n $(\"finishMsg\").textContent = \"AgEnD is up. Talk to it in the channel you just set up.\";\n return;\n }\n } catch { /* not listening yet */ }\n if (Date.now() > deadline) {\n $(\"finishMsg\").className = \"msg err\";\n $(\"finishMsg\").textContent = \"AgEnD has not come up within 60 seconds. Run `agend start` on the machine itself and check fleet.log \u2014 this page cannot tell you any more.\";\n return;\n }\n setTimeout(tick, 1000);\n };\n setTimeout(tick, 1500);\n }\n\n $(\"pickTelegram\").onclick = () => setPlatform(\"telegram\");\n $(\"pickDiscord\").onclick = () => setPlatform(\"discord\");\n loadEnvironment();\n setPlatform(\"telegram\");\n})();\n</script>\n</body>\n</html>\n";
18
+ export declare const SETUP_FORM_HTML = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Set up AgEnD</title>\n<style>\n * { box-sizing: border-box; }\n body { font-family: Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif; margin: 0; background: #ffffff; color: #111827; }\n main { max-width: 640px; margin: 0 auto; padding: 32px 20px 64px; }\n h1 { font-size: 20px; margin: 0 0 4px; }\n .sub { color: #6b7280; font-size: 13px; }\n .step { border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; margin-top: 16px; }\n .step h2 { font-size: 14px; margin: 0 0 12px; }\n label { display: block; font-size: 13px; font-weight: 600; margin: 12px 0 4px; }\n input, select { width: 100%; padding: 8px 10px; border: 1px solid #e5e7eb; border-radius: 6px; font-size: 14px; }\n button { padding: 8px 14px; border: 1px solid #e5e7eb; border-radius: 6px; background: #fff; font-size: 14px; cursor: pointer; }\n button.primary { background: #2563eb; color: #fff; border-color: #2563eb; }\n button:disabled { opacity: .5; cursor: default; }\n .row { display: flex; gap: 8px; align-items: center; margin-top: 12px; flex-wrap: wrap; }\n .msg { font-size: 13px; }\n .ok { color: #059669; } .err { color: #dc2626; }\n pre { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 12px; font-size: 12px; overflow: auto; }\n</style>\n</head>\n<body>\n<main>\n <h1>Set up AgEnD</h1>\n <div class=\"sub\" id=\"intro\">This page is open only while setup is running. It closes itself when you finish, or after 15 minutes.</div>\n\n <div class=\"step\">\n <h2>1 \u00B7 Agent</h2>\n <label for=\"backend\">Backend</label>\n <select id=\"backend\"></select>\n <div class=\"sub\" id=\"backendHint\"></div>\n <label for=\"dir\">Working directory (absolute path)</label>\n <input id=\"dir\" placeholder=\"/home/you/projects/app\">\n <label for=\"name\">First agent name</label>\n <input id=\"name\" value=\"agent-1\">\n </div>\n\n <div class=\"step\">\n <h2>2 \u00B7 Platform</h2>\n <div class=\"row\">\n <button id=\"pickTelegram\" class=\"primary\">Telegram</button>\n <button id=\"pickDiscord\">Discord</button>\n </div>\n <div class=\"sub\" id=\"platformHint\">Create a bot with @BotFather, then add it to a group.</div>\n </div>\n\n <div class=\"step\">\n <h2>3 \u00B7 Credentials</h2>\n <label for=\"token\">Bot token</label>\n <input id=\"token\" type=\"password\" placeholder=\"123456:ABC-DEF\u2026\">\n <div class=\"sub\">Written to ~/.agend/.env and never shown again.</div>\n <label for=\"tokenEnv\">Environment variable</label>\n <input id=\"tokenEnv\" value=\"AGEND_BOT_TOKEN\">\n <div class=\"row\"><button id=\"verify\">Verify</button><span class=\"msg\" id=\"verifyMsg\"></span></div>\n <div id=\"platformFields\"></div>\n </div>\n\n <div class=\"step\">\n <h2>4 \u00B7 Review and start</h2>\n <div class=\"row\"><button id=\"preview\">Show what will be written</button></div>\n <pre id=\"planOut\" hidden></pre>\n <div class=\"row\"><button id=\"finish\" class=\"primary\" disabled>Create and start AgEnD</button><span class=\"msg\" id=\"finishMsg\"></span></div>\n </div>\n</main>\n<script>\n(() => {\n \"use strict\";\n const $ = (id) => document.getElementById(id);\n const state = { platform: \"telegram\", identity: null, guilds: [], offset: 0, plan: null };\n // Relative to the page's own directory, which is /s/<sid>/. An absolute path\n // would leave that namespace and 404 \u2014 the sid is where this page lives, not\n // a prefix bolted on to it.\n const api = async (path, opts) => {\n const res = await fetch(path, Object.assign({ headers: { \"Content-Type\": \"application/json\" } }, opts || {}));\n let body = null; try { body = await res.json(); } catch {}\n return { ok: res.ok, status: res.status, body };\n };\n\n async function loadEnvironment() {\n const res = await api(\"api/settings/quickstart/environment\");\n if (formFrozen) return;\n const backends = res.body?.backends || [];\n $(\"backend\").innerHTML = \"\";\n for (const name of backends.length ? backends : [\"claude-code\"]) {\n const opt = document.createElement(\"option\"); opt.value = name; opt.textContent = name; $(\"backend\").append(opt);\n }\n $(\"backendHint\").textContent = backends.length\n ? \"Found on this host: \" + backends.join(\", \")\n : \"No backend CLI found on this host \u2014 install one first.\";\n }\n\n function setPlatform(platform) {\n if (formFrozen) return;\n state.platform = platform;\n state.identity = null; $(\"verifyMsg\").textContent = \"\";\n $(\"pickTelegram\").className = platform === \"telegram\" ? \"primary\" : \"\";\n $(\"pickDiscord\").className = platform === \"discord\" ? \"primary\" : \"\";\n $(\"platformHint\").textContent = platform === \"telegram\"\n ? \"Create a bot with @BotFather, then add it to a group.\"\n : \"Create an application in the Discord developer portal and invite the bot to your server.\";\n renderPlatformFields();\n }\n\n // Only the ids that platform has \u2014 the same question the CLI asks.\n function renderPlatformFields() {\n if (formFrozen) return;\n const host = $(\"platformFields\"); host.innerHTML = \"\";\n const field = (id, labelText, hint) => {\n const label = document.createElement(\"label\"); label.textContent = labelText; label.htmlFor = id;\n const input = document.createElement(\"input\"); input.id = id;\n host.append(label, input);\n if (hint) { const h = document.createElement(\"div\"); h.className = \"sub\"; h.textContent = hint; host.append(h); }\n return input;\n };\n if (state.platform === \"discord\") {\n const guild = document.createElement(\"select\"); guild.id = \"guild\";\n const label = document.createElement(\"label\"); label.textContent = \"Server (guild)\"; label.htmlFor = \"guild\";\n host.append(label, guild);\n for (const g of state.guilds) { const opt = document.createElement(\"option\"); opt.value = g.id; opt.textContent = g.name + \" (\" + g.id + \")\"; guild.append(opt); }\n field(\"general\", \"General channel id\");\n field(\"admin\", \"Your Discord user id\", \"Becomes the first allowed user.\");\n } else {\n field(\"group\", \"Group id\", \"Or post any message in the group and press Detect.\");\n const row = document.createElement(\"div\"); row.className = \"row\";\n const detect = document.createElement(\"button\"); detect.id = \"detect\"; detect.textContent = \"Detect from a message\";\n detect.onclick = detectGroup; row.append(detect);\n const msg = document.createElement(\"span\"); msg.className = \"msg\"; msg.id = \"detectMsg\"; row.append(msg);\n host.append(row);\n field(\"admin\", \"Your Telegram user id\", \"Becomes the first allowed user.\");\n }\n }\n\n async function detectGroup() {\n if (formFrozen) return;\n $(\"detectMsg\").textContent = \"Waiting for a message in the group\u2026\";\n const res = await api(\"api/settings/quickstart/probe\", { method: \"POST\", body: JSON.stringify({ action: \"await-telegram-start\", token: $(\"token\").value.trim(), offset: state.offset }) });\n if (formFrozen) return;\n if (!res.ok) { $(\"detectMsg\").textContent = res.body?.error || \"Failed.\"; return; }\n state.offset = res.body.offset;\n if (res.body.found) {\n $(\"group\").value = String(res.body.found.groupId);\n $(\"admin\").value = String(res.body.found.userId);\n $(\"detectMsg\").textContent = \"Found.\";\n } else $(\"detectMsg\").textContent = \"No group message yet \u2014 post one and press Detect again.\";\n }\n\n function input() {\n return {\n platform: state.platform,\n token_env: $(\"tokenEnv\").value.trim(),\n backend: $(\"backend\").value,\n working_directory: $(\"dir\").value.trim(),\n instance_name: $(\"name\").value.trim(),\n group_id: $(\"group\") ? $(\"group\").value.trim() || undefined : undefined,\n guild_id: $(\"guild\") ? $(\"guild\").value || undefined : undefined,\n general_channel_id: $(\"general\") ? $(\"general\").value.trim() || undefined : undefined,\n admin_user_id: $(\"admin\") ? $(\"admin\").value.trim() || undefined : undefined,\n };\n }\n\n $(\"verify\").onclick = async () => {\n if (formFrozen) return;\n $(\"verifyMsg\").textContent = \"Asking the provider\u2026\";\n const token = $(\"token\").value.trim();\n const res = await api(\"api/settings/quickstart/probe\", { method: \"POST\", body: JSON.stringify({ action: \"verify\", platform: state.platform, token }) });\n if (formFrozen) return;\n state.identity = res.body?.identity || { valid: false };\n $(\"verifyMsg\").className = \"msg \" + (state.identity.valid ? \"ok\" : \"err\");\n $(\"verifyMsg\").textContent = state.identity.valid ? \"\u2713 \" + (state.identity.username || \"verified\") : \"\u2717 \" + (state.identity.reason || \"rejected\");\n if (state.identity.valid && state.platform === \"discord\") {\n const guilds = await api(\"api/settings/quickstart/probe\", { method: \"POST\", body: JSON.stringify({ action: \"guilds\", token }) });\n if (formFrozen) return;\n state.guilds = guilds.body?.guilds || [];\n renderPlatformFields();\n }\n };\n\n $(\"preview\").onclick = async () => {\n if (formFrozen) return;\n const res = await api(\"api/settings/quickstart/plan\", { method: \"POST\", body: JSON.stringify(input()) });\n if (formFrozen) return;\n if (!res.ok) { $(\"planOut\").hidden = false; $(\"planOut\").textContent = res.body?.error || \"Failed.\"; return; }\n state.plan = res.body;\n $(\"planOut\").hidden = false;\n $(\"planOut\").textContent = JSON.stringify(res.body, null, 2);\n $(\"finish\").disabled = !state.identity?.valid;\n };\n\n let approvedSetup = false;\n let formFrozen = false;\n let submission = null;\n let pollGeneration = 0;\n const freezeFields = (frozen) => {\n formFrozen = frozen;\n for (const field of document.querySelectorAll(\"main input, main select, #verify, #preview, #pickTelegram, #pickDiscord, #detect\")) field.disabled = frozen;\n };\n const restoreEditing = (message) => {\n pollGeneration++; approvedSetup = false; submission = null; state.identity = null;\n freezeFields(false); $(\"finish\").disabled = true; $(\"finish\").textContent = \"Create and start AgEnD\";\n $(\"finishMsg\").className = \"msg err\"; $(\"finishMsg\").textContent = message;\n };\n for (const field of document.querySelectorAll(\"main input, main select\")) field.addEventListener(\"input\", () => {\n approvedSetup = false; submission = null; $(\"finish\").textContent = \"Create and start AgEnD\";\n });\n const watchPending = (pendingId) => {\n const generation = ++pollGeneration;\n freezeFields(true); $(\"finish\").disabled = true;\n $(\"finishMsg\").textContent = \"On the host, run: agend settings confirm \" + pendingId;\n const poll = async () => {\n if (generation !== pollGeneration) return;\n try {\n const result = await api(\"api/settings/pending/\" + pendingId);\n if (generation !== pollGeneration) return;\n if (!result.ok) { restoreEditing(\"Setup confirmation is no longer available. Review and enter the token again.\"); return; }\n const pending = result.body;\n if (pending.state === \"pending\" || pending.state === \"applying\") { setTimeout(poll, 1000); return; }\n if (pending.state !== \"applied\") { restoreEditing(pending.outcome?.message || \"Change was not applied. Review and enter the token again.\"); return; }\n approvedSetup = true; submission = null; $(\"finish\").disabled = false; $(\"finish\").textContent = \"Start AgEnD\";\n $(\"finishMsg\").textContent = \"Configuration confirmed. Select Start AgEnD to finish setup.\";\n } catch { if (generation === pollGeneration) setTimeout(poll, 1000); }\n };\n setTimeout(poll, 1000);\n };\n $(\"finish\").onclick = async () => {\n $(\"finish\").disabled = true;\n try {\n if (approvedSetup) {\n const finished = await api(\"setup/finish\", { method: \"POST\" });\n if (!finished.ok) { restoreEditing(finished.body?.error || \"Confirmation is no longer current.\"); return; }\n $(\"finishMsg\").textContent = \"AgEnD is starting. Continue in the channel you configured.\";\n if (finished.body?.watch !== false) waitForFleet();\n return;\n }\n // Preserve bytes and key on a lost HTTP response; a deliberate retry after\n // a terminal outcome receives a new key and requires secret reentry.\n submission ||= { key: crypto.randomUUID(), body: JSON.stringify(Object.assign({}, input(), { token: $(\"token\").value.trim() })) };\n freezeFields(true);\n $(\"finishMsg\").textContent = \"Requesting host confirmation\u2026\";\n const commit = await api(\"api/settings/quickstart/commit\", { method: \"POST\", headers: { \"Content-Type\": \"application/json\", \"Idempotency-Key\": submission.key }, body: submission.body });\n if (!commit.ok) { restoreEditing(commit.body?.error || \"Failed.\"); return; }\n if (commit.body?.result !== \"pending_confirmation\" || !commit.body.pending_change?.id) { restoreEditing(\"Expected a pending host confirmation.\"); return; }\n $(\"token\").value = \"\"; submission = null;\n watchPending(commit.body.pending_change.id);\n } catch { $(\"finishMsg\").textContent = \"Connection failed. Retry to find the same confirmation.\"; $(\"finish\").disabled = false; }\n };\n // Same-cookie reloads rediscover an unresolved consent request without a secret.\n api(\"api/settings/pending\").then(result => {\n const pending = result.ok && Array.isArray(result.body) && result.body.find(item => item.state === \"pending\" || item.state === \"applying\");\n if (pending) { watchPending(pending.id); return; }\n api(\"setup/status\").then(status => {\n if (!status.ok || !status.body?.finish_ready || submission || pollGeneration) return;\n approvedSetup = true; freezeFields(true); $(\"finish\").disabled = false; $(\"finish\").textContent = \"Start AgEnD\";\n $(\"finishMsg\").textContent = \"Configuration confirmed. Select Start AgEnD to finish setup.\";\n }).catch(() => {});\n }).catch(() => {});\n\n // The host is gone; the fleet is binding the same port. Connection refused is\n // the expected state in between, so it is not an error until the cap.\n function waitForFleet() {\n const deadline = Date.now() + 60_000;\n const tick = async () => {\n try {\n const res = await fetch(\"/health\", { cache: \"no-store\" });\n if (res.status) {\n $(\"finishMsg\").className = \"msg ok\";\n $(\"finishMsg\").textContent = \"AgEnD is up. Talk to it in the channel you just set up.\";\n return;\n }\n } catch { /* not listening yet */ }\n if (Date.now() > deadline) {\n $(\"finishMsg\").className = \"msg err\";\n $(\"finishMsg\").textContent = \"AgEnD has not come up within 60 seconds. Run `agend start` on the machine itself and check fleet.log \u2014 this page cannot tell you any more.\";\n return;\n }\n setTimeout(tick, 1000);\n };\n setTimeout(tick, 1500);\n }\n\n $(\"pickTelegram\").onclick = () => setPlatform(\"telegram\");\n $(\"pickDiscord\").onclick = () => setPlatform(\"discord\");\n loadEnvironment();\n setPlatform(\"telegram\");\n})();\n</script>\n</body>\n</html>\n";
@@ -161,6 +161,7 @@ export const SETUP_FORM_HTML = `<!DOCTYPE html>
161
161
 
162
162
  async function loadEnvironment() {
163
163
  const res = await api("api/settings/quickstart/environment");
164
+ if (formFrozen) return;
164
165
  const backends = res.body?.backends || [];
165
166
  $("backend").innerHTML = "";
166
167
  for (const name of backends.length ? backends : ["claude-code"]) {
@@ -172,6 +173,7 @@ export const SETUP_FORM_HTML = `<!DOCTYPE html>
172
173
  }
173
174
 
174
175
  function setPlatform(platform) {
176
+ if (formFrozen) return;
175
177
  state.platform = platform;
176
178
  state.identity = null; $("verifyMsg").textContent = "";
177
179
  $("pickTelegram").className = platform === "telegram" ? "primary" : "";
@@ -184,6 +186,7 @@ export const SETUP_FORM_HTML = `<!DOCTYPE html>
184
186
 
185
187
  // Only the ids that platform has — the same question the CLI asks.
186
188
  function renderPlatformFields() {
189
+ if (formFrozen) return;
187
190
  const host = $("platformFields"); host.innerHTML = "";
188
191
  const field = (id, labelText, hint) => {
189
192
  const label = document.createElement("label"); label.textContent = labelText; label.htmlFor = id;
@@ -202,7 +205,7 @@ export const SETUP_FORM_HTML = `<!DOCTYPE html>
202
205
  } else {
203
206
  field("group", "Group id", "Or post any message in the group and press Detect.");
204
207
  const row = document.createElement("div"); row.className = "row";
205
- const detect = document.createElement("button"); detect.textContent = "Detect from a message";
208
+ const detect = document.createElement("button"); detect.id = "detect"; detect.textContent = "Detect from a message";
206
209
  detect.onclick = detectGroup; row.append(detect);
207
210
  const msg = document.createElement("span"); msg.className = "msg"; msg.id = "detectMsg"; row.append(msg);
208
211
  host.append(row);
@@ -211,8 +214,10 @@ export const SETUP_FORM_HTML = `<!DOCTYPE html>
211
214
  }
212
215
 
213
216
  async function detectGroup() {
217
+ if (formFrozen) return;
214
218
  $("detectMsg").textContent = "Waiting for a message in the group…";
215
219
  const res = await api("api/settings/quickstart/probe", { method: "POST", body: JSON.stringify({ action: "await-telegram-start", token: $("token").value.trim(), offset: state.offset }) });
220
+ if (formFrozen) return;
216
221
  if (!res.ok) { $("detectMsg").textContent = res.body?.error || "Failed."; return; }
217
222
  state.offset = res.body.offset;
218
223
  if (res.body.found) {
@@ -237,21 +242,26 @@ export const SETUP_FORM_HTML = `<!DOCTYPE html>
237
242
  }
238
243
 
239
244
  $("verify").onclick = async () => {
245
+ if (formFrozen) return;
240
246
  $("verifyMsg").textContent = "Asking the provider…";
241
247
  const token = $("token").value.trim();
242
248
  const res = await api("api/settings/quickstart/probe", { method: "POST", body: JSON.stringify({ action: "verify", platform: state.platform, token }) });
249
+ if (formFrozen) return;
243
250
  state.identity = res.body?.identity || { valid: false };
244
251
  $("verifyMsg").className = "msg " + (state.identity.valid ? "ok" : "err");
245
252
  $("verifyMsg").textContent = state.identity.valid ? "✓ " + (state.identity.username || "verified") : "✗ " + (state.identity.reason || "rejected");
246
253
  if (state.identity.valid && state.platform === "discord") {
247
254
  const guilds = await api("api/settings/quickstart/probe", { method: "POST", body: JSON.stringify({ action: "guilds", token }) });
255
+ if (formFrozen) return;
248
256
  state.guilds = guilds.body?.guilds || [];
249
257
  renderPlatformFields();
250
258
  }
251
259
  };
252
260
 
253
261
  $("preview").onclick = async () => {
262
+ if (formFrozen) return;
254
263
  const res = await api("api/settings/quickstart/plan", { method: "POST", body: JSON.stringify(input()) });
264
+ if (formFrozen) return;
255
265
  if (!res.ok) { $("planOut").hidden = false; $("planOut").textContent = res.body?.error || "Failed."; return; }
256
266
  state.plan = res.body;
257
267
  $("planOut").hidden = false;
@@ -259,26 +269,73 @@ export const SETUP_FORM_HTML = `<!DOCTYPE html>
259
269
  $("finish").disabled = !state.identity?.valid;
260
270
  };
261
271
 
272
+ let approvedSetup = false;
273
+ let formFrozen = false;
274
+ let submission = null;
275
+ let pollGeneration = 0;
276
+ const freezeFields = (frozen) => {
277
+ formFrozen = frozen;
278
+ for (const field of document.querySelectorAll("main input, main select, #verify, #preview, #pickTelegram, #pickDiscord, #detect")) field.disabled = frozen;
279
+ };
280
+ const restoreEditing = (message) => {
281
+ pollGeneration++; approvedSetup = false; submission = null; state.identity = null;
282
+ freezeFields(false); $("finish").disabled = true; $("finish").textContent = "Create and start AgEnD";
283
+ $("finishMsg").className = "msg err"; $("finishMsg").textContent = message;
284
+ };
285
+ for (const field of document.querySelectorAll("main input, main select")) field.addEventListener("input", () => {
286
+ approvedSetup = false; submission = null; $("finish").textContent = "Create and start AgEnD";
287
+ });
288
+ const watchPending = (pendingId) => {
289
+ const generation = ++pollGeneration;
290
+ freezeFields(true); $("finish").disabled = true;
291
+ $("finishMsg").textContent = "On the host, run: agend settings confirm " + pendingId;
292
+ const poll = async () => {
293
+ if (generation !== pollGeneration) return;
294
+ try {
295
+ const result = await api("api/settings/pending/" + pendingId);
296
+ if (generation !== pollGeneration) return;
297
+ if (!result.ok) { restoreEditing("Setup confirmation is no longer available. Review and enter the token again."); return; }
298
+ const pending = result.body;
299
+ if (pending.state === "pending" || pending.state === "applying") { setTimeout(poll, 1000); return; }
300
+ if (pending.state !== "applied") { restoreEditing(pending.outcome?.message || "Change was not applied. Review and enter the token again."); return; }
301
+ approvedSetup = true; submission = null; $("finish").disabled = false; $("finish").textContent = "Start AgEnD";
302
+ $("finishMsg").textContent = "Configuration confirmed. Select Start AgEnD to finish setup.";
303
+ } catch { if (generation === pollGeneration) setTimeout(poll, 1000); }
304
+ };
305
+ setTimeout(poll, 1000);
306
+ };
262
307
  $("finish").onclick = async () => {
263
308
  $("finish").disabled = true;
264
- $("finishMsg").textContent = "Writing configuration…";
265
- const commit = await api("api/settings/quickstart/commit", { method: "POST", body: JSON.stringify(Object.assign({}, input(), { token: $("token").value.trim() })) });
266
- if (!commit.ok) { $("finishMsg").className = "msg err"; $("finishMsg").textContent = commit.body?.error || "Failed."; $("finish").disabled = false; return; }
267
- const finished = await api("setup/finish", { method: "POST" });
268
- $("finishMsg").className = "msg";
269
- if (finished.body && finished.body.watch === false) {
270
- // Behind a tunnel this page and its hostname are both about to stop
271
- // existing, so there is nothing here to watch. And the dashboard is not
272
- // the thing to promise: it binds loopback, so a link to it is one this
273
- // phone cannot open. The channel is what was just set up, and it is
274
- // where the agent will be.
275
- $("finishMsg").className = "msg ok";
276
- $("finishMsg").textContent = "AgEnD is starting. Talk to it in the channel you just set up — this page is done.";
277
- return;
278
- }
279
- $("finishMsg").textContent = "Starting AgEnD… this page will stop responding while the port changes hands.";
280
- waitForFleet();
309
+ try {
310
+ if (approvedSetup) {
311
+ const finished = await api("setup/finish", { method: "POST" });
312
+ if (!finished.ok) { restoreEditing(finished.body?.error || "Confirmation is no longer current."); return; }
313
+ $("finishMsg").textContent = "AgEnD is starting. Continue in the channel you configured.";
314
+ if (finished.body?.watch !== false) waitForFleet();
315
+ return;
316
+ }
317
+ // Preserve bytes and key on a lost HTTP response; a deliberate retry after
318
+ // a terminal outcome receives a new key and requires secret reentry.
319
+ submission ||= { key: crypto.randomUUID(), body: JSON.stringify(Object.assign({}, input(), { token: $("token").value.trim() })) };
320
+ freezeFields(true);
321
+ $("finishMsg").textContent = "Requesting host confirmation…";
322
+ const commit = await api("api/settings/quickstart/commit", { method: "POST", headers: { "Content-Type": "application/json", "Idempotency-Key": submission.key }, body: submission.body });
323
+ if (!commit.ok) { restoreEditing(commit.body?.error || "Failed."); return; }
324
+ if (commit.body?.result !== "pending_confirmation" || !commit.body.pending_change?.id) { restoreEditing("Expected a pending host confirmation."); return; }
325
+ $("token").value = ""; submission = null;
326
+ watchPending(commit.body.pending_change.id);
327
+ } catch { $("finishMsg").textContent = "Connection failed. Retry to find the same confirmation."; $("finish").disabled = false; }
281
328
  };
329
+ // Same-cookie reloads rediscover an unresolved consent request without a secret.
330
+ api("api/settings/pending").then(result => {
331
+ const pending = result.ok && Array.isArray(result.body) && result.body.find(item => item.state === "pending" || item.state === "applying");
332
+ if (pending) { watchPending(pending.id); return; }
333
+ api("setup/status").then(status => {
334
+ if (!status.ok || !status.body?.finish_ready || submission || pollGeneration) return;
335
+ approvedSetup = true; freezeFields(true); $("finish").disabled = false; $("finish").textContent = "Start AgEnD";
336
+ $("finishMsg").textContent = "Configuration confirmed. Select Start AgEnD to finish setup.";
337
+ }).catch(() => {});
338
+ }).catch(() => {});
282
339
 
283
340
  // The host is gone; the fleet is binding the same port. Connection refused is
284
341
  // the expected state in between, so it is not an error until the cap.
@@ -1 +1 @@
1
- {"version":3,"file":"setup-form.js","sourceRoot":"","sources":["../src/setup-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8DnC,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0O9B,CAAC"}
1
+ {"version":3,"file":"setup-form.js","sourceRoot":"","sources":["../src/setup-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8DnC,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmS9B,CAAC"}
@@ -0,0 +1,13 @@
1
+ /** Locale-keyed setup guide URLs per platform. */
2
+ export declare const SETUP_GUIDE_URLS: {
3
+ readonly telegram: {
4
+ readonly en: "https://songsid.github.io/AgEnD/setup-telegram/";
5
+ readonly "zh-TW": "https://songsid.github.io/AgEnD/zh-tw/setup-telegram/";
6
+ };
7
+ readonly discord: {
8
+ readonly en: "https://songsid.github.io/AgEnD/setup-discord/";
9
+ readonly "zh-TW": "https://songsid.github.io/AgEnD/zh-tw/setup-discord/";
10
+ };
11
+ };
12
+ /** Returns the locale-appropriate setup guide URL for the given platform. */
13
+ export declare function setupGuideUrl(platform: "telegram" | "discord"): string;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Setup guide URLs for the bot-token steps in quickstart and the setup wizard.
3
+ *
4
+ * A single constant used by both src/quickstart.ts and src/setup-wizard.ts to
5
+ * avoid duplication and prevent a circular import (quickstart imports from
6
+ * setup-wizard, so the URLs must live in a third module that both can import).
7
+ */
8
+ import { getLocale } from "./locale.js";
9
+ /** Locale-keyed setup guide URLs per platform. */
10
+ export const SETUP_GUIDE_URLS = {
11
+ telegram: {
12
+ en: "https://songsid.github.io/AgEnD/setup-telegram/",
13
+ "zh-TW": "https://songsid.github.io/AgEnD/zh-tw/setup-telegram/",
14
+ },
15
+ discord: {
16
+ en: "https://songsid.github.io/AgEnD/setup-discord/",
17
+ "zh-TW": "https://songsid.github.io/AgEnD/zh-tw/setup-discord/",
18
+ },
19
+ };
20
+ /** Returns the locale-appropriate setup guide URL for the given platform. */
21
+ export function setupGuideUrl(platform) {
22
+ const locale = getLocale();
23
+ return SETUP_GUIDE_URLS[platform][locale === "zh-TW" ? "zh-TW" : "en"];
24
+ }
25
+ //# sourceMappingURL=setup-guide.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-guide.js","sourceRoot":"","sources":["../src/setup-guide.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,kDAAkD;AAClD,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,QAAQ,EAAE;QACR,EAAE,EAAE,iDAAiD;QACrD,OAAO,EAAE,uDAAuD;KACjE;IACD,OAAO,EAAE;QACP,EAAE,EAAE,gDAAgD;QACpD,OAAO,EAAE,sDAAsD;KAChE;CACO,CAAC;AAEX,6EAA6E;AAC7E,MAAM,UAAU,aAAa,CAAC,QAAgC;IAC5D,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,OAAO,gBAAgB,CAAC,QAAQ,CAAC,CAAC,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AACzE,CAAC"}
@@ -67,6 +67,8 @@ export declare class SetupHost {
67
67
  private get basePath();
68
68
  private managedTunnel;
69
69
  private tunnelHandle;
70
+ private tunnelStartup;
71
+ private tunnelAbort;
70
72
  /** The exact host the tunnel answers on; nothing else is added to the set. */
71
73
  private externalHost;
72
74
  private tunnelStopResult;
@@ -80,6 +82,19 @@ export declare class SetupHost {
80
82
  * "starting", and no fleet.
81
83
  */
82
84
  private handoverRequested;
85
+ private confirmation;
86
+ private confirmationControl;
87
+ private setupContext;
88
+ private committedFingerprint;
89
+ private committedOwner;
90
+ private committedExecution;
91
+ private committedApproved;
92
+ private startedAt;
93
+ private admittedFinish;
94
+ private shutdownPromise;
95
+ private confirmationGate;
96
+ private finishReady;
97
+ private finishSetup;
83
98
  private get ttlMs();
84
99
  private get idleMs();
85
100
  private log;
@@ -145,6 +160,7 @@ export declare class SetupHost {
145
160
  * hang at "starting…" and the fleet would never be spawned.
146
161
  */
147
162
  shutdown(spawnSuccessor: boolean, reason: string): Promise<void>;
163
+ private performShutdown;
148
164
  /**
149
165
  * Whether an unconfirmed tunnel may be left behind while the fleet starts.
150
166
  *