@wrongstack/cli 0.296.4 → 0.298.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (523) hide show
  1. package/dist/{acp-A5OSIU2P.js → acp-S4LZQB66.js} +91 -23
  2. package/dist/{auth-CJWQJVRH.js → auth-NLLGEYQR.js} +3 -3
  3. package/dist/boot/dispatch-webui.d.ts +7 -0
  4. package/dist/boot/governance-evidence-candidate.d.ts +3 -0
  5. package/dist/boot/governance-shadow-bridge.d.ts +19 -0
  6. package/dist/boot/governance-trace-context.d.ts +64 -0
  7. package/dist/boot/launch-banner.d.ts +22 -0
  8. package/dist/boot/tui-session-resume.d.ts +10 -1
  9. package/dist/chimera-leader-wake.d.ts +57 -0
  10. package/dist/chimera-reviewer-policy.d.ts +89 -1
  11. package/dist/chimera-subagent-ladder.d.ts +72 -0
  12. package/dist/{chunk-AZESNNIC.js → chunk-4HH3YGG4.js} +3 -3
  13. package/dist/{chunk-IHBYBYGA.js → chunk-A6Q5KGLV.js} +51 -18
  14. package/dist/{chunk-ABXK4VVY.js → chunk-ARWGQUB3.js} +2 -1
  15. package/dist/{chunk-OI7GWJDS.js → chunk-C5GH4YBL.js} +1389 -184
  16. package/dist/{chunk-TADVX644.js → chunk-GO3TJICK.js} +11 -2
  17. package/dist/{chunk-JBTFWWKV.js → chunk-I2BM6FK6.js} +9 -4
  18. package/dist/{chunk-AXVZMEWG.js → chunk-I5MEUFDI.js} +2 -2
  19. package/dist/{chunk-WY7KKOUK.js → chunk-JW75HY4F.js} +31 -10
  20. package/dist/chunk-U3SS4NRH.js +10 -0
  21. package/dist/{chunk-Z7YGASQP.js → chunk-V3XH6XBJ.js} +30 -26
  22. package/dist/{chunk-BQCAJ7FI.js → chunk-XTIXVJXP.js} +16 -3
  23. package/dist/{cli-main-TOAWVJDY.js → cli-main-TJEU4OUY.js} +25912 -25134
  24. package/dist/cli-main-helpers.d.ts +42 -2
  25. package/dist/{execution-O7I5DYE6.js → execution-INOV5P7N.js} +1264 -650
  26. package/dist/execution-chimera-cascade.d.ts +7 -1
  27. package/dist/execution-chimera-review.d.ts +35 -0
  28. package/dist/fleet/host-types.d.ts +6 -1
  29. package/dist/fleet-statusline.d.ts +1 -0
  30. package/dist/goal-host.d.ts +1 -1
  31. package/dist/governance-V2Z42RBE.js +65 -0
  32. package/dist/governance-hq-telemetry.d.ts +17 -0
  33. package/dist/{hq-JUYKUNJX.js → hq-WO6CLUSI.js} +2 -2
  34. package/dist/hq-publisher.d.ts +3 -2
  35. package/dist/hq-server/auth.d.ts +33 -2
  36. package/dist/hq-server/login-attempt-store.d.ts +73 -0
  37. package/dist/hq-server/routes/auth-handlers.d.ts +73 -5
  38. package/dist/hq-server/routes.d.ts +5 -5
  39. package/dist/hq-server/types.d.ts +44 -2
  40. package/dist/hq-server/utils.d.ts +51 -6
  41. package/dist/hq-server/ws.d.ts +61 -2
  42. package/dist/{hq-server-6AQ3BXZA.js → hq-server-6TSAORAC.js} +2 -2
  43. package/dist/hq-server.d.ts +10 -0
  44. package/dist/index.js +337 -42
  45. package/dist/kanban-hq-sync.d.ts +13 -1
  46. package/dist/live-settings-input.d.ts +10 -1
  47. package/dist/{mailbox-serve-2UWHEN3M.js → mailbox-serve-DBOOOOWZ.js} +50 -8
  48. package/dist/{sdd-runtime-JG4EEKL6.js → sdd-runtime-AIJCJGWC.js} +2 -2
  49. package/dist/services/sdd/state.d.ts +3 -3
  50. package/dist/settings-menu.d.ts +1 -1
  51. package/dist/slash-commands/command-context.d.ts +2 -0
  52. package/dist/slash-commands/fallback.d.ts +2 -0
  53. package/dist/slash-commands/goal-refiner.d.ts +1 -1
  54. package/dist/slash-commands/goal.d.ts +1 -1
  55. package/dist/subcommands/handlers/acp-connection-gate.d.ts +20 -0
  56. package/dist/subcommands/handlers/governance.d.ts +8 -0
  57. package/dist/{update-YC4TYW2Y.js → update-MV6TUACD.js} +3 -3
  58. package/dist/utils/win32-cmd.d.ts +9 -6
  59. package/dist/{version-help-DROHZRW6.js → version-help-ASKQYXDJ.js} +2 -2
  60. package/dist/webui-server/kanban-host-adapter.d.ts +1 -1
  61. package/dist/webui-server/session-start-payload.d.ts +4 -0
  62. package/dist/{webui-server-ZF5KVGC5.js → webui-server-PCLSXX6L.js} +164 -134
  63. package/dist/webui-server-options.d.ts +7 -0
  64. package/dist/wiring/brain-and-orchestration.d.ts +2 -0
  65. package/dist/wiring/hq-telemetry.d.ts +4 -3
  66. package/dist/wiring/lifecycle-plugins.d.ts +5 -8
  67. package/dist/wiring/metrics.d.ts +1 -1
  68. package/dist/wiring/provider-runtime-setup.d.ts +2 -2
  69. package/package.json +23 -22
  70. package/dist/acp-A5OSIU2P.js.map +0 -7
  71. package/dist/acp-registry-cache.d.ts.map +0 -1
  72. package/dist/acp-server-agent.d.ts.map +0 -1
  73. package/dist/arg-parser.d.ts.map +0 -1
  74. package/dist/audit-2ZSFGNIF.js.map +0 -7
  75. package/dist/auth-CJWQJVRH.js.map +0 -7
  76. package/dist/auth-menu/add-provider.d.ts.map +0 -1
  77. package/dist/auth-menu/anthropic-oauth.d.ts.map +0 -1
  78. package/dist/auth-menu/auth-menu-audit.d.ts.map +0 -1
  79. package/dist/auth-menu/direct.d.ts.map +0 -1
  80. package/dist/auth-menu/github-copilot-oauth.d.ts.map +0 -1
  81. package/dist/auth-menu/helpers.d.ts.map +0 -1
  82. package/dist/auth-menu/index.d.ts.map +0 -1
  83. package/dist/auth-menu/local-presets.d.ts.map +0 -1
  84. package/dist/auth-menu/local.d.ts.map +0 -1
  85. package/dist/auth-menu/loopback-server.d.ts.map +0 -1
  86. package/dist/auth-menu/oauth-menu.d.ts.map +0 -1
  87. package/dist/auth-menu/openai-codex-oauth.d.ts.map +0 -1
  88. package/dist/auth-menu/panel-service.d.ts.map +0 -1
  89. package/dist/auth-menu/provider-menu.d.ts.map +0 -1
  90. package/dist/auth-menu/shared.d.ts.map +0 -1
  91. package/dist/auth-menu/top-menu.d.ts.map +0 -1
  92. package/dist/auth-menu/types.d.ts.map +0 -1
  93. package/dist/auth-menu.d.ts.map +0 -1
  94. package/dist/bench-7ZDWSNEF.js.map +0 -7
  95. package/dist/boot/auto-discover-providers.d.ts.map +0 -1
  96. package/dist/boot/brain-decision-log.d.ts.map +0 -1
  97. package/dist/boot/config-backup-recovery.d.ts.map +0 -1
  98. package/dist/boot/container-wiring.d.ts.map +0 -1
  99. package/dist/boot/dispatch-singleshot.d.ts.map +0 -1
  100. package/dist/boot/dispatch-tui.d.ts.map +0 -1
  101. package/dist/boot/dispatch-webui.d.ts.map +0 -1
  102. package/dist/boot/event-wiring.d.ts.map +0 -1
  103. package/dist/boot/execution-mode.d.ts.map +0 -1
  104. package/dist/boot/launch-menu.d.ts.map +0 -1
  105. package/dist/boot/short-circuit-desktop.d.ts.map +0 -1
  106. package/dist/boot/short-circuit-flags.d.ts.map +0 -1
  107. package/dist/boot/short-circuit-hq.d.ts.map +0 -1
  108. package/dist/boot/simpleui-full-auto.d.ts.map +0 -1
  109. package/dist/boot/system-prompt-builder.d.ts.map +0 -1
  110. package/dist/boot/system-prompt.d.ts.map +0 -1
  111. package/dist/boot/tool-registry.d.ts.map +0 -1
  112. package/dist/boot/tui-coordinator-callbacks.d.ts.map +0 -1
  113. package/dist/boot/tui-coordinator-setup.d.ts.map +0 -1
  114. package/dist/boot/tui-debug-stream.d.ts.map +0 -1
  115. package/dist/boot/tui-goal-wiring.d.ts.map +0 -1
  116. package/dist/boot/tui-live-sessions.d.ts.map +0 -1
  117. package/dist/boot/tui-project-picker-callback.d.ts.map +0 -1
  118. package/dist/boot/tui-project-spawn.d.ts.map +0 -1
  119. package/dist/boot/tui-project-switch.d.ts.map +0 -1
  120. package/dist/boot/tui-runtime-state.d.ts.map +0 -1
  121. package/dist/boot/tui-sdd-callback.d.ts.map +0 -1
  122. package/dist/boot/tui-session-resume.d.ts.map +0 -1
  123. package/dist/boot/tui-settings-adapter.d.ts.map +0 -1
  124. package/dist/boot-config.d.ts.map +0 -1
  125. package/dist/boot.d.ts.map +0 -1
  126. package/dist/brain-menu/panel-service.d.ts.map +0 -1
  127. package/dist/chimera-review-task.d.ts.map +0 -1
  128. package/dist/chimera-reviewer-policy.d.ts.map +0 -1
  129. package/dist/chronicle-QQEQFSSE.js.map +0 -7
  130. package/dist/chunk-5N34EWJR.js.map +0 -7
  131. package/dist/chunk-66T43Q4Y.js.map +0 -7
  132. package/dist/chunk-7OCVIDC7.js.map +0 -7
  133. package/dist/chunk-ABXK4VVY.js.map +0 -7
  134. package/dist/chunk-AXVZMEWG.js.map +0 -7
  135. package/dist/chunk-AZESNNIC.js.map +0 -7
  136. package/dist/chunk-BGFM3PD7.js.map +0 -7
  137. package/dist/chunk-BQCAJ7FI.js.map +0 -7
  138. package/dist/chunk-DPJZQCVP.js.map +0 -7
  139. package/dist/chunk-F4PEGAJZ.js.map +0 -7
  140. package/dist/chunk-IHBYBYGA.js.map +0 -7
  141. package/dist/chunk-JBSQ7ONN.js.map +0 -7
  142. package/dist/chunk-JBTFWWKV.js.map +0 -7
  143. package/dist/chunk-JUHRI6OV.js.map +0 -7
  144. package/dist/chunk-OI7GWJDS.js.map +0 -7
  145. package/dist/chunk-OP47B3IX.js.map +0 -7
  146. package/dist/chunk-PJHET45A.js.map +0 -7
  147. package/dist/chunk-Q5GTM25S.js.map +0 -7
  148. package/dist/chunk-QD544J2B.js.map +0 -7
  149. package/dist/chunk-QUNHS6O3.js.map +0 -7
  150. package/dist/chunk-TADVX644.js.map +0 -7
  151. package/dist/chunk-TGDHN4LM.js.map +0 -7
  152. package/dist/chunk-TYO2OVAD.js.map +0 -7
  153. package/dist/chunk-VCYZ3WZV.js.map +0 -7
  154. package/dist/chunk-VJUEZZYB.js +0 -29
  155. package/dist/chunk-VJUEZZYB.js.map +0 -7
  156. package/dist/chunk-WHOIR577.js.map +0 -7
  157. package/dist/chunk-WY7KKOUK.js.map +0 -7
  158. package/dist/chunk-XJXDOF63.js.map +0 -7
  159. package/dist/chunk-XWEGWYDU.js.map +0 -7
  160. package/dist/chunk-YWUPHRGB.js.map +0 -7
  161. package/dist/chunk-Z7YGASQP.js.map +0 -7
  162. package/dist/cli-bundled-prompts.d.ts.map +0 -1
  163. package/dist/cli-bundled-skills.d.ts.map +0 -1
  164. package/dist/cli-context.d.ts.map +0 -1
  165. package/dist/cli-entry-main.d.ts.map +0 -1
  166. package/dist/cli-entry-point.d.ts.map +0 -1
  167. package/dist/cli-eternal-flag.d.ts.map +0 -1
  168. package/dist/cli-main-TOAWVJDY.js.map +0 -7
  169. package/dist/cli-main-helpers.d.ts.map +0 -1
  170. package/dist/cli-main.d.ts.map +0 -1
  171. package/dist/cli-update-notice.d.ts.map +0 -1
  172. package/dist/config-doctor.d.ts.map +0 -1
  173. package/dist/config-history.d.ts.map +0 -1
  174. package/dist/context-limit.d.ts.map +0 -1
  175. package/dist/context-overflow-diagnostic.d.ts.map +0 -1
  176. package/dist/diag-doctor-OQ3AA63O.js.map +0 -7
  177. package/dist/diff-renderer.d.ts.map +0 -1
  178. package/dist/execute-deps.d.ts.map +0 -1
  179. package/dist/execution-O7I5DYE6.js.map +0 -7
  180. package/dist/execution-chimera-ask.d.ts.map +0 -1
  181. package/dist/execution-chimera-cascade.d.ts.map +0 -1
  182. package/dist/execution-cleanup.d.ts.map +0 -1
  183. package/dist/execution-kanban-dispatch.d.ts.map +0 -1
  184. package/dist/execution-repl-fleet-callbacks.d.ts.map +0 -1
  185. package/dist/execution-storage-observability.d.ts.map +0 -1
  186. package/dist/execution-tui-next-step-callbacks.d.ts.map +0 -1
  187. package/dist/execution.d.ts.map +0 -1
  188. package/dist/export-TPORYVRZ.js.map +0 -7
  189. package/dist/fleet/host-acp-runner-cache.d.ts.map +0 -1
  190. package/dist/fleet/host-acp.d.ts.map +0 -1
  191. package/dist/fleet/host-concurrency.d.ts.map +0 -1
  192. package/dist/fleet/host-context.d.ts.map +0 -1
  193. package/dist/fleet/host-director-builder.d.ts.map +0 -1
  194. package/dist/fleet/host-director-event-bridges.d.ts.map +0 -1
  195. package/dist/fleet/host-director-services.d.ts.map +0 -1
  196. package/dist/fleet/host-event-bridge.d.ts.map +0 -1
  197. package/dist/fleet/host-helpers.d.ts.map +0 -1
  198. package/dist/fleet/host-learning-tracker.d.ts.map +0 -1
  199. package/dist/fleet/host-learning.d.ts.map +0 -1
  200. package/dist/fleet/host-lifecycle-events.d.ts.map +0 -1
  201. package/dist/fleet/host-paths.d.ts.map +0 -1
  202. package/dist/fleet/host-provider.d.ts.map +0 -1
  203. package/dist/fleet/host-session-writer.d.ts.map +0 -1
  204. package/dist/fleet/host-shadow-pass.d.ts.map +0 -1
  205. package/dist/fleet/host-spawn-types.d.ts.map +0 -1
  206. package/dist/fleet/host-status.d.ts.map +0 -1
  207. package/dist/fleet/host-subagent-factory.d.ts.map +0 -1
  208. package/dist/fleet/host-supervisor.d.ts.map +0 -1
  209. package/dist/fleet/host-task-result-report.d.ts.map +0 -1
  210. package/dist/fleet/host-types.d.ts.map +0 -1
  211. package/dist/fleet/host.d.ts.map +0 -1
  212. package/dist/fleet/routing.d.ts.map +0 -1
  213. package/dist/fleet/status-broadcast.d.ts.map +0 -1
  214. package/dist/fleet/supervisor-registry.d.ts.map +0 -1
  215. package/dist/fleet-statusline.d.ts.map +0 -1
  216. package/dist/goal-host.d.ts.map +0 -1
  217. package/dist/hooks-wiring.d.ts.map +0 -1
  218. package/dist/hq-JUYKUNJX.js.map +0 -7
  219. package/dist/hq-command-controller.d.ts.map +0 -1
  220. package/dist/hq-publisher.d.ts.map +0 -1
  221. package/dist/hq-recovery-html.d.ts.map +0 -1
  222. package/dist/hq-server/audit-actor.d.ts.map +0 -1
  223. package/dist/hq-server/auth-state.d.ts.map +0 -1
  224. package/dist/hq-server/auth.d.ts.map +0 -1
  225. package/dist/hq-server/preflight.d.ts.map +0 -1
  226. package/dist/hq-server/project-root.d.ts.map +0 -1
  227. package/dist/hq-server/routes/auth-handlers.d.ts.map +0 -1
  228. package/dist/hq-server/routes/session-handlers.d.ts.map +0 -1
  229. package/dist/hq-server/routes/system-handlers.d.ts.map +0 -1
  230. package/dist/hq-server/routes.d.ts.map +0 -1
  231. package/dist/hq-server/snapshot.d.ts.map +0 -1
  232. package/dist/hq-server/startup.d.ts.map +0 -1
  233. package/dist/hq-server/trust-boundary.d.ts.map +0 -1
  234. package/dist/hq-server/types.d.ts.map +0 -1
  235. package/dist/hq-server/utils.d.ts.map +0 -1
  236. package/dist/hq-server/ws.d.ts.map +0 -1
  237. package/dist/hq-server-6AQ3BXZA.js.map +0 -7
  238. package/dist/hq-server.d.ts.map +0 -1
  239. package/dist/hq-static-serve.d.ts.map +0 -1
  240. package/dist/hq-ttl-U2FAYFAT.js.map +0 -7
  241. package/dist/hq-tunnel-RFKGC4HI.js.map +0 -7
  242. package/dist/hq-tunnel.d.ts.map +0 -1
  243. package/dist/index.d.ts.map +0 -1
  244. package/dist/index.js.map +0 -7
  245. package/dist/init-E2NDDOHI.js.map +0 -7
  246. package/dist/input-reader.d.ts.map +0 -1
  247. package/dist/kanban-dispatch-route.d.ts.map +0 -1
  248. package/dist/kanban-hq-sync.d.ts.map +0 -1
  249. package/dist/kanban-run-mirror-EKVNGXWW.js.map +0 -7
  250. package/dist/launch-hints.d.ts.map +0 -1
  251. package/dist/live-settings-input.d.ts.map +0 -1
  252. package/dist/mailbox-bridge-bootstrap.d.ts.map +0 -1
  253. package/dist/mailbox-serve-2UWHEN3M.js.map +0 -7
  254. package/dist/mcp-CYLPUEHC.js.map +0 -7
  255. package/dist/mcp-serve.d.ts.map +0 -1
  256. package/dist/modeldiag-HENYJ4N7.js.map +0 -7
  257. package/dist/multi-agent.d.ts.map +0 -1
  258. package/dist/next-task-predictor.d.ts.map +0 -1
  259. package/dist/permission-prompt.d.ts.map +0 -1
  260. package/dist/permissions-FNYLZIL4.js.map +0 -7
  261. package/dist/picker-key-state.d.ts.map +0 -1
  262. package/dist/picker-model-picker.d.ts.map +0 -1
  263. package/dist/picker-ui.d.ts.map +0 -1
  264. package/dist/picker.d.ts.map +0 -1
  265. package/dist/plugin-api-factory.d.ts.map +0 -1
  266. package/dist/plugin-management.d.ts.map +0 -1
  267. package/dist/plugin-usage-OU6X6LM3.js.map +0 -7
  268. package/dist/plugins-4LJ5IUBI.js.map +0 -7
  269. package/dist/pre-launch/launch-prompts.d.ts.map +0 -1
  270. package/dist/pre-launch/project-check.d.ts.map +0 -1
  271. package/dist/pre-launch.d.ts.map +0 -1
  272. package/dist/preflight.d.ts.map +0 -1
  273. package/dist/profile-config-path.d.ts.map +0 -1
  274. package/dist/project-BLQ3ALVL.js.map +0 -7
  275. package/dist/project-manifest-GE25PW4S.js.map +0 -7
  276. package/dist/project-picker-BOCWHBXP.js.map +0 -7
  277. package/dist/project-picker.d.ts.map +0 -1
  278. package/dist/projects-XE76NFNZ.js.map +0 -7
  279. package/dist/provider-config-utils.d.ts.map +0 -1
  280. package/dist/provider-helpers.d.ts.map +0 -1
  281. package/dist/provider-id.d.ts.map +0 -1
  282. package/dist/providers-models-GDELBHZK.js.map +0 -7
  283. package/dist/quick-VIO2BEHS.js.map +0 -7
  284. package/dist/renderer.d.ts.map +0 -1
  285. package/dist/repl-auto-proceed.d.ts.map +0 -1
  286. package/dist/repl-client-registration.d.ts.map +0 -1
  287. package/dist/repl-input.d.ts.map +0 -1
  288. package/dist/repl-options.d.ts.map +0 -1
  289. package/dist/repl-rendering.d.ts.map +0 -1
  290. package/dist/repl-suggestions.d.ts.map +0 -1
  291. package/dist/repl.d.ts.map +0 -1
  292. package/dist/replay-KCTXNMZQ.js.map +0 -7
  293. package/dist/rewind-EJMVFVLS.js.map +0 -7
  294. package/dist/sdd-runtime-JG4EEKL6.js.map +0 -7
  295. package/dist/services/autonomy-mode.d.ts.map +0 -1
  296. package/dist/services/commit-message.d.ts.map +0 -1
  297. package/dist/services/dispatch-classifier.d.ts.map +0 -1
  298. package/dist/services/mcp-management.d.ts.map +0 -1
  299. package/dist/services/project-facts.d.ts.map +0 -1
  300. package/dist/services/project-manifest.d.ts.map +0 -1
  301. package/dist/services/sdd/project-context.d.ts.map +0 -1
  302. package/dist/services/sdd/spec-detection.d.ts.map +0 -1
  303. package/dist/services/sdd/state.d.ts.map +0 -1
  304. package/dist/services/sdd/task-manager.d.ts.map +0 -1
  305. package/dist/services/sdd-runtime.d.ts.map +0 -1
  306. package/dist/services/statusline-config.d.ts.map +0 -1
  307. package/dist/services/suggestion-store.d.ts.map +0 -1
  308. package/dist/session-event-wiring.d.ts.map +0 -1
  309. package/dist/session-stats.d.ts.map +0 -1
  310. package/dist/sessions-config-NLCMK3BY.js.map +0 -7
  311. package/dist/settings-menu.d.ts.map +0 -1
  312. package/dist/shutdown-cleanup.d.ts.map +0 -1
  313. package/dist/simpleui-dist-JIFGUJO7.js.map +0 -7
  314. package/dist/simpleui-dist.d.ts.map +0 -1
  315. package/dist/slash-commands/acp.d.ts.map +0 -1
  316. package/dist/slash-commands/agent-improve.d.ts.map +0 -1
  317. package/dist/slash-commands/agents.d.ts.map +0 -1
  318. package/dist/slash-commands/audit.d.ts.map +0 -1
  319. package/dist/slash-commands/auth.d.ts.map +0 -1
  320. package/dist/slash-commands/autonomy.d.ts.map +0 -1
  321. package/dist/slash-commands/brain.d.ts.map +0 -1
  322. package/dist/slash-commands/btw.d.ts.map +0 -1
  323. package/dist/slash-commands/clear.d.ts.map +0 -1
  324. package/dist/slash-commands/codebase-reindex.d.ts.map +0 -1
  325. package/dist/slash-commands/collab.d.ts.map +0 -1
  326. package/dist/slash-commands/command-context.d.ts.map +0 -1
  327. package/dist/slash-commands/commit-llm.d.ts.map +0 -1
  328. package/dist/slash-commands/compact.d.ts.map +0 -1
  329. package/dist/slash-commands/context.d.ts.map +0 -1
  330. package/dist/slash-commands/coordinator.d.ts.map +0 -1
  331. package/dist/slash-commands/delegate.d.ts.map +0 -1
  332. package/dist/slash-commands/design.d.ts.map +0 -1
  333. package/dist/slash-commands/dev.d.ts.map +0 -1
  334. package/dist/slash-commands/diag-stats.d.ts.map +0 -1
  335. package/dist/slash-commands/dispatch-llm.d.ts.map +0 -1
  336. package/dist/slash-commands/doctor.d.ts.map +0 -1
  337. package/dist/slash-commands/enhance.d.ts.map +0 -1
  338. package/dist/slash-commands/ensemble.d.ts.map +0 -1
  339. package/dist/slash-commands/f-keys.d.ts.map +0 -1
  340. package/dist/slash-commands/fallback.d.ts.map +0 -1
  341. package/dist/slash-commands/fix-classifier.d.ts.map +0 -1
  342. package/dist/slash-commands/fix.d.ts.map +0 -1
  343. package/dist/slash-commands/fleet.d.ts.map +0 -1
  344. package/dist/slash-commands/git.d.ts.map +0 -1
  345. package/dist/slash-commands/gitid.d.ts.map +0 -1
  346. package/dist/slash-commands/goal-refiner.d.ts.map +0 -1
  347. package/dist/slash-commands/goal.d.ts.map +0 -1
  348. package/dist/slash-commands/health.d.ts.map +0 -1
  349. package/dist/slash-commands/help.d.ts.map +0 -1
  350. package/dist/slash-commands/helpers.d.ts.map +0 -1
  351. package/dist/slash-commands/hq.d.ts.map +0 -1
  352. package/dist/slash-commands/index.d.ts.map +0 -1
  353. package/dist/slash-commands/init.d.ts.map +0 -1
  354. package/dist/slash-commands/interrupt.d.ts.map +0 -1
  355. package/dist/slash-commands/kanban-agent-helpers.d.ts.map +0 -1
  356. package/dist/slash-commands/kanban-format.d.ts.map +0 -1
  357. package/dist/slash-commands/kanban-help.d.ts.map +0 -1
  358. package/dist/slash-commands/kanban.d.ts.map +0 -1
  359. package/dist/slash-commands/mailbox-demo.d.ts.map +0 -1
  360. package/dist/slash-commands/mailbox-serve.d.ts.map +0 -1
  361. package/dist/slash-commands/mailbox.d.ts.map +0 -1
  362. package/dist/slash-commands/mcp-utils.d.ts.map +0 -1
  363. package/dist/slash-commands/mcp.d.ts.map +0 -1
  364. package/dist/slash-commands/memory-compact.d.ts.map +0 -1
  365. package/dist/slash-commands/memory-flags.d.ts.map +0 -1
  366. package/dist/slash-commands/memory-formatters.d.ts.map +0 -1
  367. package/dist/slash-commands/memory.d.ts.map +0 -1
  368. package/dist/slash-commands/metrics.d.ts.map +0 -1
  369. package/dist/slash-commands/mode.d.ts.map +0 -1
  370. package/dist/slash-commands/modelcaps.d.ts.map +0 -1
  371. package/dist/slash-commands/models.d.ts.map +0 -1
  372. package/dist/slash-commands/mouse.d.ts.map +0 -1
  373. package/dist/slash-commands/next.d.ts.map +0 -1
  374. package/dist/slash-commands/plan.d.ts.map +0 -1
  375. package/dist/slash-commands/plugin.d.ts.map +0 -1
  376. package/dist/slash-commands/profile.d.ts.map +0 -1
  377. package/dist/slash-commands/project-utils.d.ts.map +0 -1
  378. package/dist/slash-commands/project.d.ts.map +0 -1
  379. package/dist/slash-commands/provider-status.d.ts.map +0 -1
  380. package/dist/slash-commands/prune.d.ts.map +0 -1
  381. package/dist/slash-commands/refiner.d.ts.map +0 -1
  382. package/dist/slash-commands/review.d.ts.map +0 -1
  383. package/dist/slash-commands/sdd/rendering.d.ts.map +0 -1
  384. package/dist/slash-commands/sdd-command-helpers.d.ts.map +0 -1
  385. package/dist/slash-commands/sdd-format.d.ts.map +0 -1
  386. package/dist/slash-commands/sdd.d.ts.map +0 -1
  387. package/dist/slash-commands/security.d.ts.map +0 -1
  388. package/dist/slash-commands/session.d.ts.map +0 -1
  389. package/dist/slash-commands/setmodel.d.ts.map +0 -1
  390. package/dist/slash-commands/settings.d.ts.map +0 -1
  391. package/dist/slash-commands/shadow.d.ts.map +0 -1
  392. package/dist/slash-commands/slash-deep-help.d.ts.map +0 -1
  393. package/dist/slash-commands/spawn-agents.d.ts.map +0 -1
  394. package/dist/slash-commands/statusline.d.ts.map +0 -1
  395. package/dist/slash-commands/suggest.d.ts.map +0 -1
  396. package/dist/slash-commands/suggestion-store.d.ts.map +0 -1
  397. package/dist/slash-commands/supervisor.d.ts.map +0 -1
  398. package/dist/slash-commands/surfaces.d.ts.map +0 -1
  399. package/dist/slash-commands/tasks.d.ts.map +0 -1
  400. package/dist/slash-commands/techstack.d.ts.map +0 -1
  401. package/dist/slash-commands/telegram-pairing.d.ts.map +0 -1
  402. package/dist/slash-commands/telegram-settings.d.ts.map +0 -1
  403. package/dist/slash-commands/telegram-setup.d.ts.map +0 -1
  404. package/dist/slash-commands/todos.d.ts.map +0 -1
  405. package/dist/slash-commands/tool.d.ts.map +0 -1
  406. package/dist/slash-commands/tools.d.ts.map +0 -1
  407. package/dist/slash-commands/tuneup.d.ts.map +0 -1
  408. package/dist/slash-commands/working-dir.d.ts.map +0 -1
  409. package/dist/slash-commands/worktree.d.ts.map +0 -1
  410. package/dist/slash-commands/yolo.d.ts.map +0 -1
  411. package/dist/spinner.d.ts.map +0 -1
  412. package/dist/subcommands/contracts.d.ts.map +0 -1
  413. package/dist/subcommands/handlers/acp.d.ts.map +0 -1
  414. package/dist/subcommands/handlers/audit.d.ts.map +0 -1
  415. package/dist/subcommands/handlers/auth-local-help.d.ts.map +0 -1
  416. package/dist/subcommands/handlers/auth.d.ts.map +0 -1
  417. package/dist/subcommands/handlers/bench-run-help.d.ts.map +0 -1
  418. package/dist/subcommands/handlers/bench.d.ts.map +0 -1
  419. package/dist/subcommands/handlers/chronicle.d.ts.map +0 -1
  420. package/dist/subcommands/handlers/config-history.d.ts.map +0 -1
  421. package/dist/subcommands/handlers/diag-doctor.d.ts.map +0 -1
  422. package/dist/subcommands/handlers/export.d.ts.map +0 -1
  423. package/dist/subcommands/handlers/helpers.d.ts.map +0 -1
  424. package/dist/subcommands/handlers/hq.d.ts.map +0 -1
  425. package/dist/subcommands/handlers/init.d.ts.map +0 -1
  426. package/dist/subcommands/handlers/mailbox-serve.d.ts.map +0 -1
  427. package/dist/subcommands/handlers/mcp.d.ts.map +0 -1
  428. package/dist/subcommands/handlers/model-smoke-test.d.ts.map +0 -1
  429. package/dist/subcommands/handlers/modeldiag-profiles.d.ts.map +0 -1
  430. package/dist/subcommands/handlers/modeldiag.d.ts.map +0 -1
  431. package/dist/subcommands/handlers/models-add-help.d.ts.map +0 -1
  432. package/dist/subcommands/handlers/per-subcommand-deep-help-table.d.ts.map +0 -1
  433. package/dist/subcommands/handlers/per-subcommand-help-data.d.ts.map +0 -1
  434. package/dist/subcommands/handlers/per-subcommand-help-table.d.ts.map +0 -1
  435. package/dist/subcommands/handlers/per-subcommand-help-types.d.ts.map +0 -1
  436. package/dist/subcommands/handlers/per-subcommand-help.d.ts.map +0 -1
  437. package/dist/subcommands/handlers/permissions.d.ts.map +0 -1
  438. package/dist/subcommands/handlers/plugin-usage.d.ts.map +0 -1
  439. package/dist/subcommands/handlers/project.d.ts.map +0 -1
  440. package/dist/subcommands/handlers/projects.d.ts.map +0 -1
  441. package/dist/subcommands/handlers/providers-models.d.ts.map +0 -1
  442. package/dist/subcommands/handlers/quick.d.ts.map +0 -1
  443. package/dist/subcommands/handlers/replay.d.ts.map +0 -1
  444. package/dist/subcommands/handlers/rewind.d.ts.map +0 -1
  445. package/dist/subcommands/handlers/sessions-config.d.ts.map +0 -1
  446. package/dist/subcommands/handlers/sessions-fleet.d.ts.map +0 -1
  447. package/dist/subcommands/handlers/tools-skills.d.ts.map +0 -1
  448. package/dist/subcommands/handlers/update.d.ts.map +0 -1
  449. package/dist/subcommands/handlers/version-help.d.ts.map +0 -1
  450. package/dist/subcommands/index.d.ts.map +0 -1
  451. package/dist/theme.d.ts.map +0 -1
  452. package/dist/tools-skills-UNKLOB7M.js.map +0 -7
  453. package/dist/tui-thinking-word.d.ts.map +0 -1
  454. package/dist/tuneup.d.ts.map +0 -1
  455. package/dist/update-YC4TYW2Y.js.map +0 -7
  456. package/dist/update-check-UMYTOH7Z.js.map +0 -7
  457. package/dist/update-check.d.ts.map +0 -1
  458. package/dist/utils/delay-format.d.ts.map +0 -1
  459. package/dist/utils/hq-ttl.d.ts.map +0 -1
  460. package/dist/utils/win32-cmd.d.ts.map +0 -1
  461. package/dist/utils.d.ts.map +0 -1
  462. package/dist/version-help-DROHZRW6.js.map +0 -7
  463. package/dist/version.d.ts.map +0 -1
  464. package/dist/webui-server/client-registration.d.ts.map +0 -1
  465. package/dist/webui-server/connection-handler.d.ts.map +0 -1
  466. package/dist/webui-server/context-breakdown.d.ts.map +0 -1
  467. package/dist/webui-server/contracts.d.ts.map +0 -1
  468. package/dist/webui-server/cost-helpers.d.ts.map +0 -1
  469. package/dist/webui-server/kanban-host-adapter.d.ts.map +0 -1
  470. package/dist/webui-server/kanban-run-mirror.d.ts.map +0 -1
  471. package/dist/webui-server/kanban-supervisor.d.ts.map +0 -1
  472. package/dist/webui-server/lifecycle.d.ts.map +0 -1
  473. package/dist/webui-server/listen-helpers.d.ts.map +0 -1
  474. package/dist/webui-server/logger-shim.d.ts.map +0 -1
  475. package/dist/webui-server/node-pty-loader.d.ts.map +0 -1
  476. package/dist/webui-server/prefs-seeding.d.ts.map +0 -1
  477. package/dist/webui-server/privileged-actions.d.ts.map +0 -1
  478. package/dist/webui-server/provider-config.d.ts.map +0 -1
  479. package/dist/webui-server/session-start-payload.d.ts.map +0 -1
  480. package/dist/webui-server/setup-events.d.ts.map +0 -1
  481. package/dist/webui-server/static-serve.d.ts.map +0 -1
  482. package/dist/webui-server/stream-coalescer.d.ts.map +0 -1
  483. package/dist/webui-server/terminal-log-view.d.ts.map +0 -1
  484. package/dist/webui-server-ZF5KVGC5.js.map +0 -7
  485. package/dist/webui-server-options.d.ts.map +0 -1
  486. package/dist/webui-server.d.ts.map +0 -1
  487. package/dist/wiring/brain-and-orchestration.d.ts.map +0 -1
  488. package/dist/wiring/codebase-index.d.ts.map +0 -1
  489. package/dist/wiring/command-host-adapters.d.ts.map +0 -1
  490. package/dist/wiring/command-host-state.d.ts.map +0 -1
  491. package/dist/wiring/controllers.d.ts.map +0 -1
  492. package/dist/wiring/dep-watcher-bridge.d.ts.map +0 -1
  493. package/dist/wiring/dep-watcher.d.ts.map +0 -1
  494. package/dist/wiring/design-studio.d.ts.map +0 -1
  495. package/dist/wiring/director-setup.d.ts.map +0 -1
  496. package/dist/wiring/eternal-command-handlers.d.ts.map +0 -1
  497. package/dist/wiring/fleet-command-handlers.d.ts.map +0 -1
  498. package/dist/wiring/hq-telemetry.d.ts.map +0 -1
  499. package/dist/wiring/lifecycle-plugins.d.ts.map +0 -1
  500. package/dist/wiring/mailbox-bridge-bootstrap.d.ts.map +0 -1
  501. package/dist/wiring/management-tools.d.ts.map +0 -1
  502. package/dist/wiring/metrics.d.ts.map +0 -1
  503. package/dist/wiring/pipeline.d.ts.map +0 -1
  504. package/dist/wiring/plugins.d.ts.map +0 -1
  505. package/dist/wiring/provider-runtime-setup.d.ts.map +0 -1
  506. package/dist/wiring/provider-runtime.d.ts.map +0 -1
  507. package/dist/wiring/provider-status.d.ts.map +0 -1
  508. package/dist/wiring/provider-utility-tools.d.ts.map +0 -1
  509. package/dist/wiring/provider.d.ts.map +0 -1
  510. package/dist/wiring/replay.d.ts.map +0 -1
  511. package/dist/wiring/runtime-controller-deps.d.ts.map +0 -1
  512. package/dist/wiring/runtime-dispatch-state.d.ts.map +0 -1
  513. package/dist/wiring/runtime-lifecycle-deps.d.ts.map +0 -1
  514. package/dist/wiring/runtime-picker-deps.d.ts.map +0 -1
  515. package/dist/wiring/sage.d.ts.map +0 -1
  516. package/dist/wiring/sdd-handlers.d.ts.map +0 -1
  517. package/dist/wiring/security-command.d.ts.map +0 -1
  518. package/dist/wiring/session-command-handlers.d.ts.map +0 -1
  519. package/dist/wiring/session-registry.d.ts.map +0 -1
  520. package/dist/wiring/session.d.ts.map +0 -1
  521. package/dist/wiring/slash-commands.d.ts.map +0 -1
  522. package/dist/wiring/to-execute-deps.d.ts.map +0 -1
  523. package/dist/wiring/tools.d.ts.map +0 -1
@@ -4,9 +4,9 @@ import {
4
4
 
5
5
  // src/hq-server.ts
6
6
  import { randomUUID as randomUUID4 } from "node:crypto";
7
- import * as fs4 from "node:fs/promises";
7
+ import * as fs5 from "node:fs/promises";
8
8
  import * as http from "node:http";
9
- import * as path6 from "node:path";
9
+ import * as path7 from "node:path";
10
10
  import {
11
11
  createMailboxHttpRouter,
12
12
  createProjectMailbox,
@@ -16,16 +16,20 @@ import {
16
16
  MailboxHttpRateLimiter
17
17
  } from "@wrongstack/core/coordination";
18
18
  import {
19
+ assessHqExposure as assessHqExposure2,
19
20
  createHqPersistence,
20
21
  HqAlertEngine,
21
- HqCommandAuditLog,
22
22
  HqBootstrapCodeStore,
23
+ HqCommandAuditLog,
24
+ hqTokenKey as hqTokenKey3,
25
+ hqTokenVerifier as hqTokenVerifier2,
23
26
  mintHqCookieSecret as mintHqCookieSecret2,
24
27
  mutateHqAuthFile as mutateHqAuthFile2,
25
28
  toAlertMessage,
26
29
  watchHqAuthFile
27
30
  } from "@wrongstack/core/hq";
28
31
  import { createCompatibilityTrustBoundary } from "@wrongstack/core/security";
32
+ import { isLoopbackHost as isLoopbackHost3 } from "@wrongstack/core/hq";
29
33
  import { WebSocket as WebSocket3, WebSocketServer } from "ws";
30
34
 
31
35
  // src/hq-recovery-html.ts
@@ -55,8 +59,14 @@ var HQ_HTML = HQ_RECOVERY_HTML;
55
59
  // src/hq-server/auth.ts
56
60
  import { createHmac, timingSafeEqual } from "node:crypto";
57
61
  import { isIP } from "node:net";
62
+ import { hqTokenVerifier, isLoopbackHost, isTokenExpired } from "@wrongstack/core/hq";
58
63
  var HQ_SESSION_COOKIE = "hq.session";
64
+ var HQ_SESSION_COOKIE_SECURE = "__Host-hq.session";
65
+ function cookieName(secure) {
66
+ return secure ? HQ_SESSION_COOKIE_SECURE : HQ_SESSION_COOKIE;
67
+ }
59
68
  var HQ_SESSION_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
69
+ var HQ_SESSION_IDLE_TIMEOUT_MS = 30 * 6e4;
60
70
  function setHqSecurityHeaders(res) {
61
71
  res.setHeader("X-Content-Type-Options", "nosniff");
62
72
  res.setHeader("X-Frame-Options", "DENY");
@@ -70,7 +80,7 @@ function setHqSecurityHeaders(res) {
70
80
  "object-src 'none'",
71
81
  "frame-ancestors 'none'",
72
82
  "form-action 'self'",
73
- "script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'",
83
+ "script-src 'self' 'wasm-unsafe-eval'",
74
84
  "style-src 'self' 'unsafe-inline'",
75
85
  "font-src 'self' data:",
76
86
  "img-src 'self' data:",
@@ -106,13 +116,13 @@ function hasTrustedRequestAuthority(req, boundHost, boundPort, trustedPublicOrig
106
116
  return false;
107
117
  }
108
118
  }
109
- function hasTrustedBrowserOrigin(req, boundHost, boundPort, trustedPublicOrigins = /* @__PURE__ */ new Set()) {
119
+ function hasTrustedBrowserOrigin(req, boundHost, boundPort, trustedPublicOrigins = /* @__PURE__ */ new Set(), allowFileOrigin = false) {
110
120
  if (!hasTrustedRequestAuthority(req, boundHost, boundPort, trustedPublicOrigins)) return false;
111
121
  const origin = req.headers.origin;
112
122
  if (origin === void 0) return true;
113
123
  try {
114
124
  const parsed = new URL(origin);
115
- if (parsed.protocol === "file:") return true;
125
+ if (parsed.protocol === "file:") return allowFileOrigin;
116
126
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
117
127
  const rawRequestHost = req.headers.host?.trim();
118
128
  if (!rawRequestHost) return false;
@@ -165,8 +175,9 @@ function parseCookieHeader(cookieHeader) {
165
175
  return out;
166
176
  }
167
177
  function setHqSessionCookie(res, value, secure) {
178
+ const name = cookieName(secure);
168
179
  const parts = [
169
- `${HQ_SESSION_COOKIE}=${encodeURIComponent(value)}`,
180
+ `${name}=${encodeURIComponent(value)}`,
170
181
  "Path=/",
171
182
  "HttpOnly",
172
183
  "SameSite=Lax",
@@ -176,7 +187,8 @@ function setHqSessionCookie(res, value, secure) {
176
187
  res.setHeader("Set-Cookie", parts.join("; "));
177
188
  }
178
189
  function clearHqSessionCookie(res, secure) {
179
- const parts = [`${HQ_SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`];
190
+ const name = cookieName(secure);
191
+ const parts = [`${name}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`];
180
192
  if (secure) parts.push("Secure");
181
193
  res.setHeader("Set-Cookie", parts.join("; "));
182
194
  }
@@ -186,18 +198,40 @@ function isTokenAuth(auth) {
186
198
  function isCookieAuth(auth) {
187
199
  return auth !== void 0 && auth.kind === "cookie";
188
200
  }
201
+ function hqAuthRequired(mutableAuth, requireBrowserAuth) {
202
+ return requireBrowserAuth === true || mutableAuth.requireAuthFloor === true || mutableAuth.browserTokens.size > 0 || mutableAuth.passwordHash !== void 0;
203
+ }
204
+ function hqClientAuthRequired(mutableAuth) {
205
+ return mutableAuth.requireAuthFloor === true || mutableAuth.clientTokens.size > 0;
206
+ }
189
207
  function extractBrowserToken(req, url) {
190
208
  const queryToken = url.searchParams.get("token");
191
209
  if (queryToken) {
210
+ const requestHost = (req.headers.host ?? "").trim();
211
+ let hostname = "";
212
+ try {
213
+ hostname = new URL(`http://${requestHost}`).hostname;
214
+ } catch {
215
+ }
216
+ if (hostname && isLoopbackHost(hostname)) {
217
+ console.warn(
218
+ JSON.stringify({
219
+ level: "warn",
220
+ event: "hq.token_from_query_param",
221
+ message: "Browser token accepted from URL query parameter \u2014 token can leak through browser history, server access logs, and Referer headers.",
222
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
223
+ })
224
+ );
225
+ return queryToken;
226
+ }
192
227
  console.warn(
193
228
  JSON.stringify({
194
229
  level: "warn",
195
- event: "hq.token_from_query_param",
196
- message: "Browser token accepted from URL query parameter \u2014 token can leak through browser history, server access logs, and Referer headers.",
230
+ event: "hq.token_from_query_param_rejected",
231
+ message: "Browser token in URL query rejected on a non-loopback request \u2014 send it in the Authorization header instead.",
197
232
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
198
233
  })
199
234
  );
200
- return queryToken;
201
235
  }
202
236
  const auth = req.headers.authorization;
203
237
  if (typeof auth === "string" && auth.toLowerCase().startsWith("bearer ")) {
@@ -207,7 +241,7 @@ function extractBrowserToken(req, url) {
207
241
  }
208
242
  function timingSafeTokenMatch(tokens, supplied) {
209
243
  if (!supplied) return void 0;
210
- const b = Buffer.from(supplied);
244
+ const b = Buffer.from(hqTokenVerifier(supplied));
211
245
  for (const candidate of tokens) {
212
246
  const a = Buffer.from(candidate);
213
247
  if (a.length === b.length && timingSafeEqual(a, b)) return candidate;
@@ -220,6 +254,7 @@ function authenticateBrowserRequest(req, url, mutableAuth, sessions) {
220
254
  const matchedToken = timingSafeTokenMatch(mutableAuth.browserTokens, token);
221
255
  if (matchedToken) {
222
256
  const obj = mutableAuth.browserTokenObjs.get(matchedToken);
257
+ if (isTokenExpired(obj)) return void 0;
223
258
  const ctx = {
224
259
  kind: "token",
225
260
  token: matchedToken,
@@ -231,12 +266,19 @@ function authenticateBrowserRequest(req, url, mutableAuth, sessions) {
231
266
  }
232
267
  if (mutableAuth.cookieSecret) {
233
268
  const cookies = parseCookieHeader(req.headers.cookie);
234
- const raw = cookies[HQ_SESSION_COOKIE];
269
+ const raw = cookies[HQ_SESSION_COOKIE] ?? cookies["__Host-hq.session"];
235
270
  if (raw) {
236
271
  const sessionId = parseHqSessionCookie(raw, mutableAuth.cookieSecret);
237
272
  if (sessionId) {
238
273
  const session = sessions.get(sessionId);
239
- if (session && Date.now() - session.createdAt < HQ_SESSION_MAX_AGE_MS) {
274
+ const now = Date.now();
275
+ if (session && now - session.createdAt < HQ_SESSION_MAX_AGE_MS) {
276
+ if (session.pending2fa) return void 0;
277
+ if (now - session.lastSeenAt > HQ_SESSION_IDLE_TIMEOUT_MS) {
278
+ sessions.delete(sessionId);
279
+ return void 0;
280
+ }
281
+ session.lastSeenAt = now;
240
282
  if (session.kind === "token" && session.tokenId !== void 0) {
241
283
  const liveToken = [...mutableAuth.browserTokenObjs.values()].find(
242
284
  (obj) => obj.id === session.tokenId
@@ -259,10 +301,16 @@ function authenticateBrowserRequest(req, url, mutableAuth, sessions) {
259
301
  }
260
302
 
261
303
  // src/hq-server/auth-state.ts
262
- import { DEFAULT_HQ_REDACTION_POLICY, hqAuthContentHash, isTokenExpired, logHqAuthAudit } from "@wrongstack/core/hq";
304
+ import {
305
+ DEFAULT_HQ_REDACTION_POLICY,
306
+ hqAuthContentHash,
307
+ hqTokenKey,
308
+ isTokenExpired as isTokenExpired2,
309
+ logHqAuthAudit
310
+ } from "@wrongstack/core/hq";
263
311
  var TOKEN_EXPIRY_WARNING_MS = 24 * 60 * 60 * 1e3;
264
312
  function liveTokens(list) {
265
- return (list ?? []).filter((token) => !isTokenExpired(token));
313
+ return (list ?? []).filter((token) => !isTokenExpired2(token));
266
314
  }
267
315
  function createHqAuthState(authFile, dataDir) {
268
316
  let rawBrowserTokens = authFile.browserTokens ?? [];
@@ -270,12 +318,17 @@ function createHqAuthState(authFile, dataDir) {
270
318
  const mutableAuth = {
271
319
  operatorPolicy: { ...DEFAULT_HQ_REDACTION_POLICY, ...authFile.redactionPolicy ?? {} },
272
320
  operatorPolicyOverride: authFile.redactionPolicy,
273
- browserTokens: new Set(liveTokens(authFile.browserTokens).map(({ token }) => token)),
274
- clientTokens: new Set(liveTokens(authFile.clientTokens).map(({ token }) => token)),
321
+ browserTokens: new Set(liveTokens(authFile.browserTokens).map(hqTokenKey)),
322
+ clientTokens: new Set(liveTokens(authFile.clientTokens).map(hqTokenKey)),
275
323
  browserTokenObjs: browserTokenMap(authFile.browserTokens),
276
- clientTokenObjs: new Map(liveTokens(authFile.clientTokens).map((token) => [token.token, token])),
324
+ clientTokenObjs: new Map(
325
+ liveTokens(authFile.clientTokens).map((token) => [hqTokenKey(token), token])
326
+ ),
277
327
  passwordHash: authFile.passwordHash,
278
328
  cookieSecret: authFile.cookieSecret,
329
+ totpSecret: authFile.totpSecret,
330
+ totpPendingSecret: authFile.totpPendingSecret,
331
+ totpRecoveryCodes: authFile.totpRecoveryCodes,
279
332
  alertRules: authFile.alertRules
280
333
  };
281
334
  return {
@@ -283,7 +336,7 @@ function createHqAuthState(authFile, dataDir) {
283
336
  tokenStats: () => {
284
337
  const now = Date.now();
285
338
  const all = [...rawBrowserTokens, ...rawClientTokens];
286
- const live = all.filter((token) => !isTokenExpired(token, now));
339
+ const live = all.filter((token) => !isTokenExpired2(token, now));
287
340
  return {
288
341
  browserTotal: mutableAuth.browserTokens.size,
289
342
  clientTotal: mutableAuth.clientTokens.size,
@@ -307,14 +360,17 @@ function createHqAuthState(authFile, dataDir) {
307
360
  ...next.redactionPolicy ?? {}
308
361
  };
309
362
  mutableAuth.operatorPolicyOverride = next.redactionPolicy;
310
- mutableAuth.browserTokens = new Set(liveTokens(next.browserTokens).map(({ token }) => token));
311
- mutableAuth.clientTokens = new Set(liveTokens(next.clientTokens).map(({ token }) => token));
363
+ mutableAuth.browserTokens = new Set(liveTokens(next.browserTokens).map(hqTokenKey));
364
+ mutableAuth.clientTokens = new Set(liveTokens(next.clientTokens).map(hqTokenKey));
312
365
  mutableAuth.browserTokenObjs = browserTokenMap(next.browserTokens);
313
366
  mutableAuth.clientTokenObjs = new Map(
314
- liveTokens(next.clientTokens).map((token) => [token.token, token])
367
+ liveTokens(next.clientTokens).map((token) => [hqTokenKey(token), token])
315
368
  );
316
369
  mutableAuth.passwordHash = next.passwordHash;
317
370
  mutableAuth.cookieSecret = next.cookieSecret;
371
+ mutableAuth.totpSecret = next.totpSecret;
372
+ mutableAuth.totpPendingSecret = next.totpPendingSecret;
373
+ mutableAuth.totpRecoveryCodes = next.totpRecoveryCodes;
318
374
  mutableAuth.alertRules = next.alertRules;
319
375
  }
320
376
  };
@@ -322,7 +378,7 @@ function createHqAuthState(authFile, dataDir) {
322
378
  function browserTokenMap(tokens) {
323
379
  return new Map(
324
380
  liveTokens(tokens).map((token) => [
325
- token.token,
381
+ hqTokenKey(token),
326
382
  {
327
383
  id: token.id,
328
384
  ...token.capabilities !== void 0 ? { capabilities: token.capabilities } : {},
@@ -332,8 +388,12 @@ function browserTokenMap(tokens) {
332
388
  );
333
389
  }
334
390
  function auditPrunedTokens(scope, previous, next, authFile, dataDir) {
335
- const previouslyLive = new Set(previous.filter((token) => !isTokenExpired(token)).map(({ id }) => id));
336
- const prunedCount = next.filter((token) => isTokenExpired(token) && previouslyLive.has(token.id)).length;
391
+ const previouslyLive = new Set(
392
+ previous.filter((token) => !isTokenExpired2(token)).map(({ id }) => id)
393
+ );
394
+ const prunedCount = next.filter(
395
+ (token) => isTokenExpired2(token) && previouslyLive.has(token.id)
396
+ ).length;
337
397
  if (prunedCount === 0) return;
338
398
  const contentHash = hqAuthContentHash(authFile);
339
399
  logHqAuthAudit(dataDir, {
@@ -345,8 +405,159 @@ function auditPrunedTokens(scope, previous, next, authFile, dataDir) {
345
405
  });
346
406
  }
347
407
 
408
+ // src/hq-server/login-attempt-store.ts
409
+ import { createHash } from "node:crypto";
410
+ import * as fs from "node:fs/promises";
411
+ import * as path from "node:path";
412
+ import { atomicWrite } from "@wrongstack/core/utils";
413
+ import { restrictFilePermissions } from "@wrongstack/core/security";
414
+ var LoginAttemptStore = class _LoginAttemptStore {
415
+ store = /* @__PURE__ */ new Map();
416
+ writeTimer;
417
+ dirty = false;
418
+ filePath;
419
+ debounceMs;
420
+ retentionMs;
421
+ constructor(dataDir, opts) {
422
+ this.filePath = path.join(dataDir, "login-attempts.json");
423
+ this.debounceMs = opts?.debounceMs ?? 500;
424
+ this.retentionMs = opts?.retentionMs ?? 15 * 6e4;
425
+ }
426
+ /**
427
+ * Load persisted state from disk and prune stale entries.
428
+ * Safe to call once at startup; subsequent calls reload from disk.
429
+ */
430
+ async load() {
431
+ try {
432
+ const raw = await fs.readFile(this.filePath, "utf8");
433
+ const parsed = JSON.parse(raw);
434
+ const cutoff = Date.now() - this.retentionMs;
435
+ for (const [key, entry] of Object.entries(parsed)) {
436
+ if (entry && typeof entry.lastAttempt === "number" && entry.lastAttempt > cutoff) {
437
+ this.store.set(key, entry);
438
+ }
439
+ }
440
+ } catch {
441
+ }
442
+ }
443
+ get(key) {
444
+ return this.store.get(key);
445
+ }
446
+ set(key, entry) {
447
+ this.store.set(key, entry);
448
+ this.scheduleWrite();
449
+ }
450
+ delete(key) {
451
+ if (this.store.delete(key)) {
452
+ this.scheduleWrite();
453
+ }
454
+ }
455
+ /**
456
+ * Compound rate-limit key for public-relay mode: IP + SHA-256(password).
457
+ * Limits how often the SAME password can be tried from different IPs,
458
+ * so a rotating-IP attacker can't bypass per-IP backoff.
459
+ */
460
+ static credentialKey(ip, password) {
461
+ const hash = createHash("sha256").update(password, "utf8").digest("hex").slice(0, 16);
462
+ return `cred:${ip}:${hash}`;
463
+ }
464
+ /**
465
+ * Check both IP and credential keys; return the stricter (later blockedUntil).
466
+ */
467
+ checkBlocked(ip, password) {
468
+ const ipEntry = this.store.get(ip);
469
+ const credKey = _LoginAttemptStore.credentialKey(ip, password);
470
+ const credEntry = this.store.get(credKey);
471
+ const now = Date.now();
472
+ const ipBlocked = ipEntry && ipEntry.blockedUntil > now ? ipEntry.blockedUntil : 0;
473
+ const credBlocked = credEntry && credEntry.blockedUntil > now ? credEntry.blockedUntil : 0;
474
+ const blockedUntil = Math.max(ipBlocked, credBlocked);
475
+ if (blockedUntil > now) {
476
+ return { blocked: true, retryAfter: Math.ceil((blockedUntil - now) / 1e3) };
477
+ }
478
+ return { blocked: false, retryAfter: 0 };
479
+ }
480
+ /**
481
+ * Record a failed attempt on both IP and credential keys.
482
+ * Returns the updated IP entry (for the response Retry-After header).
483
+ */
484
+ recordFailure(ip, password, maxBackoffMs = 16e3) {
485
+ const backoff = (prev) => {
486
+ const count = (prev?.count ?? 0) + 1;
487
+ return {
488
+ count,
489
+ blockedUntil: Date.now() + Math.min(2 ** count * 1e3, maxBackoffMs),
490
+ lastAttempt: Date.now()
491
+ };
492
+ };
493
+ this.set(ip, backoff(this.store.get(ip)));
494
+ const credKey = _LoginAttemptStore.credentialKey(ip, password);
495
+ this.set(credKey, backoff(this.store.get(credKey)));
496
+ return this.store.get(ip);
497
+ }
498
+ /**
499
+ * Clear both IP and credential entries on successful login.
500
+ */
501
+ clearOnSuccess(ip, password) {
502
+ this.delete(ip);
503
+ this.delete(_LoginAttemptStore.credentialKey(ip, password));
504
+ }
505
+ /** Number of tracked entries (for diagnostics). */
506
+ get size() {
507
+ return this.store.size;
508
+ }
509
+ /**
510
+ * Flush pending writes immediately. Called on server shutdown.
511
+ */
512
+ async flush() {
513
+ if (this.writeTimer) {
514
+ clearTimeout(this.writeTimer);
515
+ this.writeTimer = void 0;
516
+ }
517
+ if (this.dirty) {
518
+ await this.writeToDisk();
519
+ }
520
+ }
521
+ scheduleWrite() {
522
+ this.dirty = true;
523
+ if (this.writeTimer) return;
524
+ this.writeTimer = setTimeout(() => {
525
+ this.writeTimer = void 0;
526
+ void this.writeToDisk();
527
+ }, this.debounceMs);
528
+ this.writeTimer.unref?.();
529
+ }
530
+ async writeToDisk() {
531
+ this.dirty = false;
532
+ const now = Date.now();
533
+ const cutoff = now - this.retentionMs;
534
+ const obj = {};
535
+ for (const [key, entry] of this.store) {
536
+ if (entry.lastAttempt > cutoff) {
537
+ obj[key] = entry;
538
+ } else {
539
+ this.store.delete(key);
540
+ }
541
+ }
542
+ try {
543
+ await atomicWrite(this.filePath, JSON.stringify(obj, null, 2), { mode: 384 });
544
+ await restrictFilePermissions(this.filePath, { label: "hq-login-attempts" });
545
+ } catch {
546
+ }
547
+ }
548
+ /** Iterate entries (for cleanup timers). */
549
+ entries() {
550
+ return this.store.entries();
551
+ }
552
+ };
553
+
348
554
  // src/hq-server/preflight.ts
349
- import { assessHqExposure, ensureHqFirstRunAuthFile, resolveHqDataDir } from "@wrongstack/core/hq";
555
+ import {
556
+ assessHqExposure,
557
+ ensureHqFirstRunAuthFile,
558
+ isTokenExpired as isTokenExpired3,
559
+ resolveHqDataDir
560
+ } from "@wrongstack/core/hq";
350
561
  import { HqInsecureExposureError } from "@wrongstack/core/hq";
351
562
  async function prepareHqServerStart(options, defaults) {
352
563
  const host = options.host ?? defaults.host;
@@ -366,18 +577,26 @@ async function prepareHqServerStart(options, defaults) {
366
577
  ...options.tokenTtlMs !== void 0 ? { tokenTtlMs: options.tokenTtlMs } : {}
367
578
  });
368
579
  const { authFile } = firstRunAuth;
369
- if (options.requireBrowserAuth && (authFile.browserTokens ?? []).length === 0 && authFile.passwordHash === void 0) {
580
+ const rawBrowserTokens = authFile.browserTokens ?? [];
581
+ const liveBrowserTokens = rawBrowserTokens.filter((token) => !isTokenExpired3(token));
582
+ const hasPassword = authFile.passwordHash !== void 0;
583
+ const allTokensExpired = rawBrowserTokens.length > 0 && liveBrowserTokens.length === 0;
584
+ if (options.requireBrowserAuth && liveBrowserTokens.length === 0 && !hasPassword) {
370
585
  throw new HqInsecureExposureError(
371
- "HQ public relay requires browser authentication. Set --password or create a browser token first."
586
+ allTokensExpired ? `HQ public relay requires browser authentication, but all ${rawBrowserTokens.length} browser token(s) in the auth file have expired. Create a fresh token or set --password.` : "HQ public relay requires browser authentication. Set --password or create a browser token first."
372
587
  );
373
588
  }
374
589
  const exposure = assessHqExposure({
375
590
  host,
376
- hasBrowserTokens: (authFile.browserTokens ?? []).length > 0,
377
- hasPassword: authFile.passwordHash !== void 0,
591
+ hasBrowserTokens: liveBrowserTokens.length > 0,
592
+ hasPassword,
378
593
  allowInsecure: options.allowInsecureOpen
379
594
  });
380
- if (exposure.kind === "refuse") throw new HqInsecureExposureError(exposure.message);
595
+ if (exposure.kind === "refuse") {
596
+ throw new HqInsecureExposureError(
597
+ allTokensExpired ? `${exposure.message} (all ${rawBrowserTokens.length} browser token(s) in the auth file have expired)` : exposure.message
598
+ );
599
+ }
381
600
  if (exposure.kind === "warn") {
382
601
  console.warn(
383
602
  JSON.stringify({
@@ -394,16 +613,16 @@ async function prepareHqServerStart(options, defaults) {
394
613
 
395
614
  // src/hq-server/routes.ts
396
615
  import { randomUUID as randomUUID2 } from "node:crypto";
397
- import * as path4 from "node:path";
616
+ import * as path5 from "node:path";
398
617
  import { tokenHasCapability, validateHqCommand } from "@wrongstack/core/hq";
399
618
  import {
400
619
  resolveProjectDir
401
620
  } from "@wrongstack/core/coordination";
402
621
 
403
622
  // src/hq-static-serve.ts
404
- import * as fs from "node:fs";
623
+ import * as fs2 from "node:fs";
405
624
  import { createRequire } from "node:module";
406
- import * as path from "node:path";
625
+ import * as path2 from "node:path";
407
626
  var requireFromHere = createRequire(import.meta.url);
408
627
  var MIME = {
409
628
  ".html": "text/html; charset=utf-8",
@@ -423,22 +642,22 @@ function resolveHqDistDir() {
423
642
  if (cachedDistDir !== void 0) return cachedDistDir;
424
643
  try {
425
644
  const pkgJson = requireFromHere.resolve("@wrongstack/webui-hq/package.json");
426
- const dist = path.join(path.dirname(pkgJson), "dist");
427
- if (fs.existsSync(path.join(dist, "index.html"))) {
645
+ const dist = path2.join(path2.dirname(pkgJson), "dist");
646
+ if (fs2.existsSync(path2.join(dist, "index.html"))) {
428
647
  cachedDistDir = dist;
429
648
  return dist;
430
649
  }
431
650
  } catch {
432
651
  }
433
652
  try {
434
- let dir = path.dirname(new URL(import.meta.url).pathname.replace(/^\/(\w:)/, "$1"));
653
+ let dir = path2.dirname(new URL(import.meta.url).pathname.replace(/^\/(\w:)/, "$1"));
435
654
  for (let i = 0; i < 8; i++) {
436
- const candidate = path.join(dir, "node_modules", "@wrongstack", "webui-hq", "dist");
437
- if (fs.existsSync(path.join(candidate, "index.html"))) {
655
+ const candidate = path2.join(dir, "node_modules", "@wrongstack", "webui-hq", "dist");
656
+ if (fs2.existsSync(path2.join(candidate, "index.html"))) {
438
657
  cachedDistDir = candidate;
439
658
  return candidate;
440
659
  }
441
- const parent = path.dirname(dir);
660
+ const parent = path2.dirname(dir);
442
661
  if (parent === dir) break;
443
662
  dir = parent;
444
663
  }
@@ -448,8 +667,8 @@ function resolveHqDistDir() {
448
667
  return null;
449
668
  }
450
669
  function isInsideDist(filePath, distDir) {
451
- const rel = path.relative(distDir, filePath);
452
- return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
670
+ const rel = path2.relative(distDir, filePath);
671
+ return rel !== "" && !rel.startsWith("..") && !path2.isAbsolute(rel);
453
672
  }
454
673
  function isMissingPath(error) {
455
674
  const code = error.code;
@@ -457,9 +676,9 @@ function isMissingPath(error) {
457
676
  }
458
677
  async function readRegularFile(filePath) {
459
678
  try {
460
- const info = await fs.promises.stat(filePath);
679
+ const info = await fs2.promises.stat(filePath);
461
680
  if (!info.isFile()) return null;
462
- return await fs.promises.readFile(filePath);
681
+ return await fs2.promises.readFile(filePath);
463
682
  } catch (error) {
464
683
  if (isMissingPath(error)) return null;
465
684
  throw error;
@@ -473,13 +692,13 @@ async function serveHqStatic(_req, res, urlPath, distDir) {
473
692
  } catch {
474
693
  return { handled: false };
475
694
  }
476
- const filePath = path.join(distDir, decodedPath);
695
+ const filePath = path2.join(distDir, decodedPath);
477
696
  if (!isInsideDist(filePath, distDir)) {
478
697
  return { handled: false };
479
698
  }
480
699
  const data = await readRegularFile(filePath);
481
700
  if (data === null) {
482
- const indexPath = path.join(distDir, "index.html");
701
+ const indexPath = path2.join(distDir, "index.html");
483
702
  const html = await readRegularFile(indexPath);
484
703
  if (html !== null) {
485
704
  res.writeHead(200, {
@@ -492,7 +711,7 @@ async function serveHqStatic(_req, res, urlPath, distDir) {
492
711
  }
493
712
  return { handled: false };
494
713
  }
495
- const ext = path.extname(filePath).toLowerCase();
714
+ const ext = path2.extname(filePath).toLowerCase();
496
715
  const mime = MIME[ext] ?? "application/octet-stream";
497
716
  const cacheControl = decodedPath.startsWith("/assets/") ? "public, max-age=31536000, immutable" : ext === ".html" ? "no-cache" : "public, max-age=3600";
498
717
  res.writeHead(200, {
@@ -508,10 +727,11 @@ async function serveHqStatic(_req, res, urlPath, distDir) {
508
727
  import { WebSocket } from "ws";
509
728
 
510
729
  // src/hq-server/utils.ts
511
- import * as fs2 from "node:fs/promises";
730
+ import * as fs3 from "node:fs/promises";
512
731
  import * as os from "node:os";
513
- import * as path2 from "node:path";
732
+ import * as path3 from "node:path";
514
733
  import { resolveHqDataDir as resolveHqDataDir2 } from "@wrongstack/core/hq";
734
+ import { sanitizeApiError, scrubErrorDetail } from "@wrongstack/core/security";
515
735
  var TRANSCRIPT_RING_MAX = 4e3;
516
736
  var MAX_TRANSCRIPT_SESSIONS = 400;
517
737
  var MAX_AGENT_RINGS = 800;
@@ -540,14 +760,17 @@ async function readLocalSubagentTranscript(sessionId, subagentId) {
540
760
  try {
541
761
  const { SessionRegistry } = await import("@wrongstack/core/storage");
542
762
  const { resolveWstackPaths, sessionScopedPath } = await import("@wrongstack/core/utils");
543
- const globalRoot = path2.dirname(resolveHqDataDir2());
763
+ const globalRoot = path3.dirname(resolveHqDataDir2());
544
764
  const registry = new SessionRegistry(globalRoot);
545
765
  const entry = await registry.get(sessionId).catch(() => null);
546
766
  if (!entry) return null;
547
767
  const paths = resolveWstackPaths({ projectRoot: entry.projectRoot, globalRoot });
548
768
  const sessionDir = sessionScopedPath(paths.projectSessions, sessionId, "");
549
- const file = path2.join(sessionDir, "subagents", "transcripts", subagentId, "transcript.jsonl");
550
- const raw = await fs2.readFile(file, "utf8").catch(() => null);
769
+ if (!isSafePathSegment(subagentId)) return null;
770
+ const transcriptsRoot = path3.resolve(sessionDir, "subagents", "transcripts");
771
+ const file = path3.resolve(transcriptsRoot, subagentId, "transcript.jsonl");
772
+ if (!file.startsWith(transcriptsRoot + path3.sep)) return null;
773
+ const raw = await fs3.readFile(file, "utf8").catch(() => null);
551
774
  if (raw === null) return null;
552
775
  const out = [];
553
776
  for (const line of raw.split("\n")) {
@@ -564,11 +787,38 @@ async function readLocalSubagentTranscript(sessionId, subagentId) {
564
787
  }
565
788
  }
566
789
  function decodePathSegment(value) {
790
+ let decoded;
791
+ try {
792
+ decoded = decodeURIComponent(value);
793
+ } catch {
794
+ return null;
795
+ }
796
+ return isSafePathSegment(decoded) ? decoded : null;
797
+ }
798
+ function decodeSessionId(value) {
799
+ let decoded;
567
800
  try {
568
- return decodeURIComponent(value);
801
+ decoded = decodeURIComponent(value);
569
802
  } catch {
570
803
  return null;
571
804
  }
805
+ return isSafeSessionId(decoded) ? decoded : null;
806
+ }
807
+ var MAX_PATH_SEGMENT_LENGTH = 256;
808
+ function isSafePathSegment(value) {
809
+ if (value.length === 0 || value.length > MAX_PATH_SEGMENT_LENGTH) return false;
810
+ if (value === "." || value === "..") return false;
811
+ if (value.includes("/") || value.includes("\\")) return false;
812
+ if (value.includes("\0")) return false;
813
+ if (value.includes(":")) return false;
814
+ return true;
815
+ }
816
+ function isSafeSessionId(value) {
817
+ if (value.length === 0 || value.length > MAX_PATH_SEGMENT_LENGTH) return false;
818
+ if (value.includes("\\") || value.includes("\0") || value.includes(":")) return false;
819
+ const parts = value.split("/");
820
+ if (parts.length > 2) return false;
821
+ return parts.every((p) => p.length > 0 && p !== "." && p !== "..");
572
822
  }
573
823
  function displayHost(host) {
574
824
  return host === "0.0.0.0" ? "127.0.0.1" : host;
@@ -576,7 +826,7 @@ function displayHost(host) {
576
826
  var RequestBodyTooLargeError = class extends Error {
577
827
  };
578
828
  function readRequestBody(req, maxBytes = 1e6) {
579
- return new Promise((resolve, reject) => {
829
+ return new Promise((resolve2, reject) => {
580
830
  const chunks = [];
581
831
  let size = 0;
582
832
  let settled = false;
@@ -592,7 +842,7 @@ function readRequestBody(req, maxBytes = 1e6) {
592
842
  chunks.push(chunk);
593
843
  });
594
844
  req.on("end", () => {
595
- if (!settled) resolve(Buffer.concat(chunks).toString("utf8"));
845
+ if (!settled) resolve2(Buffer.concat(chunks).toString("utf8"));
596
846
  });
597
847
  req.on("error", (error) => {
598
848
  if (!settled) reject(error);
@@ -620,7 +870,7 @@ function buildClientWsUrl(host, port, token) {
620
870
  return url.toString();
621
871
  }
622
872
  function hqRuntimeMarkerPath(dataDir) {
623
- return path2.join(dataDir, "runtime.json");
873
+ return path3.join(dataDir, "runtime.json");
624
874
  }
625
875
  function lanIPv4Addresses() {
626
876
  const out = [];
@@ -667,20 +917,6 @@ function recordTimeseriesSignal(persistence, event) {
667
917
  } catch {
668
918
  }
669
919
  }
670
- function sanitizeApiError(err) {
671
- const message = err instanceof Error ? err.message : String(err);
672
- const lower = message.toLowerCase();
673
- if (lower.includes("enoent") || lower.includes("no such file")) {
674
- return "resource not found";
675
- }
676
- if (lower.includes("eacces") || lower.includes("permission")) {
677
- return "permission denied";
678
- }
679
- if (lower.includes("json") && (lower.includes("parse") || lower.includes("unexpected"))) {
680
- return "malformed data";
681
- }
682
- return "internal error";
683
- }
684
920
 
685
921
  // src/hq-server/snapshot.ts
686
922
  var HQ_SNAPSHOT_BROADCAST_DEBOUNCE_MS = 100;
@@ -719,6 +955,9 @@ function reapStaleClientState(clients, nowMs = Date.now()) {
719
955
  for (const [sessionId, tracked] of client.mcpSnapshots) {
720
956
  if (nowMs - tracked.receivedAt > HQ_STALE_SNAPSHOT_MS) client.mcpSnapshots.delete(sessionId);
721
957
  }
958
+ if (client.governanceSnapshot !== void 0 && nowMs - client.governanceSnapshot.receivedAt > HQ_STALE_SNAPSHOT_MS) {
959
+ delete client.governanceSnapshot;
960
+ }
722
961
  }
723
962
  }
724
963
  function buildSnapshot(clients, options) {
@@ -765,6 +1004,10 @@ function buildSnapshot(clients, options) {
765
1004
  } else if (machineId && !project.machineIds.includes(machineId)) {
766
1005
  project.machineIds = [...project.machineIds, machineId];
767
1006
  }
1007
+ const governance = client.governanceSnapshot?.payload;
1008
+ if (governance !== void 0 && (project.governance === void 0 || Date.parse(governance.observedAt) >= Date.parse(project.governance.observedAt))) {
1009
+ project.governance = governance;
1010
+ }
768
1011
  for (const tracked of client.sessions.values()) {
769
1012
  const cutoff = Date.now() - 5 * 6e4;
770
1013
  const agents = tracked.payload.agents.filter((agent) => {
@@ -1087,7 +1330,9 @@ function broadcastCommandStatus(command, browsers) {
1087
1330
  }
1088
1331
 
1089
1332
  // src/hq-server/trust-boundary.ts
1090
- import { isTrustDecisionAllowed } from "@wrongstack/core/security";
1333
+ import {
1334
+ isTrustDecisionAllowed
1335
+ } from "@wrongstack/core/security";
1091
1336
  async function authorizeHqCommand(input) {
1092
1337
  const { boundary, command, commandId, enqueuedBy, authMethod, target } = input;
1093
1338
  const subject = command.type === "run-command" ? {
@@ -1102,11 +1347,29 @@ async function authorizeHqCommand(input) {
1102
1347
  const request = {
1103
1348
  version: 1,
1104
1349
  requestId: commandId,
1105
- // This request is initiated by the authenticated HQ operator. Browser and
1106
- // target-client capability checks have already passed before this adapter
1107
- // is called, so classifying it as an unauthenticated remote client would
1108
- // make the compatibility policy deny every legitimate run-command.
1109
- actor: { kind: "user", id: enqueuedBy },
1350
+ // WS-050: this was hardcoded to `kind: 'user'`, which made the
1351
+ // compatibility policy's ONLY deny rule `remote-client` + `critical` —
1352
+ // unreachable from HQ. A boundary whose single rule can never fire is not
1353
+ // defence in depth, it is decoration.
1354
+ //
1355
+ // The provenance is derived from how the caller authenticated, because
1356
+ // that is the honest signal available here:
1357
+ //
1358
+ // session cookie → `user`. The dashboard's primary flow is a bootstrap
1359
+ // code exchanged for an HttpOnly cookie, so this is a human at the
1360
+ // browser. NOTE: bootstrap sessions DO carry a `tokenId`, so a
1361
+ // "cookie without tokenId" test would have misclassified every normal
1362
+ // dashboard user as remote and denied them run-command. Checked before
1363
+ // writing this, not assumed.
1364
+ // raw bearer token → `remote-client`. The legacy `?token=` / manual-entry
1365
+ // path and any script calling the HTTP API directly. A credential that
1366
+ // can be copied into a script is not a person at a console.
1367
+ //
1368
+ // Consequence, deliberate: a bearer-token caller can no longer trigger
1369
+ // `run-command` (the only `critical` capability here). `enqueue` is `high`
1370
+ // and unaffected. The migration for automation that needs execute is the
1371
+ // bootstrap exchange, which is the same thing the dashboard does.
1372
+ actor: { kind: authMethod === "session" ? "user" : "remote-client", id: enqueuedBy },
1110
1373
  surface: "hq",
1111
1374
  capability: command.type === "run-command" ? "hq.control.execute" : "hq.control.enqueue",
1112
1375
  subject,
@@ -1137,18 +1400,29 @@ import { randomUUID } from "node:crypto";
1137
1400
  import {
1138
1401
  DEFAULT_HQ_REDACTION_POLICY as DEFAULT_HQ_REDACTION_POLICY2,
1139
1402
  hashHqPassword,
1140
- isLoopbackHost,
1403
+ hqTokenKey as hqTokenKey2,
1404
+ isLoopbackHost as isLoopbackHost2,
1405
+ isTokenExpired as isTokenExpired4,
1141
1406
  mintHqCookieSecret,
1142
1407
  mutateHqAuthFile,
1143
1408
  verifyHqPassword
1144
1409
  } from "@wrongstack/core/hq";
1410
+ import {
1411
+ buildOtpAuthUri,
1412
+ generateRecoveryCodes,
1413
+ generateTotpSecret,
1414
+ hashRecoveryCode,
1415
+ verifyRecoveryCode,
1416
+ verifyTotp
1417
+ } from "@wrongstack/core/security";
1418
+ import { readHqAuthAuditTail } from "@wrongstack/core/hq";
1145
1419
  function isLoopbackRequest(req) {
1146
1420
  const address = req.socket.remoteAddress?.replace(/^::ffff:/, "");
1147
- return address !== void 0 && isLoopbackHost(address);
1421
+ return address !== void 0 && isLoopbackHost2(address);
1148
1422
  }
1149
1423
  async function handleApiAuthStatus(req, res, url, mutableAuth, sessions, requireBrowserAuth, trustedPublicOrigins, secureCookies) {
1150
1424
  const auth = authenticateBrowserRequest(req, url, mutableAuth, sessions);
1151
- const openMode = mutableAuth.browserTokens.size === 0 && mutableAuth.passwordHash === void 0;
1425
+ const openMode = mutableAuth.browserTokens.size === 0 && mutableAuth.passwordHash === void 0 && mutableAuth.requireAuthFloor !== true;
1152
1426
  const localOpenMode = openMode && !requireBrowserAuth && isLoopbackRequest(req);
1153
1427
  const publicOrigin = trustedPublicOrigins.values().next().value;
1154
1428
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -1156,6 +1430,8 @@ async function handleApiAuthStatus(req, res, url, mutableAuth, sessions, require
1156
1430
  JSON.stringify({
1157
1431
  tokenMode: mutableAuth.browserTokens.size > 0,
1158
1432
  passwordMode: mutableAuth.passwordHash !== void 0,
1433
+ totpEnabled: mutableAuth.totpSecret !== void 0,
1434
+ recoveryCodesRemaining: mutableAuth.totpRecoveryCodes?.length ?? 0,
1159
1435
  publicRelay: requireBrowserAuth === true || publicOrigin !== void 0,
1160
1436
  ...publicOrigin !== void 0 ? { publicOrigin } : {},
1161
1437
  secureCookies: secureCookies === true,
@@ -1178,9 +1454,8 @@ async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, se
1178
1454
  return;
1179
1455
  }
1180
1456
  const clientIp = req.socket.remoteAddress ?? "unknown";
1181
- const existing = loginAttempts.get(clientIp);
1182
- if (existing && existing.blockedUntil > Date.now()) {
1183
- const retryAfter = Math.ceil((existing.blockedUntil - Date.now()) / 1e3);
1457
+ const { blocked, retryAfter } = loginAttempts.checkBlocked(clientIp, "");
1458
+ if (blocked) {
1184
1459
  res.writeHead(429, {
1185
1460
  "Content-Type": "application/json",
1186
1461
  "Retry-After": String(retryAfter)
@@ -1204,30 +1479,36 @@ async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, se
1204
1479
  }
1205
1480
  if (typeof body.password !== "string" || body.password.length === 0) {
1206
1481
  res.writeHead(400, { "Content-Type": "application/json" });
1207
- res.end(
1208
- JSON.stringify({ error: { code: "BAD_REQUEST", message: "password is required" } })
1209
- );
1482
+ res.end(JSON.stringify({ error: { code: "BAD_REQUEST", message: "password is required" } }));
1210
1483
  return;
1211
1484
  }
1212
1485
  const ok = await verifyHqPassword(body.password, mutableAuth.passwordHash);
1213
1486
  if (!ok || !mutableAuth.cookieSecret) {
1214
- const prev = loginAttempts.get(clientIp);
1215
- const count = (prev?.count ?? 0) + 1;
1216
- const backoffMs = Math.min(2 ** count * 1e3, 16e3);
1217
- loginAttempts.set(clientIp, {
1218
- count,
1219
- blockedUntil: Date.now() + backoffMs,
1220
- lastAttempt: Date.now()
1221
- });
1487
+ loginAttempts.recordFailure(clientIp, body.password);
1222
1488
  res.writeHead(401, { "Content-Type": "application/json" });
1223
- res.end(
1224
- JSON.stringify({ error: { code: "INVALID_PASSWORD", message: "Invalid password." } })
1489
+ res.end(JSON.stringify({ error: { code: "INVALID_PASSWORD", message: "Invalid password." } }));
1490
+ return;
1491
+ }
1492
+ if (mutableAuth.totpSecret) {
1493
+ const pendingSessionId = randomUUID();
1494
+ sessions.set(pendingSessionId, {
1495
+ createdAt: Date.now(),
1496
+ kind: "password",
1497
+ pending2fa: true,
1498
+ lastSeenAt: Date.now()
1499
+ });
1500
+ setHqSessionCookie(
1501
+ res,
1502
+ serializeHqSessionCookie(pendingSessionId, mutableAuth.cookieSecret),
1503
+ secureCookies
1225
1504
  );
1505
+ res.writeHead(200, { "Content-Type": "application/json" });
1506
+ res.end(JSON.stringify({ loggedIn: false, totpRequired: true }));
1226
1507
  return;
1227
1508
  }
1228
- loginAttempts.delete(clientIp);
1509
+ loginAttempts.clearOnSuccess(clientIp, body.password);
1229
1510
  const sessionId = randomUUID();
1230
- sessions.set(sessionId, { createdAt: Date.now(), kind: "password" });
1511
+ sessions.set(sessionId, { createdAt: Date.now(), kind: "password", lastSeenAt: Date.now() });
1231
1512
  setHqSessionCookie(
1232
1513
  res,
1233
1514
  serializeHqSessionCookie(sessionId, mutableAuth.cookieSecret),
@@ -1245,7 +1526,7 @@ async function handleApiLogout(req, res, mutableAuth, sessions, secureCookies) {
1245
1526
  );
1246
1527
  if (isCookieAuth(auth)) {
1247
1528
  const cookies = parseCookieHeader(req.headers.cookie);
1248
- const raw = cookies[HQ_SESSION_COOKIE];
1529
+ const raw = cookies[HQ_SESSION_COOKIE] ?? cookies["__Host-hq.session"];
1249
1530
  if (raw) {
1250
1531
  const sessionId = parseHqSessionCookie(raw, mutableAuth.cookieSecret ?? "");
1251
1532
  if (sessionId) sessions.delete(sessionId);
@@ -1275,23 +1556,34 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
1275
1556
  );
1276
1557
  return;
1277
1558
  }
1559
+ if (localOpenBootstrap) {
1560
+ console.warn(
1561
+ JSON.stringify({
1562
+ level: "warn",
1563
+ event: "hq.auth.password_claimed_unauthenticated",
1564
+ message: "An unauthenticated loopback request set the HQ password on an open instance. If this was not you, stop HQ and remove passwordHash from auth.json.",
1565
+ remoteAddress: req.socket.remoteAddress ?? "unknown",
1566
+ dataDir,
1567
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1568
+ })
1569
+ );
1570
+ }
1278
1571
  let body = {};
1279
1572
  try {
1280
1573
  body = JSON.parse(await readRequestBody(req));
1281
1574
  } catch {
1282
1575
  res.writeHead(400, { "Content-Type": "application/json" });
1283
- res.end(
1284
- JSON.stringify({ error: { code: "BAD_REQUEST", message: "Invalid JSON body." } })
1285
- );
1576
+ res.end(JSON.stringify({ error: { code: "BAD_REQUEST", message: "Invalid JSON body." } }));
1286
1577
  return;
1287
1578
  }
1288
- if (isCookieAuth(auth) && mutableAuth.passwordHash !== void 0) {
1579
+ const hasAdminCapability = auth !== void 0 && "capabilities" in auth && auth.capabilities !== void 0 && auth.capabilities.includes("auth.admin");
1580
+ if (!localOpenBootstrap && mutableAuth.passwordHash !== void 0 && !hasAdminCapability) {
1289
1581
  const currentPassword = typeof body.currentPassword === "string" ? body.currentPassword : "";
1290
1582
  if (!currentPassword || !await verifyHqPassword(currentPassword, mutableAuth.passwordHash)) {
1291
1583
  res.writeHead(403, { "Content-Type": "application/json" });
1292
1584
  res.end(
1293
1585
  JSON.stringify({
1294
- error: { code: "INVALID_CURRENT_PASSWORD", message: "Current password is invalid." }
1586
+ error: { code: "INVALID_CURRENT_PASSWORD", message: "Current password is required to change or remove it." }
1295
1587
  })
1296
1588
  );
1297
1589
  return;
@@ -1314,6 +1606,9 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
1314
1606
  const updated = { ...current };
1315
1607
  delete updated.passwordHash;
1316
1608
  delete updated.cookieSecret;
1609
+ delete updated.totpSecret;
1610
+ delete updated.totpPendingSecret;
1611
+ delete updated.totpRecoveryCodes;
1317
1612
  return updated;
1318
1613
  });
1319
1614
  applyAuthFile(mutableAuth, next2);
@@ -1334,7 +1629,10 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
1334
1629
  res.writeHead(400, { "Content-Type": "application/json" });
1335
1630
  res.end(
1336
1631
  JSON.stringify({
1337
- error: { code: "INVALID_PASSWORD", message: "New password must be between 8 and 1024 characters." }
1632
+ error: {
1633
+ code: "INVALID_PASSWORD",
1634
+ message: "New password must be between 8 and 1024 characters."
1635
+ }
1338
1636
  })
1339
1637
  );
1340
1638
  return;
@@ -1350,12 +1648,8 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
1350
1648
  sessions.clear();
1351
1649
  if (isCookieAuth(auth) || localOpenBootstrap) {
1352
1650
  const sessionId = randomUUID();
1353
- sessions.set(sessionId, { createdAt: Date.now(), kind: "password" });
1354
- setHqSessionCookie(
1355
- res,
1356
- serializeHqSessionCookie(sessionId, cookieSecret),
1357
- secureCookies
1358
- );
1651
+ sessions.set(sessionId, { createdAt: Date.now(), kind: "password", lastSeenAt: Date.now() });
1652
+ setHqSessionCookie(res, serializeHqSessionCookie(sessionId, cookieSecret), secureCookies);
1359
1653
  }
1360
1654
  res.writeHead(200, { "Content-Type": "application/json" });
1361
1655
  res.end(
@@ -1372,20 +1666,28 @@ function applyAuthFile(mutableAuth, next) {
1372
1666
  ...next.redactionPolicy ?? {}
1373
1667
  };
1374
1668
  mutableAuth.operatorPolicyOverride = next.redactionPolicy;
1375
- mutableAuth.browserTokens = new Set((next.browserTokens ?? []).map((t) => t.token));
1376
- mutableAuth.clientTokens = new Set((next.clientTokens ?? []).map((t) => t.token));
1669
+ const liveBrowserTokens = (next.browserTokens ?? []).filter((t) => !isTokenExpired4(t));
1670
+ mutableAuth.browserTokens = new Set(liveBrowserTokens.map(hqTokenKey2));
1671
+ mutableAuth.clientTokens = new Set((next.clientTokens ?? []).map(hqTokenKey2));
1377
1672
  mutableAuth.browserTokenObjs = new Map(
1378
- (next.browserTokens ?? []).map((t) => [
1379
- t.token,
1380
- { id: t.id, ...t.capabilities !== void 0 ? { capabilities: t.capabilities } : {} }
1673
+ liveBrowserTokens.map((t) => [
1674
+ hqTokenKey2(t),
1675
+ {
1676
+ id: t.id,
1677
+ ...t.capabilities !== void 0 ? { capabilities: t.capabilities } : {},
1678
+ ...t.expiresAt !== void 0 ? { expiresAt: t.expiresAt } : {}
1679
+ }
1381
1680
  ])
1382
1681
  );
1383
1682
  mutableAuth.clientTokenObjs = new Map(
1384
- (next.clientTokens ?? []).map((token) => [token.token, token])
1683
+ (next.clientTokens ?? []).map((token) => [hqTokenKey2(token), token])
1385
1684
  );
1386
1685
  mutableAuth.passwordHash = next.passwordHash;
1387
1686
  mutableAuth.cookieSecret = next.cookieSecret;
1388
1687
  mutableAuth.alertRules = next.alertRules;
1688
+ mutableAuth.totpSecret = next.totpSecret;
1689
+ mutableAuth.totpPendingSecret = next.totpPendingSecret;
1690
+ mutableAuth.totpRecoveryCodes = next.totpRecoveryCodes;
1389
1691
  }
1390
1692
  async function handleApiBootstrap(req, res, mutableAuth, sessions, secureCookies, bootstrapStore) {
1391
1693
  if (!mutableAuth.cookieSecret) {
@@ -1409,9 +1711,7 @@ async function handleApiBootstrap(req, res, mutableAuth, sessions, secureCookies
1409
1711
  }
1410
1712
  if (typeof body.code !== "string" || body.code.length === 0) {
1411
1713
  res.writeHead(400, { "Content-Type": "application/json" });
1412
- res.end(
1413
- JSON.stringify({ error: { code: "BAD_REQUEST", message: "code is required" } })
1414
- );
1714
+ res.end(JSON.stringify({ error: { code: "BAD_REQUEST", message: "code is required" } }));
1415
1715
  return;
1416
1716
  }
1417
1717
  const entry = bootstrapStore.consume(body.code);
@@ -1445,6 +1745,7 @@ async function handleApiBootstrap(req, res, mutableAuth, sessions, secureCookies
1445
1745
  const sessionId = randomUUID();
1446
1746
  sessions.set(sessionId, {
1447
1747
  createdAt: Date.now(),
1748
+ lastSeenAt: Date.now(),
1448
1749
  kind: "token",
1449
1750
  tokenId: entry.tokenId,
1450
1751
  ...entry.capabilities !== void 0 ? { capabilities: entry.capabilities } : {}
@@ -1457,12 +1758,406 @@ async function handleApiBootstrap(req, res, mutableAuth, sessions, secureCookies
1457
1758
  res.writeHead(200, { "Content-Type": "application/json" });
1458
1759
  res.end(JSON.stringify({ loggedIn: true }));
1459
1760
  }
1761
+ async function handleApiTokenUpgrade(req, res, url, mutableAuth, sessions, secureCookies) {
1762
+ if (!mutableAuth.cookieSecret) {
1763
+ res.writeHead(503, { "Content-Type": "application/json" });
1764
+ res.end(
1765
+ JSON.stringify({
1766
+ error: {
1767
+ code: "UPGRADE_UNAVAILABLE",
1768
+ message: "Cookie signing is not configured on this HQ server."
1769
+ }
1770
+ })
1771
+ );
1772
+ return;
1773
+ }
1774
+ const auth = authenticateBrowserRequest(req, url, mutableAuth, sessions);
1775
+ if (isCookieAuth(auth)) {
1776
+ res.writeHead(200, { "Content-Type": "application/json" });
1777
+ res.end(JSON.stringify({ loggedIn: true, upgraded: false }));
1778
+ return;
1779
+ }
1780
+ if (!isTokenAuth(auth)) {
1781
+ res.writeHead(401, { "Content-Type": "application/json" });
1782
+ res.end(
1783
+ JSON.stringify({
1784
+ error: { code: "UNAUTHORIZED", message: "A valid browser token is required." }
1785
+ })
1786
+ );
1787
+ return;
1788
+ }
1789
+ const tokenLive = [...mutableAuth.browserTokenObjs.values()].some((obj) => obj.id === auth.id);
1790
+ if (!tokenLive) {
1791
+ res.writeHead(409, { "Content-Type": "application/json" });
1792
+ res.end(
1793
+ JSON.stringify({
1794
+ error: {
1795
+ code: "TOKEN_NOT_UPGRADEABLE",
1796
+ message: "This token has no server-side record to bind a session to."
1797
+ }
1798
+ })
1799
+ );
1800
+ return;
1801
+ }
1802
+ const sessionId = randomUUID();
1803
+ sessions.set(sessionId, {
1804
+ createdAt: Date.now(),
1805
+ lastSeenAt: Date.now(),
1806
+ kind: "token",
1807
+ tokenId: auth.id,
1808
+ ...auth.capabilities !== void 0 ? { capabilities: auth.capabilities } : {}
1809
+ });
1810
+ setHqSessionCookie(
1811
+ res,
1812
+ serializeHqSessionCookie(sessionId, mutableAuth.cookieSecret),
1813
+ secureCookies
1814
+ );
1815
+ res.writeHead(200, { "Content-Type": "application/json" });
1816
+ res.end(JSON.stringify({ loggedIn: true, upgraded: true }));
1817
+ }
1818
+ var PENDING_2FA_TTL_MS = 5 * 6e4;
1819
+ var recoveryCodeLock = Promise.resolve();
1820
+ var MAX_2FA_VERIFY_FAILURES = 5;
1821
+ function recordVerifyFailure(loginAttempts, clientIp) {
1822
+ const prev = loginAttempts.get(clientIp);
1823
+ const count = (prev?.count ?? 0) + 1;
1824
+ loginAttempts.recordFailure(clientIp, "");
1825
+ return count;
1826
+ }
1827
+ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, loginAttempts, dataDir, secureCookies) {
1828
+ if (!mutableAuth.cookieSecret) {
1829
+ res.writeHead(503, { "Content-Type": "application/json" });
1830
+ res.end(JSON.stringify({ error: { code: "NO_COOKIE_SECRET" } }));
1831
+ return;
1832
+ }
1833
+ const clientIp = req.socket.remoteAddress ?? "unknown";
1834
+ const { blocked, retryAfter } = loginAttempts.checkBlocked(clientIp, "");
1835
+ if (blocked) {
1836
+ res.writeHead(429, { "Content-Type": "application/json", "Retry-After": String(retryAfter) });
1837
+ res.end(JSON.stringify({ error: { code: "RATE_LIMITED", message: "Too many attempts. Try again later." } }));
1838
+ return;
1839
+ }
1840
+ const cookies = parseCookieHeader(req.headers.cookie);
1841
+ const raw = cookies[HQ_SESSION_COOKIE] ?? cookies["__Host-hq.session"];
1842
+ const sessionId = raw ? parseHqSessionCookie(raw, mutableAuth.cookieSecret) : void 0;
1843
+ if (!sessionId) {
1844
+ res.writeHead(401, { "Content-Type": "application/json" });
1845
+ res.end(JSON.stringify({ error: { code: "NO_PENDING_SESSION", message: "No pending 2FA session." } }));
1846
+ return;
1847
+ }
1848
+ const session = sessions.get(sessionId);
1849
+ if (!session || !session.pending2fa) {
1850
+ res.writeHead(401, { "Content-Type": "application/json" });
1851
+ res.end(JSON.stringify({ error: { code: "NO_PENDING_SESSION", message: "Session is not pending 2FA." } }));
1852
+ return;
1853
+ }
1854
+ if (Date.now() - session.createdAt > PENDING_2FA_TTL_MS) {
1855
+ sessions.delete(sessionId);
1856
+ res.writeHead(401, { "Content-Type": "application/json" });
1857
+ res.end(JSON.stringify({ error: { code: "TOTP_EXPIRED", message: "2FA session expired. Please log in again." } }));
1858
+ return;
1859
+ }
1860
+ let body;
1861
+ try {
1862
+ body = JSON.parse(await readRequestBody(req));
1863
+ } catch {
1864
+ res.writeHead(400, { "Content-Type": "application/json" });
1865
+ res.end(JSON.stringify({ error: { code: "BAD_REQUEST", message: "Invalid JSON body." } }));
1866
+ return;
1867
+ }
1868
+ if (typeof body.code === "string" && body.code.length > 0) {
1869
+ if (!mutableAuth.totpSecret) {
1870
+ res.writeHead(400, { "Content-Type": "application/json" });
1871
+ res.end(JSON.stringify({ error: { code: "TOTP_NOT_ENABLED", message: "2FA is not configured." } }));
1872
+ return;
1873
+ }
1874
+ if (!verifyTotp(body.code, mutableAuth.totpSecret)) {
1875
+ if (recordVerifyFailure(loginAttempts, clientIp) >= MAX_2FA_VERIFY_FAILURES) {
1876
+ sessions.delete(sessionId);
1877
+ }
1878
+ res.writeHead(401, { "Content-Type": "application/json" });
1879
+ res.end(JSON.stringify({ error: { code: "INVALID_TOTP", message: "Invalid authenticator code." } }));
1880
+ return;
1881
+ }
1882
+ loginAttempts.clearOnSuccess(clientIp, "");
1883
+ sessions.delete(sessionId);
1884
+ const fullSessionId = randomUUID();
1885
+ sessions.set(fullSessionId, { createdAt: Date.now(), kind: "password", lastSeenAt: Date.now() });
1886
+ setHqSessionCookie(
1887
+ res,
1888
+ serializeHqSessionCookie(fullSessionId, mutableAuth.cookieSecret),
1889
+ secureCookies
1890
+ );
1891
+ res.writeHead(200, { "Content-Type": "application/json" });
1892
+ res.end(JSON.stringify({ loggedIn: true }));
1893
+ return;
1894
+ }
1895
+ if (typeof body.recoveryCode === "string" && body.recoveryCode.length > 0) {
1896
+ const usedHash = hashRecoveryCode(body.recoveryCode);
1897
+ const storedHashes = mutableAuth.totpRecoveryCodes ?? [];
1898
+ if (storedHashes.length === 0) {
1899
+ res.writeHead(400, { "Content-Type": "application/json" });
1900
+ res.end(JSON.stringify({ error: { code: "NO_RECOVERY_CODES", message: "No recovery codes available." } }));
1901
+ return;
1902
+ }
1903
+ if (!verifyRecoveryCode(body.recoveryCode, storedHashes)) {
1904
+ if (recordVerifyFailure(loginAttempts, clientIp) >= MAX_2FA_VERIFY_FAILURES) {
1905
+ sessions.delete(sessionId);
1906
+ }
1907
+ res.writeHead(401, { "Content-Type": "application/json" });
1908
+ res.end(JSON.stringify({ error: { code: "INVALID_RECOVERY_CODE", message: "Invalid recovery code." } }));
1909
+ return;
1910
+ }
1911
+ let consumed = false;
1912
+ let remainingCount = 0;
1913
+ await recoveryCodeLock;
1914
+ let releaseLock = () => {
1915
+ };
1916
+ recoveryCodeLock = new Promise((resolve2) => {
1917
+ releaseLock = resolve2;
1918
+ });
1919
+ try {
1920
+ const next = await mutateHqAuthFile(dataDir, (current) => {
1921
+ const diskHashes = current.totpRecoveryCodes ?? [];
1922
+ const idx = diskHashes.indexOf(usedHash);
1923
+ if (idx === -1) {
1924
+ consumed = false;
1925
+ remainingCount = diskHashes.length;
1926
+ return current;
1927
+ }
1928
+ consumed = true;
1929
+ const remaining = diskHashes.filter((_, i) => i !== idx);
1930
+ remainingCount = remaining.length;
1931
+ return { ...current, totpRecoveryCodes: remaining };
1932
+ });
1933
+ if (!consumed) {
1934
+ res.writeHead(409, { "Content-Type": "application/json" });
1935
+ res.end(JSON.stringify({ error: { code: "RECOVERY_CODE_ALREADY_USED", message: "This recovery code has already been used." } }));
1936
+ return;
1937
+ }
1938
+ applyAuthFile(mutableAuth, next);
1939
+ } finally {
1940
+ releaseLock();
1941
+ }
1942
+ loginAttempts.clearOnSuccess(clientIp, "");
1943
+ sessions.delete(sessionId);
1944
+ const fullSessionId = randomUUID();
1945
+ sessions.set(fullSessionId, { createdAt: Date.now(), kind: "password", lastSeenAt: Date.now() });
1946
+ setHqSessionCookie(
1947
+ res,
1948
+ serializeHqSessionCookie(fullSessionId, mutableAuth.cookieSecret),
1949
+ secureCookies
1950
+ );
1951
+ res.writeHead(200, { "Content-Type": "application/json" });
1952
+ res.end(JSON.stringify({ loggedIn: true, recoveryCodesRemaining: remainingCount }));
1953
+ return;
1954
+ }
1955
+ res.writeHead(400, { "Content-Type": "application/json" });
1956
+ res.end(
1957
+ JSON.stringify({ error: { code: "BAD_REQUEST", message: "Provide a `code` or `recoveryCode`." } })
1958
+ );
1959
+ }
1960
+ async function handleApiTotpSetup(req, res, mutableAuth, sessions, dataDir) {
1961
+ const auth = authenticateBrowserRequest(
1962
+ req,
1963
+ new URL(req.url ?? "/", "http://localhost"),
1964
+ mutableAuth,
1965
+ sessions
1966
+ );
1967
+ if (!auth) {
1968
+ res.writeHead(401, { "Content-Type": "application/json" });
1969
+ res.end(JSON.stringify({ error: { code: "UNAUTHORIZED", message: "Authentication required." } }));
1970
+ return;
1971
+ }
1972
+ if (mutableAuth.totpSecret) {
1973
+ res.writeHead(409, { "Content-Type": "application/json" });
1974
+ res.end(
1975
+ JSON.stringify({ error: { code: "TOTP_ALREADY_ENABLED", message: "2FA is already active. Disable it first to re-enroll." } })
1976
+ );
1977
+ return;
1978
+ }
1979
+ const secret = generateTotpSecret();
1980
+ const uri = buildOtpAuthUri(secret, "HQ");
1981
+ const next = await mutateHqAuthFile(dataDir, (current) => ({
1982
+ ...current,
1983
+ totpPendingSecret: secret
1984
+ }));
1985
+ applyAuthFile(mutableAuth, next);
1986
+ res.writeHead(200, { "Content-Type": "application/json" });
1987
+ res.end(JSON.stringify({ secret, uri, enabled: false }));
1988
+ }
1989
+ async function handleApiTotpEnable(req, res, mutableAuth, sessions, dataDir) {
1990
+ const auth = authenticateBrowserRequest(
1991
+ req,
1992
+ new URL(req.url ?? "/", "http://localhost"),
1993
+ mutableAuth,
1994
+ sessions
1995
+ );
1996
+ if (!auth) {
1997
+ res.writeHead(401, { "Content-Type": "application/json" });
1998
+ res.end(JSON.stringify({ error: { code: "UNAUTHORIZED", message: "Authentication required." } }));
1999
+ return;
2000
+ }
2001
+ if (mutableAuth.totpSecret) {
2002
+ res.writeHead(409, { "Content-Type": "application/json" });
2003
+ res.end(
2004
+ JSON.stringify({ error: { code: "TOTP_ALREADY_ENABLED", message: "2FA is already active." } })
2005
+ );
2006
+ return;
2007
+ }
2008
+ const pendingSecret = mutableAuth.totpPendingSecret;
2009
+ if (!pendingSecret) {
2010
+ res.writeHead(400, { "Content-Type": "application/json" });
2011
+ res.end(
2012
+ JSON.stringify({ error: { code: "TOTP_NOT_SETUP", message: "Call /api/auth/totp/setup first." } })
2013
+ );
2014
+ return;
2015
+ }
2016
+ let body;
2017
+ try {
2018
+ body = JSON.parse(await readRequestBody(req));
2019
+ } catch {
2020
+ res.writeHead(400, { "Content-Type": "application/json" });
2021
+ res.end(JSON.stringify({ error: { code: "BAD_REQUEST", message: "Invalid JSON body." } }));
2022
+ return;
2023
+ }
2024
+ if (typeof body.code !== "string" || body.code.length === 0) {
2025
+ res.writeHead(400, { "Content-Type": "application/json" });
2026
+ res.end(JSON.stringify({ error: { code: "BAD_REQUEST", message: "code is required." } }));
2027
+ return;
2028
+ }
2029
+ if (!verifyTotp(body.code, pendingSecret)) {
2030
+ res.writeHead(401, { "Content-Type": "application/json" });
2031
+ res.end(JSON.stringify({ error: { code: "INVALID_TOTP", message: "Invalid authenticator code." } }));
2032
+ return;
2033
+ }
2034
+ const recoveryCodes = generateRecoveryCodes();
2035
+ const recoveryHashes = recoveryCodes.map(hashRecoveryCode);
2036
+ const next = await mutateHqAuthFile(dataDir, (current) => {
2037
+ const updated = { ...current };
2038
+ updated.totpSecret = pendingSecret;
2039
+ delete updated.totpPendingSecret;
2040
+ updated.totpRecoveryCodes = recoveryHashes;
2041
+ return updated;
2042
+ });
2043
+ applyAuthFile(mutableAuth, next);
2044
+ sessions.clear();
2045
+ res.writeHead(200, { "Content-Type": "application/json" });
2046
+ res.end(JSON.stringify({ enabled: true, recoveryCodes }));
2047
+ }
2048
+ async function handleApiTotpDisable(req, res, mutableAuth, sessions, loginAttempts, dataDir) {
2049
+ const auth = authenticateBrowserRequest(
2050
+ req,
2051
+ new URL(req.url ?? "/", "http://localhost"),
2052
+ mutableAuth,
2053
+ sessions
2054
+ );
2055
+ if (!auth) {
2056
+ res.writeHead(401, { "Content-Type": "application/json" });
2057
+ res.end(JSON.stringify({ error: { code: "UNAUTHORIZED", message: "Authentication required." } }));
2058
+ return;
2059
+ }
2060
+ const clientIp = req.socket.remoteAddress ?? "unknown";
2061
+ const { blocked, retryAfter } = loginAttempts.checkBlocked(clientIp, "");
2062
+ if (blocked) {
2063
+ res.writeHead(429, { "Content-Type": "application/json", "Retry-After": String(retryAfter) });
2064
+ res.end(JSON.stringify({ error: { code: "RATE_LIMITED", message: "Too many attempts. Try again later." } }));
2065
+ return;
2066
+ }
2067
+ let body;
2068
+ try {
2069
+ body = JSON.parse(await readRequestBody(req));
2070
+ } catch {
2071
+ res.writeHead(400, { "Content-Type": "application/json" });
2072
+ res.end(JSON.stringify({ error: { code: "BAD_REQUEST", message: "Invalid JSON body." } }));
2073
+ return;
2074
+ }
2075
+ let confirmed = false;
2076
+ if (typeof body.password === "string" && body.password.length > 0 && mutableAuth.passwordHash) {
2077
+ confirmed = await verifyHqPassword(body.password, mutableAuth.passwordHash);
2078
+ }
2079
+ if (!confirmed && typeof body.code === "string" && mutableAuth.totpSecret) {
2080
+ confirmed = verifyTotp(body.code, mutableAuth.totpSecret);
2081
+ }
2082
+ if (!confirmed) {
2083
+ recordVerifyFailure(loginAttempts, clientIp);
2084
+ res.writeHead(403, { "Content-Type": "application/json" });
2085
+ res.end(
2086
+ JSON.stringify({ error: { code: "CONFIRMATION_REQUIRED", message: "Provide the current password or a TOTP code." } })
2087
+ );
2088
+ return;
2089
+ }
2090
+ loginAttempts.delete(clientIp);
2091
+ const next = await mutateHqAuthFile(dataDir, (current) => {
2092
+ const updated = { ...current };
2093
+ delete updated.totpSecret;
2094
+ delete updated.totpPendingSecret;
2095
+ delete updated.totpRecoveryCodes;
2096
+ return updated;
2097
+ });
2098
+ applyAuthFile(mutableAuth, next);
2099
+ res.writeHead(200, { "Content-Type": "application/json" });
2100
+ res.end(JSON.stringify({ enabled: false }));
2101
+ }
2102
+ function handleApiAuthSessions(_req, res, mutableAuth, sessions) {
2103
+ const now = Date.now();
2104
+ const list = [];
2105
+ for (const [id, session] of sessions) {
2106
+ if (session.pending2fa) continue;
2107
+ list.push({
2108
+ // Truncated for display only — the client sends the full ID to revoke.
2109
+ id,
2110
+ shortId: id.slice(0, 8),
2111
+ kind: session.kind,
2112
+ createdAt: new Date(session.createdAt).toISOString(),
2113
+ lastSeenAt: new Date(session.lastSeenAt).toISOString(),
2114
+ ageMinutes: Math.round((now - session.createdAt) / 6e4),
2115
+ idleMinutes: Math.round((now - session.lastSeenAt) / 6e4)
2116
+ });
2117
+ }
2118
+ res.writeHead(200, { "Content-Type": "application/json" });
2119
+ res.end(
2120
+ JSON.stringify({
2121
+ sessions: list,
2122
+ idleTimeoutMinutes: Math.round(
2123
+ (HQ_SESSION_IDLE_TIMEOUT_MS ?? 30 * 6e4) / 6e4
2124
+ ),
2125
+ maxAgeDays: Math.round(HQ_SESSION_MAX_AGE_MS / (24 * 60 * 6e4)),
2126
+ passwordMode: mutableAuth.passwordHash !== void 0
2127
+ })
2128
+ );
2129
+ }
2130
+ async function handleApiAuthSessionsRevoke(_req, res, url, sessions) {
2131
+ const pathParts = url.pathname.split("/").filter(Boolean);
2132
+ const targetId = pathParts[3];
2133
+ if (targetId) {
2134
+ const session = sessions.get(targetId);
2135
+ if (!session) {
2136
+ res.writeHead(404, { "Content-Type": "application/json" });
2137
+ res.end(JSON.stringify({ error: { code: "NOT_FOUND", message: "Session not found." } }));
2138
+ return;
2139
+ }
2140
+ sessions.delete(targetId);
2141
+ res.writeHead(200, { "Content-Type": "application/json" });
2142
+ res.end(JSON.stringify({ revoked: 1 }));
2143
+ return;
2144
+ }
2145
+ const count = sessions.size;
2146
+ sessions.clear();
2147
+ res.writeHead(200, { "Content-Type": "application/json" });
2148
+ res.end(JSON.stringify({ revoked: count }));
2149
+ }
2150
+ function handleApiAuthAudit(_req, res, dataDir) {
2151
+ const entries = readHqAuthAuditTail(dataDir, 50);
2152
+ res.writeHead(200, { "Content-Type": "application/json" });
2153
+ res.end(JSON.stringify({ entries }));
2154
+ }
1460
2155
 
1461
2156
  // src/hq-server/routes/system-handlers.ts
1462
2157
  async function handleApiSystemUpdate(res) {
1463
2158
  const [{ checkForUpdate }, { detectUpdatePackageName }] = await Promise.all([
1464
2159
  import("./update-check-UMYTOH7Z.js"),
1465
- import("./update-YC4TYW2Y.js")
2160
+ import("./update-MV6TUACD.js")
1466
2161
  ]);
1467
2162
  const packageName = detectUpdatePackageName();
1468
2163
  const info = await checkForUpdate({ packageName });
@@ -1546,14 +2241,14 @@ async function handleApiAlerts(req, res, alertEngine) {
1546
2241
  }
1547
2242
 
1548
2243
  // src/hq-server/routes/session-handlers.ts
1549
- import * as path3 from "node:path";
2244
+ import * as path4 from "node:path";
1550
2245
  import {
1551
2246
  buildTranscriptFromEvents,
1552
2247
  resolveHqDataDir as resolveHqDataDir3
1553
2248
  } from "@wrongstack/core/hq";
1554
2249
  async function handleApiSessions(res) {
1555
2250
  const { SessionRegistry } = await import("@wrongstack/core/storage");
1556
- const globalRoot = path3.dirname(resolveHqDataDir3());
2251
+ const globalRoot = path4.dirname(resolveHqDataDir3());
1557
2252
  try {
1558
2253
  const registry = new SessionRegistry(globalRoot);
1559
2254
  const sessions = await registry.list();
@@ -1592,13 +2287,13 @@ async function handleApiSessionEvents(req, res, match, transcripts) {
1592
2287
  const full = url.searchParams.get("full") === "1";
1593
2288
  const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "200", 10);
1594
2289
  const limit = Math.min(5e3, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 200));
1595
- const sessionId = decodePathSegment(match[1]);
2290
+ const sessionId = decodeSessionId(match[1]);
1596
2291
  if (sessionId === null) {
1597
2292
  res.writeHead(400, { "Content-Type": "application/json" });
1598
2293
  res.end(JSON.stringify({ error: "invalid sessionId encoding" }));
1599
2294
  return;
1600
2295
  }
1601
- const globalRoot = path3.dirname(resolveHqDataDir3());
2296
+ const globalRoot = path4.dirname(resolveHqDataDir3());
1602
2297
  try {
1603
2298
  const registry = new SessionRegistry(globalRoot);
1604
2299
  const entry = await registry.get(sessionId).catch(() => null);
@@ -1660,7 +2355,7 @@ async function handleApiSessionEvents(req, res, match, transcripts) {
1660
2355
  }
1661
2356
  async function handleApiSessionAgentMessages(req, res, match, agentMessages) {
1662
2357
  const url = new URL(req.url ?? "/", "http://localhost");
1663
- const sid = decodePathSegment(match[1]);
2358
+ const sid = decodeSessionId(match[1]);
1664
2359
  const aid = decodePathSegment(match[2]);
1665
2360
  if (sid === null || aid === null) {
1666
2361
  res.writeHead(400, { "Content-Type": "application/json" });
@@ -1706,7 +2401,7 @@ function resolveHqProjectRoot(globalRoot, ids) {
1706
2401
  if (entry?.projectRoot) return entry.projectRoot;
1707
2402
  }
1708
2403
  if (typeof ids.projectId === "string") {
1709
- const { createHash } = await import("node:crypto");
2404
+ const { createHash: createHash2 } = await import("node:crypto");
1710
2405
  const all = await registry.list().catch(() => []);
1711
2406
  const projectIds = /* @__PURE__ */ new Map();
1712
2407
  const projectIdForRoot = (projectRoot) => {
@@ -1717,7 +2412,7 @@ function resolveHqProjectRoot(globalRoot, ids) {
1717
2412
  return derived;
1718
2413
  };
1719
2414
  const match = all.find(
1720
- (e) => e.projectSlug === ids.projectId || projectIdForRoot(e.projectRoot) === ids.projectId || createHash("sha256").update(e.projectRoot).digest("hex").slice(0, 12) === ids.projectId
2415
+ (e) => e.projectSlug === ids.projectId || projectIdForRoot(e.projectRoot) === ids.projectId || createHash2("sha256").update(e.projectRoot).digest("hex").slice(0, 12) === ids.projectId
1721
2416
  );
1722
2417
  if (match) return match.projectRoot;
1723
2418
  }
@@ -1734,6 +2429,7 @@ var hasTrustedBrowserOrigin2 = hasTrustedBrowserOrigin;
1734
2429
  var authenticateBrowserRequest2 = authenticateBrowserRequest;
1735
2430
  var isTokenAuth2 = isTokenAuth;
1736
2431
  var isCookieAuth2 = isCookieAuth;
2432
+ var hqAuthRequired2 = hqAuthRequired;
1737
2433
  var parseHqSessionCookie2 = parseHqSessionCookie;
1738
2434
  var parseCookieHeader2 = parseCookieHeader;
1739
2435
  var HQ_SESSION_COOKIE2 = HQ_SESSION_COOKIE;
@@ -1752,6 +2448,7 @@ function createHqRouter(deps) {
1752
2448
  host,
1753
2449
  listeningPort,
1754
2450
  trustedPublicOrigins,
2451
+ allowFileOrigin,
1755
2452
  mutableAuth,
1756
2453
  sessions,
1757
2454
  loginAttempts,
@@ -1777,12 +2474,18 @@ function createHqRouter(deps) {
1777
2474
  try {
1778
2475
  const url = new URL(req.url ?? "/", `http://${host}:${listeningPort}`);
1779
2476
  setHqSecurityHeaders2(res);
1780
- if (!hasTrustedBrowserOrigin2(req, host, listeningPort, trustedPublicOrigins)) {
2477
+ if (!hasTrustedBrowserOrigin2(
2478
+ req,
2479
+ host,
2480
+ listeningPort,
2481
+ trustedPublicOrigins,
2482
+ allowFileOrigin
2483
+ )) {
1781
2484
  res.writeHead(403, { "Content-Type": "application/json" });
1782
2485
  res.end(JSON.stringify({ error: "forbidden: untrusted request origin" }));
1783
2486
  return;
1784
2487
  }
1785
- if (url.pathname.startsWith("/api/") && url.pathname !== "/api/auth/status" && url.pathname !== "/api/login" && url.pathname !== "/api/auth/bootstrap" && (requireBrowserAuth || mutableAuth.browserTokens.size > 0 || mutableAuth.passwordHash)) {
2488
+ if (url.pathname.startsWith("/api/") && url.pathname !== "/api/auth/status" && url.pathname !== "/api/login" && url.pathname !== "/api/login/verify" && url.pathname !== "/api/auth/bootstrap" && hqAuthRequired2(mutableAuth, requireBrowserAuth)) {
1786
2489
  const auth = authenticateBrowserRequest2(req, url, mutableAuth, sessions);
1787
2490
  if (!auth) {
1788
2491
  const tokenOnly = mutableAuth.browserTokens.size > 0 && !mutableAuth.passwordHash;
@@ -1813,6 +2516,18 @@ function createHqRouter(deps) {
1813
2516
  await handleApiAuthStatus(req, res, url, mutableAuth, sessions, requireBrowserAuth, trustedPublicOrigins, secureCookies);
1814
2517
  return;
1815
2518
  }
2519
+ if (url.pathname === "/api/auth/audit" && req.method === "GET") {
2520
+ handleApiAuthAudit(req, res, dataDir);
2521
+ return;
2522
+ }
2523
+ if (url.pathname === "/api/auth/sessions" && req.method === "GET") {
2524
+ handleApiAuthSessions(req, res, mutableAuth, sessions);
2525
+ return;
2526
+ }
2527
+ if (url.pathname.startsWith("/api/auth/sessions") && req.method === "DELETE") {
2528
+ await handleApiAuthSessionsRevoke(req, res, url, sessions);
2529
+ return;
2530
+ }
1816
2531
  if (url.pathname === "/api/login" && req.method === "POST") {
1817
2532
  await handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, secureCookies);
1818
2533
  return;
@@ -1833,10 +2548,30 @@ function createHqRouter(deps) {
1833
2548
  }
1834
2549
  return;
1835
2550
  }
2551
+ if (url.pathname === "/api/auth/upgrade" && req.method === "POST") {
2552
+ await handleApiTokenUpgrade(req, res, url, mutableAuth, sessions, secureCookies);
2553
+ return;
2554
+ }
1836
2555
  if (url.pathname === "/api/logout" && req.method === "POST") {
1837
2556
  await handleApiLogout(req, res, mutableAuth, sessions, secureCookies);
1838
2557
  return;
1839
2558
  }
2559
+ if (url.pathname === "/api/login/verify" && req.method === "POST") {
2560
+ await handleApiLoginVerify(req, res, url, mutableAuth, sessions, loginAttempts, dataDir, secureCookies);
2561
+ return;
2562
+ }
2563
+ if (url.pathname === "/api/auth/totp/setup" && req.method === "POST") {
2564
+ await handleApiTotpSetup(req, res, mutableAuth, sessions, dataDir);
2565
+ return;
2566
+ }
2567
+ if (url.pathname === "/api/auth/totp/enable" && req.method === "POST") {
2568
+ await handleApiTotpEnable(req, res, mutableAuth, sessions, dataDir);
2569
+ return;
2570
+ }
2571
+ if (url.pathname === "/api/auth/totp/disable" && req.method === "POST") {
2572
+ await handleApiTotpDisable(req, res, mutableAuth, sessions, loginAttempts, dataDir);
2573
+ return;
2574
+ }
1840
2575
  if (url.pathname === "/api/auth/password" && (req.method === "POST" || req.method === "DELETE")) {
1841
2576
  await handleApiPassword(req, res, mutableAuth, sessions, dataDir, secureCookies, requireBrowserAuth);
1842
2577
  return;
@@ -1972,7 +2707,7 @@ async function handleMailboxGateway(_req, res, match, authorizeMailboxGateway, g
1972
2707
  );
1973
2708
  return;
1974
2709
  }
1975
- const gatewayGlobalRoot = path4.dirname(dataDir);
2710
+ const gatewayGlobalRoot = path5.dirname(dataDir);
1976
2711
  const preliminaryAccess = authorizeMailboxGateway(_req, `project:${projectId}`);
1977
2712
  if (!preliminaryAccess.allowed) {
1978
2713
  res.writeHead(preliminaryAccess.status ?? 401, {
@@ -2044,14 +2779,12 @@ async function handleApiTrendsCost(req, res, persistence) {
2044
2779
  }
2045
2780
  async function handleApiCommand(req, res, url, mutableAuth, sessions, clients, browsers, auditLog, trustBoundary) {
2046
2781
  const auth = authenticateBrowserRequest2(req, url, mutableAuth, sessions);
2047
- const inBrowserTokenMode = mutableAuth.browserTokens.size > 0;
2048
- const inPasswordMode = mutableAuth.passwordHash !== void 0;
2049
- if ((inBrowserTokenMode || inPasswordMode) && !auth) {
2782
+ if (hqAuthRequired2(mutableAuth) && !auth) {
2050
2783
  res.writeHead(401, { "Content-Type": "application/json" });
2051
2784
  res.end(JSON.stringify({ error: "unauthorized" }));
2052
2785
  return;
2053
2786
  }
2054
- const canEnqueue = isCookieAuth2(auth) ? auth.tokenId === void 0 || auth.capabilities === void 0 || auth.capabilities.includes("control.enqueue") : isTokenAuth2(auth) ? auth.capabilities === void 0 || auth.capabilities.includes("control.enqueue") : !inBrowserTokenMode;
2787
+ const canEnqueue = callerCanEnqueue(auth, mutableAuth);
2055
2788
  if (!canEnqueue) {
2056
2789
  res.writeHead(403, { "Content-Type": "application/json" });
2057
2790
  res.end(JSON.stringify({ error: "forbidden: token lacks control.enqueue capability" }));
@@ -2153,7 +2886,12 @@ async function handleApiMailboxSend(_req, res, mutableAuth, sessions, dataDir, g
2153
2886
  mutableAuth,
2154
2887
  sessions
2155
2888
  );
2156
- const canEnqueue = isCookieAuth2(auth) ? auth.tokenId === void 0 || auth.capabilities === void 0 || auth.capabilities.includes("control.enqueue") : isTokenAuth2(auth) ? auth.capabilities === void 0 || auth.capabilities.includes("control.enqueue") : mutableAuth.browserTokens.size === 0;
2889
+ if (hqAuthRequired2(mutableAuth) && !auth) {
2890
+ res.writeHead(401, { "Content-Type": "application/json" });
2891
+ res.end(JSON.stringify({ error: { code: "UNAUTHORIZED", message: "unauthorized" } }));
2892
+ return;
2893
+ }
2894
+ const canEnqueue = callerCanEnqueue(auth, mutableAuth);
2157
2895
  if (!canEnqueue) {
2158
2896
  res.writeHead(403, { "Content-Type": "application/json" });
2159
2897
  res.end(JSON.stringify({ error: "forbidden: token lacks control.enqueue capability" }));
@@ -2181,7 +2919,7 @@ async function handleApiMailboxSend(_req, res, mutableAuth, sessions, dataDir, g
2181
2919
  res.end(JSON.stringify({ error: "audience must be all or leaders" }));
2182
2920
  return;
2183
2921
  }
2184
- const mbGlobalRoot = path4.dirname(dataDir);
2922
+ const mbGlobalRoot = path5.dirname(dataDir);
2185
2923
  const projectRoot = await resolveHqProjectRoot(mbGlobalRoot, {
2186
2924
  sessionId: mbody.sessionId,
2187
2925
  projectId: mbody.projectId
@@ -2239,10 +2977,35 @@ async function handleApiMailboxSend(_req, res, mutableAuth, sessions, dataDir, g
2239
2977
  );
2240
2978
  } catch (err) {
2241
2979
  res.writeHead(500, { "Content-Type": "application/json" });
2242
- res.end(JSON.stringify({ error: "mailbox write failed", detail: String(err) }));
2980
+ res.end(JSON.stringify({ error: "mailbox write failed", detail: sanitizeApiError2(err) }));
2981
+ }
2982
+ }
2983
+ function callerCanEnqueue(auth, mutableAuth) {
2984
+ if (isCookieAuth2(auth)) {
2985
+ return auth.tokenId === void 0 || auth.capabilities === void 0 || auth.capabilities.includes("control.enqueue");
2986
+ }
2987
+ if (isTokenAuth2(auth)) {
2988
+ return auth.capabilities === void 0 || auth.capabilities.includes("control.enqueue");
2243
2989
  }
2990
+ return !hqAuthRequired2(mutableAuth);
2244
2991
  }
2245
- async function handleMailboxAction(_req, res, match, _mutableAuth, _sessions, dataDir, getMailboxGateway) {
2992
+ async function handleMailboxAction(_req, res, match, mutableAuth, sessions, dataDir, getMailboxGateway) {
2993
+ const auth = authenticateBrowserRequest2(
2994
+ _req,
2995
+ new URL(_req.url ?? "/", "http://localhost"),
2996
+ mutableAuth,
2997
+ sessions
2998
+ );
2999
+ if (hqAuthRequired2(mutableAuth) && !auth) {
3000
+ res.writeHead(401, { "Content-Type": "application/json" });
3001
+ res.end(JSON.stringify({ error: "unauthorized" }));
3002
+ return;
3003
+ }
3004
+ if (!callerCanEnqueue(auth, mutableAuth)) {
3005
+ res.writeHead(403, { "Content-Type": "application/json" });
3006
+ res.end(JSON.stringify({ error: "forbidden: token lacks control.enqueue capability" }));
3007
+ return;
3008
+ }
2246
3009
  const MAILBOX_ACTIONS = [
2247
3010
  "mark-read",
2248
3011
  "acknowledge",
@@ -2269,7 +3032,9 @@ async function handleMailboxAction(_req, res, match, _mutableAuth, _sessions, da
2269
3032
  res.end(JSON.stringify({ error: "unrecognized action", action: abody.action }));
2270
3033
  return;
2271
3034
  }
2272
- if (typeof abody.readerId !== "string" || abody.readerId.length === 0) {
3035
+ const authenticatedReaderId = isCookieAuth2(auth) ? auth.tokenId ?? "hq-password-session" : isTokenAuth2(auth) ? auth.id : void 0;
3036
+ const resolvedReaderId = authenticatedReaderId ?? abody.readerId;
3037
+ if (typeof resolvedReaderId !== "string" || resolvedReaderId.length === 0) {
2273
3038
  res.writeHead(400, { "Content-Type": "application/json" });
2274
3039
  res.end(JSON.stringify({ error: "missing readerId" }));
2275
3040
  return;
@@ -2279,7 +3044,7 @@ async function handleMailboxAction(_req, res, match, _mutableAuth, _sessions, da
2279
3044
  res.end(JSON.stringify({ error: "missing sessionId or projectId" }));
2280
3045
  return;
2281
3046
  }
2282
- const actGlobalRoot = path4.dirname(dataDir);
3047
+ const actGlobalRoot = path5.dirname(dataDir);
2283
3048
  const projectRoot = await resolveHqProjectRoot(actGlobalRoot, {
2284
3049
  sessionId: abody.sessionId,
2285
3050
  projectId: abody.projectId
@@ -2293,7 +3058,7 @@ async function handleMailboxAction(_req, res, match, _mutableAuth, _sessions, da
2293
3058
  const { actionToAckInput } = await import("@wrongstack/core/coordination");
2294
3059
  const projectDir = resolveProjectDir(projectRoot, actGlobalRoot);
2295
3060
  const mailbox = getMailboxGateway(projectDir).mailbox;
2296
- const readerId = abody.readerId;
3061
+ const readerId = resolvedReaderId;
2297
3062
  const message = action === "soft-delete" ? await mailbox.softDelete(mailId, readerId) : action === "restore" ? await mailbox.restore(mailId) : await mailbox.ack(actionToAckInput(action, { action, mailId, readerId }));
2298
3063
  if (message === null) {
2299
3064
  res.writeHead(404, { "Content-Type": "application/json" });
@@ -2314,8 +3079,8 @@ async function handleMailboxAction(_req, res, match, _mutableAuth, _sessions, da
2314
3079
  }
2315
3080
 
2316
3081
  // src/hq-server/startup.ts
2317
- import * as fs3 from "node:fs/promises";
2318
- import * as path5 from "node:path";
3082
+ import * as fs4 from "node:fs/promises";
3083
+ import * as path6 from "node:path";
2319
3084
  var hqRuntimeMarkerPath2 = hqRuntimeMarkerPath;
2320
3085
  async function writeHqRuntimeMarker(dataDir, url) {
2321
3086
  const file = hqRuntimeMarkerPath2(dataDir);
@@ -2324,17 +3089,17 @@ async function writeHqRuntimeMarker(dataDir, url) {
2324
3089
  null,
2325
3090
  2
2326
3091
  );
2327
- await fs3.mkdir(path5.dirname(file), { recursive: true });
2328
- await fs3.writeFile(file, `${payload}
3092
+ await fs4.mkdir(path6.dirname(file), { recursive: true });
3093
+ await fs4.writeFile(file, `${payload}
2329
3094
  `, { encoding: "utf8", mode: 384 });
2330
3095
  }
2331
3096
  async function clearHqRuntimeMarker(dataDir, url) {
2332
3097
  const file = hqRuntimeMarkerPath2(dataDir);
2333
3098
  try {
2334
- const raw = await fs3.readFile(file, "utf8");
3099
+ const raw = await fs4.readFile(file, "utf8");
2335
3100
  const parsed = JSON.parse(raw);
2336
3101
  if (parsed.url === url && parsed.pid === process.pid) {
2337
- await fs3.rm(file, { force: true });
3102
+ await fs4.rm(file, { force: true });
2338
3103
  }
2339
3104
  } catch {
2340
3105
  }
@@ -2370,7 +3135,7 @@ HQ auth loaded from ${startup.dataDir}
2370
3135
  if (startup.clientEnv.WRONGSTACK_HQ_TOKEN) {
2371
3136
  write(` WRONGSTACK_HQ_TOKEN=${startup.clientEnv.WRONGSTACK_HQ_TOKEN}
2372
3137
  `);
2373
- write(`Credentials are stored in ${path5.join(startup.dataDir, "auth.json")}
3138
+ write(`Credentials are stored in ${path6.join(startup.dataDir, "auth.json")}
2374
3139
  `);
2375
3140
  }
2376
3141
  writeHqLanEndpoints(write, handle, void 0);
@@ -2391,7 +3156,11 @@ function writeHqLanEndpoints(write, handle, browserToken) {
2391
3156
  // src/hq-server/ws.ts
2392
3157
  import { randomUUID as randomUUID3 } from "node:crypto";
2393
3158
  import {
3159
+ HQ_BROWSER_PEER_RESUME_CLIENT_ID,
2394
3160
  HQ_PROTOCOL_VERSION,
3161
+ HQ_RESUME_GAP_MAX_BYTES,
3162
+ HQ_RESUME_GAP_MAX_ENVELOPES,
3163
+ HQ_RESUME_GAP_MAX_STALE_MS,
2395
3164
  HQ_TRANSCRIPT_TEXT_CAP,
2396
3165
  parseHqEventPayload,
2397
3166
  parseHqFrame,
@@ -2402,7 +3171,274 @@ import {
2402
3171
  } from "@wrongstack/core/hq";
2403
3172
  import { WebSocket as WebSocket2 } from "ws";
2404
3173
  var MAX_EVENT_LOG = 5e3;
2405
- function handleBrowser(ws, snapshotBroadcaster, browsers) {
3174
+ function computeIsLeader(clients, projectId, acceptedCapabilities) {
3175
+ if (!acceptedCapabilities.includes("control.receive")) return false;
3176
+ for (const other of clients.values()) {
3177
+ if (other.projectId === projectId && other.capabilities.includes("control.receive")) {
3178
+ return false;
3179
+ }
3180
+ }
3181
+ return true;
3182
+ }
3183
+ function promoteNewLeader(clients, projectId, excludeWs, excludeClient) {
3184
+ const survivors = [];
3185
+ for (const [otherWs, otherClient] of clients) {
3186
+ if (otherWs === excludeWs || otherClient === excludeClient) continue;
3187
+ if (otherClient.projectId !== projectId) continue;
3188
+ if (!otherClient.capabilities.includes("control.receive")) continue;
3189
+ survivors.push(otherClient);
3190
+ }
3191
+ if (survivors.length === 0) return null;
3192
+ survivors.sort((a, b) => Date.parse(b.connectedAt) - Date.parse(a.connectedAt));
3193
+ const promoted = survivors[0];
3194
+ if (promoted) {
3195
+ promoted.isLeader = true;
3196
+ return promoted.clientId;
3197
+ }
3198
+ return null;
3199
+ }
3200
+ function detectLeaderLoss(lostClient, clients, browsers, reason, eventSink) {
3201
+ if (!lostClient.isLeader) return false;
3202
+ const detectedAt = (/* @__PURE__ */ new Date()).toISOString();
3203
+ const promotedLeaderId = promoteNewLeader(
3204
+ clients,
3205
+ lostClient.projectId,
3206
+ lostClient.ws,
3207
+ lostClient
3208
+ );
3209
+ const hasSurvivors = promotedLeaderId !== null;
3210
+ const machineId = lostClient.machineId?.trim() || lostClient.project.machineId.trim() || "unknown";
3211
+ const previousLeaderHandle = lostClient.clientId;
3212
+ const seq = nextPeerEventSeq(eventSink?.eventLog);
3213
+ let envelope;
3214
+ if (hasSurvivors) {
3215
+ if (checkPeerDedup(lostClient.projectId, previousLeaderHandle)) {
3216
+ return false;
3217
+ }
3218
+ const payload = {
3219
+ projectId: lostClient.projectId,
3220
+ machineId,
3221
+ leaderClientId: lostClient.clientId,
3222
+ previousLeaderHandle,
3223
+ reason,
3224
+ detectedAt
3225
+ };
3226
+ envelope = {
3227
+ id: `peer-${lostClient.clientId}-${Date.now()}`,
3228
+ type: "peer.rehydrate",
3229
+ schemaVersion: HQ_PROTOCOL_VERSION,
3230
+ timestamp: detectedAt,
3231
+ clientId: HQ_BROWSER_PEER_RESUME_CLIENT_ID,
3232
+ projectId: lostClient.projectId,
3233
+ seq,
3234
+ payload
3235
+ };
3236
+ recordPeerEmit(lostClient.projectId, previousLeaderHandle);
3237
+ } else {
3238
+ const payload = {
3239
+ projectId: lostClient.projectId,
3240
+ machineId,
3241
+ leaderClientId: lostClient.clientId,
3242
+ previousLeaderHandle,
3243
+ reason,
3244
+ detectedAt
3245
+ };
3246
+ envelope = {
3247
+ id: `peer-${lostClient.clientId}-${Date.now()}`,
3248
+ type: "peer.lost",
3249
+ schemaVersion: HQ_PROTOCOL_VERSION,
3250
+ timestamp: detectedAt,
3251
+ clientId: HQ_BROWSER_PEER_RESUME_CLIENT_ID,
3252
+ projectId: lostClient.projectId,
3253
+ seq,
3254
+ payload
3255
+ };
3256
+ }
3257
+ if (eventSink !== void 0) {
3258
+ eventSink.eventLog.push(envelope);
3259
+ if (eventSink.eventLog.length > MAX_EVENT_LOG) {
3260
+ eventSink.eventLog.splice(0, eventSink.eventLog.length - MAX_EVENT_LOG);
3261
+ }
3262
+ eventSink.persistence?.eventLog.append(envelope);
3263
+ }
3264
+ broadcastEvent(envelope, browsers);
3265
+ for (const [peerWs, peerClient] of clients) {
3266
+ if (peerWs === lostClient.ws) continue;
3267
+ if (peerClient.projectId !== lostClient.projectId) continue;
3268
+ sendGuarded(peerWs, JSON.stringify({ type: "hq.event", event: envelope }));
3269
+ }
3270
+ return true;
3271
+ }
3272
+ var peerEventSeq = 0;
3273
+ function nextPeerEventSeq(eventLog) {
3274
+ if (eventLog !== void 0) {
3275
+ for (const env of eventLog) {
3276
+ if (env.clientId === HQ_BROWSER_PEER_RESUME_CLIENT_ID && env.seq > peerEventSeq) {
3277
+ peerEventSeq = env.seq;
3278
+ }
3279
+ }
3280
+ }
3281
+ peerEventSeq += 1;
3282
+ return peerEventSeq;
3283
+ }
3284
+ function fanoutKanbanDelta(message, clients, browsers, projectId) {
3285
+ let clientsNotified = 0;
3286
+ for (const peer of clients.values()) {
3287
+ if (peer.projectId === projectId) {
3288
+ sendGuarded(peer.ws, message);
3289
+ clientsNotified++;
3290
+ }
3291
+ }
3292
+ let browsersNotified = 0;
3293
+ for (const browserWs of browsers) {
3294
+ sendGuarded(browserWs, message);
3295
+ browsersNotified++;
3296
+ }
3297
+ return { clientsNotified, browsersNotified };
3298
+ }
3299
+ function handleClientResume(ws, clients, eventLog, frame, snapshotBroadcaster) {
3300
+ const client = clients.get(ws);
3301
+ if (!client) return;
3302
+ if (frame.clientId !== void 0 && frame.clientId !== client.clientId) {
3303
+ ws.close(1008, "resume clientId mismatch");
3304
+ return;
3305
+ }
3306
+ if (client.kind !== "browser" && frame.projectId !== void 0 && frame.projectId !== client.projectId) {
3307
+ ws.close(1008, "resume projectId mismatch");
3308
+ return;
3309
+ }
3310
+ client.lastSeenAt = (/* @__PURE__ */ new Date()).toISOString();
3311
+ const cursor = frame.lastSeqSeen;
3312
+ const now = Date.now();
3313
+ const recent = [];
3314
+ let cursorTimestamp;
3315
+ const skipPeerEnvelopes = client.clientId !== HQ_BROWSER_PEER_RESUME_CLIENT_ID;
3316
+ for (const env of eventLog) {
3317
+ if (env.clientId !== client.clientId) continue;
3318
+ if (skipPeerEnvelopes && (env.type === "peer.rehydrate" || env.type === "peer.lost")) {
3319
+ continue;
3320
+ }
3321
+ if (env.seq === cursor) cursorTimestamp = env.timestamp;
3322
+ if (env.seq > cursor) {
3323
+ recent.push(env);
3324
+ }
3325
+ }
3326
+ if (cursor > 0 && cursorTimestamp === void 0) {
3327
+ if (snapshotBroadcaster !== void 0) {
3328
+ sendGuarded(ws, snapshotBroadcaster.currentSerialized());
3329
+ return;
3330
+ }
3331
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
3332
+ sendGuarded(
3333
+ ws,
3334
+ JSON.stringify({
3335
+ type: "hq.resume_reject",
3336
+ reason: "log_unavailable",
3337
+ detectedAt: nowIso
3338
+ })
3339
+ );
3340
+ return;
3341
+ }
3342
+ const hasStale = cursorTimestamp !== void 0 && now - Date.parse(cursorTimestamp) > HQ_RESUME_GAP_MAX_STALE_MS;
3343
+ if (hasStale) {
3344
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
3345
+ sendGuarded(
3346
+ ws,
3347
+ JSON.stringify({
3348
+ type: "hq.resume_reject",
3349
+ reason: "last_seen_too_old",
3350
+ detectedAt: nowIso
3351
+ })
3352
+ );
3353
+ return;
3354
+ }
3355
+ let serializedByteLength = 0;
3356
+ for (const env of recent) {
3357
+ serializedByteLength += Buffer.byteLength(JSON.stringify(env), "utf8");
3358
+ if (serializedByteLength > HQ_RESUME_GAP_MAX_BYTES) break;
3359
+ }
3360
+ const gapIsTooLarge = recent.length > HQ_RESUME_GAP_MAX_ENVELOPES || serializedByteLength > HQ_RESUME_GAP_MAX_BYTES;
3361
+ if (gapIsTooLarge) {
3362
+ if (snapshotBroadcaster !== void 0) {
3363
+ sendGuarded(ws, snapshotBroadcaster.currentSerialized());
3364
+ return;
3365
+ }
3366
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
3367
+ sendGuarded(
3368
+ ws,
3369
+ JSON.stringify({
3370
+ type: "hq.resume_reject",
3371
+ reason: "gap_too_large",
3372
+ detectedAt: nowIso
3373
+ })
3374
+ );
3375
+ return;
3376
+ }
3377
+ recent.sort((a, b) => a.seq - b.seq);
3378
+ sendGuarded(
3379
+ ws,
3380
+ JSON.stringify({
3381
+ type: "hq.resume_gap",
3382
+ lastSeqSeen: cursor,
3383
+ envelopes: recent,
3384
+ truncated: false
3385
+ })
3386
+ );
3387
+ return;
3388
+ }
3389
+ var PEER_DEDUP_WINDOW_MS = 60 * 60 * 1e3;
3390
+ var peerDedupMap = /* @__PURE__ */ new Map();
3391
+ function peerDedupKey(projectId, previousLeaderHandle) {
3392
+ return `${projectId}::${previousLeaderHandle}`;
3393
+ }
3394
+ function checkPeerDedup(projectId, previousLeaderHandle) {
3395
+ const key = peerDedupKey(projectId, previousLeaderHandle);
3396
+ const now = Date.now();
3397
+ for (const [k, ts] of peerDedupMap) {
3398
+ if (now - ts > PEER_DEDUP_WINDOW_MS) peerDedupMap.delete(k);
3399
+ }
3400
+ const last = peerDedupMap.get(key);
3401
+ return last !== void 0 && now - last <= PEER_DEDUP_WINDOW_MS;
3402
+ }
3403
+ function recordPeerEmit(projectId, previousLeaderHandle) {
3404
+ peerDedupMap.set(peerDedupKey(projectId, previousLeaderHandle), Date.now());
3405
+ }
3406
+ function browserResumeClient(ws, clientId) {
3407
+ return /* @__PURE__ */ new Map([
3408
+ [
3409
+ ws,
3410
+ {
3411
+ ws,
3412
+ clientId,
3413
+ projectId: "",
3414
+ project: {
3415
+ projectId: "",
3416
+ projectName: "",
3417
+ projectRoot: "",
3418
+ machineId: "",
3419
+ workspaceKind: "git"
3420
+ },
3421
+ kind: "browser",
3422
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
3423
+ lastSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
3424
+ capabilities: [],
3425
+ declaredRedactionPolicy: {
3426
+ rawContent: false,
3427
+ toolArgs: "summary",
3428
+ paths: "project-relative"
3429
+ },
3430
+ lastEventSeq: 0,
3431
+ mailboxes: /* @__PURE__ */ new Map(),
3432
+ sessions: /* @__PURE__ */ new Map(),
3433
+ fleets: /* @__PURE__ */ new Map(),
3434
+ mcpSnapshots: /* @__PURE__ */ new Map(),
3435
+ commandQueue: [],
3436
+ isLeader: false
3437
+ }
3438
+ ]
3439
+ ]);
3440
+ }
3441
+ function handleBrowser(ws, snapshotBroadcaster, browsers, eventLog) {
2406
3442
  browsers.add(ws);
2407
3443
  ws.on("error", (err) => {
2408
3444
  console.warn(
@@ -2415,6 +3451,40 @@ function handleBrowser(ws, snapshotBroadcaster, browsers) {
2415
3451
  );
2416
3452
  });
2417
3453
  sendGuarded(ws, snapshotBroadcaster.currentSerialized());
3454
+ ws.on("message", (data) => {
3455
+ let raw;
3456
+ if (typeof data === "string") {
3457
+ raw = Buffer.from(data, "utf8");
3458
+ } else if (Buffer.isBuffer(data)) {
3459
+ raw = data;
3460
+ } else if (Array.isArray(data)) {
3461
+ raw = Buffer.concat(data);
3462
+ } else {
3463
+ raw = Buffer.from(new TextDecoder().decode(data), "utf8");
3464
+ }
3465
+ const parsed = parseHqFrame(raw);
3466
+ if (!parsed.ok) {
3467
+ const code = parsed.reason === "invalid-json" ? 1003 : 1008;
3468
+ ws.close(code, `invalid frame: ${parsed.reason}`);
3469
+ return;
3470
+ }
3471
+ const frame = parsed.frame;
3472
+ if (frame.type !== "client.resume") {
3473
+ ws.close(1008, "browser frame must be client.resume");
3474
+ return;
3475
+ }
3476
+ if (frame.clientId === void 0 || frame.clientId.length === 0) {
3477
+ ws.close(1008, "browser resume requires clientId");
3478
+ return;
3479
+ }
3480
+ handleClientResume(
3481
+ ws,
3482
+ browserResumeClient(ws, frame.clientId),
3483
+ eventLog,
3484
+ frame,
3485
+ snapshotBroadcaster
3486
+ );
3487
+ });
2418
3488
  ws.on("close", () => {
2419
3489
  browsers.delete(ws);
2420
3490
  });
@@ -2463,9 +3533,16 @@ function handleClient(ws, clients, browsers, eventLog, auth, snapshotBroadcaster
2463
3533
  (capability) => capability === "control.receive" || canPublishTelemetry
2464
3534
  );
2465
3535
  const declaredRedactionPolicy = resolveHqRedactionPolicy(payload.redactionPolicy);
3536
+ const supersededLeaders = [];
3537
+ let inheritedLeader = false;
2466
3538
  for (const [otherWs, otherClient] of clients) {
3539
+ const sameClientId = otherClient.clientId === payload.client.clientId;
2467
3540
  const samePublisher = otherClient.projectId === payload.project.projectId && otherClient.kind === payload.client.kind && otherClient.pid !== void 0 && payload.client.pid !== void 0 && otherClient.pid === payload.client.pid && (otherClient.machineId || otherClient.project.machineId) === (payload.client.machineId || payload.project.machineId);
2468
- if (otherWs !== ws && (otherClient.clientId === payload.client.clientId || samePublisher && otherClient.sessions.size === 0)) {
3541
+ if (otherWs !== ws && (sameClientId || samePublisher && otherClient.sessions.size === 0)) {
3542
+ if (otherClient.isLeader) {
3543
+ if (sameClientId) inheritedLeader = true;
3544
+ else supersededLeaders.push(otherClient);
3545
+ }
2469
3546
  clients.delete(otherWs);
2470
3547
  otherWs.close(4001, "superseded by a newer HQ connection");
2471
3548
  }
@@ -2490,10 +3567,19 @@ function handleClient(ws, clients, browsers, eventLog, auth, snapshotBroadcaster
2490
3567
  sessions: /* @__PURE__ */ new Map(),
2491
3568
  fleets: /* @__PURE__ */ new Map(),
2492
3569
  mcpSnapshots: /* @__PURE__ */ new Map(),
2493
- commandQueue: []
3570
+ commandQueue: [],
3571
+ // Leader for the project is the unique client with `control.receive`
3572
+ // capability. First such client wins; subsequent clients with the
3573
+ // capability are followers. Same-clientId reconnects inherit this
3574
+ // flag from the superseded socket because that identity never really
3575
+ // disappeared; different-client supersedes still use the loss detector.
3576
+ isLeader: inheritedLeader || computeIsLeader(clients, payload.project.projectId, acceptedCapabilities)
2494
3577
  };
2495
3578
  clients.set(ws, client);
2496
3579
  registered = true;
3580
+ for (const lostLeader of supersededLeaders) {
3581
+ detectLeaderLoss(lostLeader, clients, browsers, "crash", { eventLog, persistence });
3582
+ }
2497
3583
  const welcome = {
2498
3584
  type: "hq.welcome",
2499
3585
  protocolVersion: HQ_PROTOCOL_VERSION,
@@ -2583,6 +3669,10 @@ function handleClient(ws, clients, browsers, eventLog, auth, snapshotBroadcaster
2583
3669
  }
2584
3670
  return;
2585
3671
  }
3672
+ if (frame.type === "client.resume") {
3673
+ handleClientResume(ws, clients, eventLog, frame, snapshotBroadcaster);
3674
+ return;
3675
+ }
2586
3676
  if (frame.type === "client.event") {
2587
3677
  const client = clients.get(ws);
2588
3678
  if (!client) return;
@@ -2636,9 +3726,7 @@ function handleClient(ws, clients, browsers, eventLog, auth, snapshotBroadcaster
2636
3726
  tombstones: merged.tombstones.filter((record) => touched.has(record.boardId))
2637
3727
  };
2638
3728
  const message = JSON.stringify({ type: "hq.kanban_snapshot", payload: delta });
2639
- for (const peer of clients.values()) {
2640
- if (peer.projectId === client.projectId) sendGuarded(peer.ws, message);
2641
- }
3729
+ fanoutKanbanDelta(message, clients, browsers, client.projectId);
2642
3730
  }).catch((error) => {
2643
3731
  console.warn(
2644
3732
  JSON.stringify({
@@ -2727,6 +3815,17 @@ function handleClient(ws, clients, browsers, eventLog, auth, snapshotBroadcaster
2727
3815
  }
2728
3816
  return;
2729
3817
  }
3818
+ if (event.type === "governance.snapshot" && client !== void 0) {
3819
+ if (!client.isLeader) return;
3820
+ const result = parseHqEventPayload(event.type, event.payload);
3821
+ if (result.ok) {
3822
+ const payload = result.payload;
3823
+ if (payload.projectId !== client.projectId) return;
3824
+ client.governanceSnapshot = { payload, receivedAt: Date.now() };
3825
+ snapshotBroadcaster.broadcast();
3826
+ }
3827
+ return;
3828
+ }
2730
3829
  if (event.type === "session.transcript" && client !== void 0) {
2731
3830
  const result = parseHqEventPayload(event.type, event.payload);
2732
3831
  if (result.ok) {
@@ -2768,7 +3867,11 @@ function handleClient(ws, clients, browsers, eventLog, auth, snapshotBroadcaster
2768
3867
  }
2769
3868
  });
2770
3869
  ws.on("close", () => {
3870
+ const lostClient = clients.get(ws);
2771
3871
  clients.delete(ws);
3872
+ if (lostClient) {
3873
+ detectLeaderLoss(lostClient, clients, browsers, "graceful", { eventLog, persistence });
3874
+ }
2772
3875
  snapshotBroadcaster.broadcast();
2773
3876
  });
2774
3877
  }
@@ -2827,7 +3930,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
2827
3930
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
2828
3931
  })
2829
3932
  );
2830
- return new Promise((resolve, reject) => {
3933
+ return new Promise((resolve2, reject) => {
2831
3934
  const trustedPublicOrigins = /* @__PURE__ */ new Set();
2832
3935
  let listeningPort = port;
2833
3936
  const clients = /* @__PURE__ */ new Map();
@@ -2837,37 +3940,54 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
2837
3940
  const transcripts = /* @__PURE__ */ new Map();
2838
3941
  const agentMessages = /* @__PURE__ */ new Map();
2839
3942
  const mailboxGateways = /* @__PURE__ */ new Map();
3943
+ const MAILBOX_GATEWAY_IDLE_TTL_MS = 15 * 6e4;
3944
+ const MAILBOX_GATEWAY_SWEEP_INTERVAL_MS = 6e4;
3945
+ const mailboxGatewayLastUsed = /* @__PURE__ */ new Map();
2840
3946
  const mailboxGatewayRateLimiter = new MailboxHttpRateLimiter();
2841
3947
  const mailboxGatewayRateLimitCleanup = setInterval(
2842
3948
  () => mailboxGatewayRateLimiter.cleanup(),
2843
3949
  12e4
2844
3950
  );
2845
3951
  mailboxGatewayRateLimitCleanup.unref?.();
2846
- const loginAttempts = /* @__PURE__ */ new Map();
2847
- const LOGIN_ATTEMPT_RETENTION_MS = 15 * 6e4;
3952
+ const loginAttempts = new LoginAttemptStore(dataDir);
3953
+ void loginAttempts.load();
2848
3954
  const HQ_SESSION_MAX_AGE_MS2 = 7 * 24 * 60 * 60 * 1e3;
3955
+ const HQ_SESSION_IDLE_TIMEOUT_MS2 = 30 * 6e4;
2849
3956
  const SESSION_CLEANUP_INTERVAL_MS = 6e4;
3957
+ const PENDING_2FA_TTL_MS2 = 5 * 6e4;
2850
3958
  const sessionCleanupTimer = setInterval(() => {
2851
- const cutoff = Date.now() - HQ_SESSION_MAX_AGE_MS2;
3959
+ const now = Date.now();
3960
+ const maxAgeCutoff = now - HQ_SESSION_MAX_AGE_MS2;
3961
+ const idleCutoff = now - HQ_SESSION_IDLE_TIMEOUT_MS2;
3962
+ const pending2faCutoff = now - PENDING_2FA_TTL_MS2;
2852
3963
  for (const [id, session] of sessions) {
2853
- if (session.createdAt < cutoff) sessions.delete(id);
3964
+ if (session.createdAt < maxAgeCutoff) {
3965
+ sessions.delete(id);
3966
+ continue;
3967
+ }
3968
+ if (session.pending2fa) {
3969
+ if (session.createdAt < pending2faCutoff) sessions.delete(id);
3970
+ continue;
3971
+ }
3972
+ if (session.lastSeenAt < idleCutoff) {
3973
+ sessions.delete(id);
3974
+ }
2854
3975
  }
2855
- const now = Date.now();
2856
- for (const [ip, entry] of loginAttempts) {
2857
- if (now - entry.lastAttempt > LOGIN_ATTEMPT_RETENTION_MS) loginAttempts.delete(ip);
3976
+ for (const [ip, entry] of loginAttempts.entries()) {
3977
+ if (now - entry.lastAttempt > 15 * 6e4) loginAttempts.delete(ip);
2858
3978
  }
2859
3979
  }, SESSION_CLEANUP_INTERVAL_MS);
2860
3980
  sessionCleanupTimer.unref?.();
2861
3981
  const persistence = createHqPersistence(dataDir);
2862
3982
  const auditLog = new HqCommandAuditLog(1e3, (entry) => persistence.commandLog.append(entry));
2863
3983
  void (async () => {
2864
- const projectsDir = path6.join(path6.dirname(dataDir), "projects");
2865
- const entries = await fs4.readdir(projectsDir, { withFileTypes: true }).catch(() => []);
3984
+ const projectsDir = path7.join(path7.dirname(dataDir), "projects");
3985
+ const entries = await fs5.readdir(projectsDir, { withFileTypes: true }).catch(() => []);
2866
3986
  await Promise.all(
2867
3987
  entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
2868
- const projectDir = path6.join(projectsDir, entry.name);
2869
- const metadataPath = path6.join(projectDir, ".mailbox-server.json");
2870
- const ownerPid = await fs4.readFile(metadataPath, "utf8").then((raw) => JSON.parse(raw).pid).catch(() => void 0);
3988
+ const projectDir = path7.join(projectsDir, entry.name);
3989
+ const metadataPath = path7.join(projectDir, ".mailbox-server.json");
3990
+ const ownerPid = await fs5.readFile(metadataPath, "utf8").then((raw) => JSON.parse(raw).pid).catch(() => void 0);
2871
3991
  if (ownerPid === void 0) return;
2872
3992
  let ownerAlive = false;
2873
3993
  try {
@@ -2877,7 +3997,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
2877
3997
  ownerAlive = false;
2878
3998
  }
2879
3999
  if (!ownerAlive) {
2880
- await fs4.rm(metadataPath, { force: true }).catch(() => {
4000
+ await fs5.rm(metadataPath, { force: true }).catch(() => {
2881
4001
  });
2882
4002
  return;
2883
4003
  }
@@ -2897,8 +4017,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
2897
4017
  const requestUrl = new URL(req.url ?? "/", `http://${host}:${port}`);
2898
4018
  const auth = authenticateBrowserRequest2(req, requestUrl, mutableAuth, sessions);
2899
4019
  const tokenMode = mutableAuth.browserTokens.size > 0;
2900
- const passwordMode = mutableAuth.passwordHash !== void 0;
2901
- if ((tokenMode || passwordMode) && auth === void 0) {
4020
+ if (hqAuthRequired(mutableAuth) && auth === void 0) {
2902
4021
  return {
2903
4022
  allowed: false,
2904
4023
  status: 401,
@@ -2924,7 +4043,10 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
2924
4043
  };
2925
4044
  const getMailboxGateway = (projectDir) => {
2926
4045
  const existing = mailboxGateways.get(projectDir);
2927
- if (existing) return existing;
4046
+ if (existing) {
4047
+ mailboxGatewayLastUsed.set(projectDir, Date.now());
4048
+ return existing;
4049
+ }
2928
4050
  const eventEmitter = new MailboxEventEmitter();
2929
4051
  const mailbox = createProjectMailbox({ projectDir, eventEmitter });
2930
4052
  const router = createMailboxHttpRouter({
@@ -2941,8 +4063,25 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
2941
4063
  });
2942
4064
  const gateway = { mailbox, router };
2943
4065
  mailboxGateways.set(projectDir, gateway);
4066
+ mailboxGatewayLastUsed.set(projectDir, Date.now());
2944
4067
  return gateway;
2945
4068
  };
4069
+ const evictIdleMailboxGateways = () => {
4070
+ const cutoff = Date.now() - MAILBOX_GATEWAY_IDLE_TTL_MS;
4071
+ for (const [projectDir, gateway] of [...mailboxGateways]) {
4072
+ if ((mailboxGatewayLastUsed.get(projectDir) ?? 0) > cutoff) continue;
4073
+ if (gateway.router.hasActiveStreams()) continue;
4074
+ mailboxGateways.delete(projectDir);
4075
+ mailboxGatewayLastUsed.delete(projectDir);
4076
+ gateway.router.close();
4077
+ void gateway.mailbox.close().catch(() => void 0);
4078
+ }
4079
+ };
4080
+ const mailboxGatewaySweepTimer = setInterval(
4081
+ evictIdleMailboxGateways,
4082
+ MAILBOX_GATEWAY_SWEEP_INTERVAL_MS
4083
+ );
4084
+ mailboxGatewaySweepTimer.unref?.();
2946
4085
  const alertEngine = new HqAlertEngine({
2947
4086
  onAlert: (alert) => {
2948
4087
  const msg = toAlertMessage(alert);
@@ -2989,8 +4128,13 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
2989
4128
  let changed = false;
2990
4129
  for (const [ws, client] of clients.entries()) {
2991
4130
  if (new Date(client.lastSeenAt).getTime() < cutoff) {
4131
+ const lostClient = client;
2992
4132
  ws.terminate();
2993
4133
  clients.delete(ws);
4134
+ detectLeaderLoss(lostClient, clients, browsers, "heartbeat-timeout", {
4135
+ eventLog,
4136
+ persistence
4137
+ });
2994
4138
  changed = true;
2995
4139
  continue;
2996
4140
  }
@@ -3010,8 +4154,13 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3010
4154
  }
3011
4155
  }
3012
4156
  if (client.capabilities.includes("session.summary") && client.sessions.size === 0 && Date.now() - Date.parse(client.connectedAt) > sessionSnapshotTtlMs) {
4157
+ const lostClient = client;
3013
4158
  ws.terminate();
3014
4159
  clients.delete(ws);
4160
+ detectLeaderLoss(lostClient, clients, browsers, "heartbeat-timeout", {
4161
+ eventLog,
4162
+ persistence
4163
+ });
3015
4164
  changed = true;
3016
4165
  }
3017
4166
  }
@@ -3023,6 +4172,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3023
4172
  host,
3024
4173
  listeningPort,
3025
4174
  trustedPublicOrigins,
4175
+ allowFileOrigin: options.allowFileOrigin,
3026
4176
  mutableAuth,
3027
4177
  sessions,
3028
4178
  loginAttempts,
@@ -3067,7 +4217,13 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3067
4217
  socket.destroy();
3068
4218
  return;
3069
4219
  }
3070
- if (!hasTrustedBrowserOrigin2(req, host, listeningPort, trustedPublicOrigins)) {
4220
+ if (!hasTrustedBrowserOrigin2(
4221
+ req,
4222
+ host,
4223
+ listeningPort,
4224
+ trustedPublicOrigins,
4225
+ options.allowFileOrigin
4226
+ )) {
3071
4227
  socket.write(
3072
4228
  "HTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + JSON.stringify({
3073
4229
  error: {
@@ -3080,19 +4236,43 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3080
4236
  return;
3081
4237
  }
3082
4238
  const tokenSet = pathname === "/ws/browser" ? mutableAuth.browserTokens : mutableAuth.clientTokens;
3083
- const needsAuth = pathname === "/ws/browser" ? options.requireBrowserAuth || tokenSet.size > 0 || mutableAuth.passwordHash !== void 0 : tokenSet.size > 0;
4239
+ const needsAuth = pathname === "/ws/browser" ? hqAuthRequired(mutableAuth, options.requireBrowserAuth) : hqClientAuthRequired(mutableAuth);
3084
4240
  if (needsAuth) {
3085
- const supplied = url.searchParams.get("token") ?? "";
4241
+ let supplied = url.searchParams.get("token") ?? "";
4242
+ if (supplied && pathname === "/ws/browser") {
4243
+ const requestHost = (req.headers.host ?? "").trim();
4244
+ let wsHostname = "";
4245
+ try {
4246
+ wsHostname = new URL(`http://${requestHost}`).hostname;
4247
+ } catch {
4248
+ }
4249
+ if (wsHostname && !isLoopbackHost3(wsHostname)) {
4250
+ console.warn(
4251
+ JSON.stringify({
4252
+ level: "warn",
4253
+ event: "hq.ws_token_from_query_rejected",
4254
+ message: "Browser token in WS query rejected on non-loopback \u2014 use session cookie instead.",
4255
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4256
+ })
4257
+ );
4258
+ supplied = "";
4259
+ }
4260
+ }
3086
4261
  const tokenValid = timingSafeTokenMatch(tokenSet, supplied) !== void 0;
3087
4262
  const cookieValid = pathname === "/ws/browser" && mutableAuth.cookieSecret !== void 0 && (() => {
3088
4263
  const cookies = parseCookieHeader2(req.headers.cookie);
3089
- const raw = cookies[HQ_SESSION_COOKIE2];
4264
+ const raw = cookies[HQ_SESSION_COOKIE2] ?? cookies["__Host-hq.session"];
3090
4265
  if (!raw) return false;
3091
4266
  const sessionId = parseHqSessionCookie2(raw, mutableAuth.cookieSecret);
3092
4267
  if (sessionId === void 0) return false;
3093
4268
  const session = sessions.get(sessionId);
3094
4269
  if (!session || Date.now() - session.createdAt >= HQ_SESSION_MAX_AGE_MS)
3095
4270
  return false;
4271
+ const now2 = Date.now();
4272
+ if (!session.pending2fa && now2 - session.lastSeenAt > HQ_SESSION_IDLE_TIMEOUT_MS)
4273
+ return false;
4274
+ session.lastSeenAt = now2;
4275
+ if (session.pending2fa) return false;
3096
4276
  if (session.kind === "token" && session.tokenId !== void 0) {
3097
4277
  return [...mutableAuth.browserTokenObjs.values()].some(
3098
4278
  (obj) => obj.id === session.tokenId
@@ -3119,7 +4299,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3119
4299
  });
3120
4300
  wss.on("connection", (ws, req, pathname) => {
3121
4301
  if (pathname === "/ws/browser") {
3122
- handleBrowser(ws, snapshotBroadcaster, browsers);
4302
+ handleBrowser(ws, snapshotBroadcaster, browsers, eventLog);
3123
4303
  } else {
3124
4304
  const token = new URL(req.url ?? "/", `http://${host}:${port}`).searchParams.get("token");
3125
4305
  handleClient(
@@ -3128,7 +4308,11 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3128
4308
  browsers,
3129
4309
  eventLog,
3130
4310
  {
3131
- ...token ? { token: mutableAuth.clientTokenObjs.get(token) } : {},
4311
+ // WS-044: `clientTokenObjs` is keyed on `hqTokenKey` — the verifier —
4312
+ // so the presented secret is hashed before lookup. Indexing with the
4313
+ // raw value silently returned undefined, which downgraded a scoped
4314
+ // token to "no capabilities record" at the capability gate.
4315
+ ...token ? { token: mutableAuth.clientTokenObjs.get(hqTokenVerifier2(token)) } : {},
3132
4316
  getOperatorPolicy: () => mutableAuth.operatorPolicyOverride
3133
4317
  },
3134
4318
  snapshotBroadcaster,
@@ -3143,7 +4327,26 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3143
4327
  dataDir,
3144
4328
  (next) => {
3145
4329
  authState.apply(next);
3146
- if (options.requireBrowserAuth && mutableAuth.browserTokens.size === 0 && mutableAuth.passwordHash === void 0) {
4330
+ const exposure = assessHqExposure2({
4331
+ host,
4332
+ hasBrowserTokens: mutableAuth.browserTokens.size > 0,
4333
+ hasPassword: mutableAuth.passwordHash !== void 0,
4334
+ allowInsecure: options.allowInsecureOpen
4335
+ });
4336
+ const previousFloor = mutableAuth.requireAuthFloor === true;
4337
+ mutableAuth.requireAuthFloor = exposure.kind === "refuse";
4338
+ if (mutableAuth.requireAuthFloor && !previousFloor) {
4339
+ console.error(
4340
+ JSON.stringify({
4341
+ level: "error",
4342
+ event: "hq.auth.open_mode_refused",
4343
+ message: "Last HQ credential removed while bound to a non-loopback address. Refusing to serve unauthenticated: requests will return 401 until a token or password is configured, or HQ is restarted with --host 127.0.0.1.",
4344
+ host,
4345
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4346
+ })
4347
+ );
4348
+ }
4349
+ if ((options.requireBrowserAuth || mutableAuth.requireAuthFloor) && mutableAuth.browserTokens.size === 0 && mutableAuth.passwordHash === void 0) {
3147
4350
  sessions.clear();
3148
4351
  for (const browser of browsers) browser.close(1008, "Browser authentication removed");
3149
4352
  }
@@ -3194,14 +4397,13 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3194
4397
  const addr = httpServer.address();
3195
4398
  const actualPort = typeof addr === "object" && addr ? addr.port : port;
3196
4399
  listeningPort = actualPort;
3197
- const browserToken = firstRunAuth.browserToken?.token ?? authFile.browserTokens?.find((t) => t.token.trim().length > 0)?.token;
3198
4400
  const clientToken = firstRunAuth.clientToken?.token ?? authFile.clientTokens?.find((t) => t.token.trim().length > 0)?.token;
3199
4401
  const hqUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${actualPort}`;
3200
4402
  await writeHqRuntimeMarker(dataDir, hqUrl).catch(() => {
3201
4403
  });
3202
- const browserTokenObj = mutableAuth.browserTokenObjs.get(browserToken ?? "");
4404
+ const browserTokenObj = (firstRunAuth.browserToken !== void 0 ? mutableAuth.browserTokenObjs.get(hqTokenKey3(firstRunAuth.browserToken)) : void 0) ?? mutableAuth.browserTokenObjs.values().next().value;
3203
4405
  let browserUrl;
3204
- if (browserToken && browserTokenObj) {
4406
+ if (browserTokenObj) {
3205
4407
  const code = bootstrapStore.issue({
3206
4408
  tokenId: browserTokenObj.id,
3207
4409
  ...browserTokenObj.capabilities !== void 0 ? { capabilities: browserTokenObj.capabilities } : {}
@@ -3246,7 +4448,9 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3246
4448
  clearInterval(browserHeartbeatTimer);
3247
4449
  clearInterval(timeseriesFlushTimer);
3248
4450
  clearInterval(mailboxGatewayRateLimitCleanup);
4451
+ clearInterval(mailboxGatewaySweepTimer);
3249
4452
  clearInterval(sessionCleanupTimer);
4453
+ void loginAttempts.flush();
3250
4454
  stopAlertEngine();
3251
4455
  authWatcher.close();
3252
4456
  snapshotBroadcaster.close();
@@ -3261,6 +4465,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3261
4465
  ({ mailbox }) => mailbox.close().catch(() => void 0)
3262
4466
  );
3263
4467
  mailboxGateways.clear();
4468
+ mailboxGatewayLastUsed.clear();
3264
4469
  void Promise.all([
3265
4470
  ...mailboxCloses,
3266
4471
  persistence.eventLog.drain(),
@@ -3276,7 +4481,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3276
4481
  port: actualPort,
3277
4482
  firstRunSetup: startupInfo
3278
4483
  });
3279
- resolve(handle);
4484
+ resolve2(handle);
3280
4485
  })();
3281
4486
  });
3282
4487
  });
@@ -3300,4 +4505,4 @@ export {
3300
4505
  startHqServer,
3301
4506
  HqInsecureExposureError2 as HqInsecureExposureError
3302
4507
  };
3303
- //# sourceMappingURL=chunk-OI7GWJDS.js.map
4508
+ //# sourceMappingURL=chunk-C5GH4YBL.js.map